From 4029828d0493f5fb39c72547add904cfc3fb207e Mon Sep 17 00:00:00 2001 From: shweet Date: Thu, 14 Aug 2025 21:39:17 -0400 Subject: [PATCH] The Omega Update(TM) Part 5 (Finishing) --- README.md | 3 +- src/COMMON.h | 72 ++++++++++- src/PACKED.h | 51 +++++++- src/anm2.cpp | 61 +++++++-- src/anm2.h | 13 +- src/canvas.cpp | 69 ++++------ src/canvas.h | 12 +- src/dialog.cpp | 15 +++ src/dialog.h | 3 +- src/editor.cpp | 3 +- src/editor.h | 2 +- src/generate_preview.cpp | 97 ++++---------- src/generate_preview.h | 3 + src/imgui.cpp | 273 ++++++++++++++++++++++++++------------- src/imgui.h | 153 +++++++++++++++++++--- src/main.cpp | 32 ++++- src/main.h | 5 + src/preview.cpp | 24 ++-- src/settings.cpp | 134 ++++++++++++++----- src/settings.h | 219 +++++++++++++++++++++++++++++-- src/state.cpp | 14 +- src/state.h | 4 +- src/texture.cpp | 22 +++- src/texture.h | 1 + 24 files changed, 966 insertions(+), 319 deletions(-) diff --git a/README.md b/README.md index 9c42891..3298515 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,10 @@ A reimplementation of *The Binding of Isaac: Rebirth*'s proprietary animation ed - New features - Can output .webm or *.png sequence - Cutting, copying and pasting + - Additional wizard options - Robust snapshot (undo/redo) system - Additional hotkeys/shortcuts - - Settings that will preserve on exit + - Settings that will preserve on exit (stored in %APPDATA% on Windows or ~/.local/share on Linux) ## Dependencies Download these from your package manager: diff --git a/src/COMMON.h b/src/COMMON.h index 6e9921f..7ea318a 100644 --- a/src/COMMON.h +++ b/src/COMMON.h @@ -23,7 +23,9 @@ #include #include #include -#include +#include + + typedef uint8_t u8; typedef uint16_t u16; @@ -123,6 +125,65 @@ static inline bool string_to_bool(const std::string& string) return lower == "true"; } +static inline std::string path_canonical_resolve +( + const std::string& inputPath, + const std::string& basePath = std::filesystem::current_path().string() +) +{ + auto strings_equal_ignore_case = [](std::string a, std::string b) { + auto to_lower = [](unsigned char c) { return static_cast(std::tolower(c)); }; + std::transform(a.begin(), a.end(), a.begin(), to_lower); + std::transform(b.begin(), b.end(), b.begin(), to_lower); + return a == b; + }; + + std::string sanitized = inputPath; + std::replace(sanitized.begin(), sanitized.end(), '\\', '/'); + + std::filesystem::path normalizedPath = sanitized; + std::filesystem::path absolutePath = normalizedPath.is_absolute() + ? normalizedPath + : (std::filesystem::path(basePath) / normalizedPath); + + std::error_code error; + if (std::filesystem::exists(absolutePath, error)) { + std::error_code canonicalError; + std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(absolutePath, canonicalError); + return (canonicalError ? absolutePath : canonicalPath).generic_string(); + } + + std::filesystem::path resolvedPath = absolutePath.root_path(); + std::filesystem::path remainingPath = absolutePath.relative_path(); + + for (const std::filesystem::path& segment : remainingPath) { + std::filesystem::path candidatePath = resolvedPath / segment; + if (std::filesystem::exists(candidatePath, error)) { + resolvedPath = candidatePath; + continue; + } + + bool matched = false; + if (std::filesystem::exists(resolvedPath, error) && std::filesystem::is_directory(resolvedPath, error)) { + for (const auto& directoryEntry : std::filesystem::directory_iterator(resolvedPath, error)) { + if (strings_equal_ignore_case(directoryEntry.path().filename().string(), segment.string())) { + resolvedPath = directoryEntry.path(); + matched = true; + break; + } + } + } + if (!matched) return sanitized; + } + + if (!std::filesystem::exists(resolvedPath, error)) + return sanitized; + + std::error_code canonicalError; + std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(resolvedPath, canonicalError); + return (canonicalError ? resolvedPath : canonicalPath).generic_string(); +}; + static inline std::string working_directory_from_file_set(const std::string& path) { std::filesystem::path filePath = path; @@ -138,13 +199,20 @@ static inline std::string path_extension_change(const std::string& path, const s return filePath.string(); } +static inline bool path_is_extension(const std::string& path, const std::string& extension) +{ + auto e = std::filesystem::path(path).extension().string(); + std::transform(e.begin(), e.end(), e.begin(), ::tolower); + return e == ("." + extension); +} + static inline bool path_exists(const std::filesystem::path& pathCheck) { std::error_code errorCode; return std::filesystem::exists(pathCheck, errorCode) && ((void)std::filesystem::status(pathCheck, errorCode), !errorCode); } -static inline bool path_valid(const std::filesystem::path& pathCheck) +static inline bool path_is_valid(const std::filesystem::path& pathCheck) { namespace fs = std::filesystem; std::error_code ec; diff --git a/src/PACKED.h b/src/PACKED.h index b826eb1..54c4cad 100644 --- a/src/PACKED.h +++ b/src/PACKED.h @@ -217,6 +217,7 @@ enum ShaderType { SHADER_LINE, SHADER_TEXTURE, + SHADER_GRID, SHADER_COUNT }; @@ -259,14 +260,62 @@ void main() } )"; +const std::string SHADER_GRID_VERTEX = R"( +#version 330 core +layout ( location = 0 ) in vec2 i_position; +out vec2 clip; + +void main() { + clip = i_position; + gl_Position = vec4(i_position, 0.0, 1.0); +} +)"; + +const std::string SHADER_GRID_FRAGMENT = R"( +#version 330 core +in vec2 clip; + +uniform mat4 u_model; // inverse of your world->clip matrix (MVP) +uniform vec2 u_size; // world-space cell size (e.g. 64,64) +uniform vec2 u_offset; // world-space grid offset (shifts entire grid) +uniform vec4 u_color; // RGBA + +out vec4 o_fragColor; + +void main() +{ + // clip -> world on z=0 plane + vec4 w = u_model * vec4(clip, 0.0, 1.0); + w /= w.w; + vec2 world = w.xy; + + // grid space + vec2 g = (world - u_offset) / u_size; + + vec2 d = abs(fract(g) - 0.5); + float distance = min(d.x, d.y); + + float fw = min(fwidth(g.x), fwidth(g.y)); + float alpha = 1.0 - smoothstep(0.0, fw, distance); + + if (alpha <= 0.0) discard; + o_fragColor = vec4(u_color.rgb, u_color.a * alpha); +} +)"; + + #define SHADER_UNIFORM_COLOR "u_color" #define SHADER_UNIFORM_TRANSFORM "u_transform" #define SHADER_UNIFORM_TINT "u_tint" #define SHADER_UNIFORM_COLOR_OFFSET "u_color_offset" +#define SHADER_UNIFORM_OFFSET "u_offset" +#define SHADER_UNIFORM_SIZE "u_size" +#define SHADER_UNIFORM_MODEL "u_model" #define SHADER_UNIFORM_TEXTURE "u_texture" const ShaderData SHADER_DATA[SHADER_COUNT] = { {SHADER_VERTEX, SHADER_FRAGMENT}, - {SHADER_VERTEX, SHADER_TEXTURE_FRAGMENT} + {SHADER_VERTEX, SHADER_TEXTURE_FRAGMENT}, + {SHADER_GRID_VERTEX, SHADER_GRID_FRAGMENT} }; \ No newline at end of file diff --git a/src/anm2.cpp b/src/anm2.cpp index c69fa9b..c7f242d 100644 --- a/src/anm2.cpp +++ b/src/anm2.cpp @@ -588,8 +588,8 @@ bool anm2_deserialize(Anm2* self, Resources* resources, const std::string& path) xmlAttribute = xmlAttribute->Next(); } - if (anm2Element == ANM2_ELEMENT_SPRITESHEET) - resources_texture_init(resources, spritesheet->path , id); + if (anm2Element == ANM2_ELEMENT_SPRITESHEET && resources) + resources_texture_init(resources, spritesheet->path, id); xmlChild = xmlElement->FirstChildElement(); @@ -630,14 +630,6 @@ bool anm2_deserialize(Anm2* self, Resources* resources, const std::string& path) void anm2_layer_add(Anm2* self) { - - - - - - - - s32 id = map_next_id_get(self->layers); self->layers[id] = Anm2Layer{}; @@ -1126,4 +1118,53 @@ void anm2_frame_bake(Anm2* self, Anm2Reference* reference, s32 interval, bool is delay += baked.delay; } +} + +void anm2_scale(Anm2* self, f32 scale) +{ + auto frame_scale = [&](Anm2Frame& frame) + { + frame.position = vec2((s32)(frame.position.x * scale), (s32)(frame.position.y * scale)); + frame.size = vec2((s32)(frame.size.x * scale), (s32)(frame.size.y * scale)); + frame.crop = vec2((s32)(frame.crop.x * scale), (s32)(frame.crop.y * scale)); + frame.pivot = vec2((s32)(frame.pivot.x * scale), (s32)(frame.pivot.y * scale)); + }; + + for (auto& [_, animation] : self->animations) + { + for (auto& frame : animation.rootAnimation.frames) + frame_scale(frame); + + for (auto& [_, layerAnimation] : animation.layerAnimations) + for (auto& frame : layerAnimation.frames) + frame_scale(frame); + + for (auto& [_, nullAnimation] : animation.nullAnimations) + for (auto& frame : nullAnimation.frames) + frame_scale(frame); + } +} + +void anm2_generate_from_grid(Anm2* self, Anm2Reference* reference, vec2 startPosition, vec2 size, vec2 pivot, s32 columns, s32 count, s32 delay) +{ + Anm2Item* item = anm2_item_from_reference(self, reference); + if (!item) return; + + Anm2Reference frameReference = *reference; + + for (s32 i = 0; i < count; i++) + { + const s32 row = i / columns; + const s32 column = i % columns; + + Anm2Frame frame{}; + + frame.delay = delay; + frame.pivot = pivot; + frame.size = size; + frame.crop = startPosition + vec2(size.x * column, size.y * row); + + anm2_frame_add(self, &frame, &frameReference); + frameReference.frameIndex++; + } } \ No newline at end of file diff --git a/src/anm2.h b/src/anm2.h index 3ce65d9..158a03d 100644 --- a/src/anm2.h +++ b/src/anm2.h @@ -20,6 +20,9 @@ #define ANM2_WRITE_INFO "Wrote anm2 to file: {}" #define ANM2_CREATED_ON_FORMAT "%d-%B-%Y %I:%M:%S %p" +#define ANM2_EXTENSION "anm2" +#define ANM2_SPRITESHEET_EXTENSION "png" + /* Elements */ #define ANM2_ELEMENT_LIST \ X(ANIMATED_ACTOR, "AnimatedActor") \ @@ -151,7 +154,7 @@ struct Anm2Frame { bool isVisible = true; bool isInterpolated = false; - f32 rotation = 1.0f; + f32 rotation{}; s32 delay = ANM2_FRAME_DELAY_MIN; s32 atFrame = INDEX_NONE; s32 eventID = ID_NONE; @@ -159,7 +162,7 @@ struct Anm2Frame vec2 pivot{}; vec2 position{}; vec2 size{}; - vec2 scale{}; + vec2 scale = {100, 100}; vec3 offsetRGB{}; vec4 tintRGBA = {1.0f, 1.0f, 1.0f, 1.0f}; }; @@ -269,7 +272,7 @@ Anm2Animation* anm2_animation_from_reference(Anm2* self, Anm2Reference* referenc Anm2Item* anm2_item_from_reference(Anm2* self, Anm2Reference* reference); Anm2Frame* anm2_frame_from_reference(Anm2* self, Anm2Reference* reference); s32 anm2_frame_index_from_time(Anm2* self, Anm2Reference reference, f32 time); -Anm2Frame* anm2_frame_add(Anm2* self, Anm2Frame* frame, Anm2Reference* reference, s32 time); +Anm2Frame* anm2_frame_add(Anm2* self, Anm2Frame* frame, Anm2Reference* reference, s32 time = 0.0f); void anm2_frame_erase(Anm2* self, Anm2Reference* reference); void anm2_frame_from_time(Anm2* self, Anm2Frame* frame, Anm2Reference reference, f32 time); void anm2_reference_clear(Anm2Reference* self); @@ -279,4 +282,6 @@ s32 anm2_animation_length_get(Anm2Animation* self); void anm2_animation_length_set(Anm2Animation* self); void anm2_animation_merge(Anm2* self, s32 animationID, const std::vector& mergeIDs, Anm2MergeType type); void anm2_frame_bake(Anm2* self, Anm2Reference* reference, s32 interval, bool isRoundScale, bool isRoundRotation); -void anm2_item_frame_set(Anm2* self, Anm2Reference* reference, const Anm2FrameChange& change, Anm2ChangeType type, s32 start, s32 count); \ No newline at end of file +void anm2_item_frame_set(Anm2* self, Anm2Reference* reference, const Anm2FrameChange& change, Anm2ChangeType type, s32 start, s32 count); +void anm2_scale(Anm2* self, f32 scale); +void anm2_generate_from_grid(Anm2* self, Anm2Reference* reference, vec2 startPosition, vec2 size, vec2 pivot, s32 columns, s32 count, s32 delay); \ No newline at end of file diff --git a/src/canvas.cpp b/src/canvas.cpp index 512e70f..1eb6116 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -69,6 +69,19 @@ void canvas_init(Canvas* self, const vec2& size) glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(f32), (void*)0); + // Grid + glGenVertexArrays(1, &self->gridVAO); + glBindVertexArray(self->gridVAO); + + glGenBuffers(1, &self->gridVBO); + glBindBuffer(GL_ARRAY_BUFFER, self->gridVBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_GRID_VERTICES), CANVAS_GRID_VERTICES, GL_STATIC_DRAW); + + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, (void*)0); + + glBindVertexArray(0); + // Texture glGenVertexArrays(1, &self->textureVAO); glGenBuffers(1, &self->textureVBO); @@ -93,7 +106,7 @@ void canvas_init(Canvas* self, const vec2& size) _canvas_texture_init(self, size); } -mat4 canvas_transform_get(Canvas* self, vec2& pan, f32& zoom, OriginType origin) +mat4 canvas_transform_get(Canvas* self, vec2 pan, f32 zoom, OriginType origin) { f32 zoomFactor = PERCENT_TO_UNIT(zoom); mat4 projection = glm::ortho(0.0f, self->size.x, 0.0f, self->size.y, -1.0f, 1.0f); @@ -136,55 +149,21 @@ void canvas_texture_set(Canvas* self) } } -void canvas_grid_draw(Canvas* self, GLuint& shader, mat4& transform, f32& zoom, ivec2& size, ivec2& offset, vec4& color) +void canvas_grid_draw(Canvas* self, GLuint& shader, mat4& transform, ivec2& size, ivec2& offset, vec4& color) { - if (size.x <= 0 || size.y <= 0) - return; // avoid div-by-zero - - std::vector vertices; - - vec2 gridSize = self->size * (PERCENT_TO_UNIT(CANVAS_ZOOM_MAX - zoom)); - - // First visible vertical line <= 0 - s32 startX = -(offset.x % size.x); - if (startX > 0) startX -= size.x; - - for (s32 x = startX; x <= gridSize.x; x += size.x) - { - vertices.push_back((f32)x); - vertices.push_back(0.0f); - vertices.push_back((f32)x); - vertices.push_back((f32)gridSize.y); - } - - // First visible horizontal line <= 0 - s32 startY = -(offset.y % size.y); - if (startY > 0) startY -= size.y; - - for (s32 y = startY; y <= gridSize.y; y += size.y) - { - vertices.push_back(0.0f); - vertices.push_back((f32)y); - vertices.push_back((f32)gridSize.x); - vertices.push_back((f32)y); - } - - s32 vertexCount = (s32)vertices.size() / 2; - - if (vertexCount == 0) - return; - - glBindVertexArray(self->gridVAO); - glBindBuffer(GL_ARRAY_BUFFER, self->gridVBO); - glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(f32), vertices.data(), GL_DYNAMIC_DRAW); + mat4 inverseTransform = glm::inverse(transform); glUseProgram(shader); - glBindVertexArray(self->gridVAO); - glUniformMatrix4fv(glGetUniformLocation(shader, SHADER_UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform)); - glUniform4f(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), color.r, color.g, color.b, color.a); - glDrawArrays(GL_LINES, 0, vertexCount); + glUniformMatrix4fv(glGetUniformLocation(shader, SHADER_UNIFORM_MODEL), 1, GL_FALSE, glm::value_ptr(inverseTransform)); + glUniform2f(glGetUniformLocation(shader, SHADER_UNIFORM_SIZE), size.x, size.y); + glUniform2f(glGetUniformLocation(shader, SHADER_UNIFORM_OFFSET), offset.x, offset.y); + glUniform4f(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), color.r, color.g, color.b, color.a); + + glBindVertexArray(self->gridVAO); + glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); + glUseProgram(0); } diff --git a/src/canvas.h b/src/canvas.h index effbcfb..73ab316 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -14,6 +14,7 @@ static const vec2 CANVAS_GRID_SIZE = {3200, 1600}; static const vec2 CANVAS_PIVOT_SIZE = {8, 8}; +static const vec2 CANVAS_SCALE_DEFAULT = {1.0f, 1.0f}; const f32 CANVAS_AXIS_VERTICES[] = { @@ -23,6 +24,13 @@ const f32 CANVAS_AXIS_VERTICES[] = 0.0f, CANVAS_LINE_LENGTH }; +const f32 CANVAS_GRID_VERTICES[] = +{ + -1.0f, -1.0f, + 3.0f, -1.0f, + -1.0f, 3.0f +}; + struct Canvas { GLuint fbo{}; @@ -41,13 +49,13 @@ struct Canvas }; void canvas_init(Canvas* self, const vec2& size); -mat4 canvas_transform_get(Canvas* self, vec2& pan, f32& zoom, OriginType origin); +mat4 canvas_transform_get(Canvas* self, vec2 pan, f32 zoom, OriginType origin); void canvas_clear(vec4& color); void canvas_bind(Canvas* self); void canvas_viewport_set(Canvas* self); void canvas_unbind(void); void canvas_texture_set(Canvas* self); -void canvas_grid_draw(Canvas* self, GLuint& shader, mat4& transform, f32& zoom, ivec2& size, ivec2& offset, vec4& color); +void canvas_grid_draw(Canvas* self, GLuint& shader, mat4& transform, ivec2& size, ivec2& offset, vec4& color); void canvas_axes_draw(Canvas* self, GLuint& shader, mat4& transform, vec4& color); void canvas_rect_draw(Canvas* self, const GLuint& shader, const mat4& transform, const vec4& color); void canvas_free(Canvas* self); diff --git a/src/dialog.cpp b/src/dialog.cpp index db1595b..81abdd9 100644 --- a/src/dialog.cpp +++ b/src/dialog.cpp @@ -1,5 +1,9 @@ #include "dialog.h" +#ifdef _WIN32 + #include +#endif + static void _dialog_callback(void* userdata, const char* const* filelist, s32 filter) { Dialog* self; @@ -67,6 +71,17 @@ void dialog_ffmpeg_path_set(Dialog* self) self->type = DIALOG_FFMPEG_PATH_SET; } +void dialog_explorer_open(const std::string& path) +{ +#ifdef _WIN32 + ShellExecuteA(NULL, "open", path.c_str(), NULL, NULL, SW_SHOWNORMAL); +#else + char cmd[512]; + snprintf(cmd, sizeof(cmd), "xdg-open \"%s\" &", path.c_str()); + system(cmd); +#endif +} + void dialog_reset(Dialog* self) { diff --git a/src/dialog.h b/src/dialog.h index 1fb15df..b65927c 100644 --- a/src/dialog.h +++ b/src/dialog.h @@ -53,4 +53,5 @@ void dialog_anm2_save(Dialog* self); void dialog_render_path_set(Dialog* self); void dialog_render_directory_set(Dialog* self); void dialog_ffmpeg_path_set(Dialog* self); -void dialog_reset(Dialog* self); \ No newline at end of file +void dialog_reset(Dialog* self); +void dialog_explorer_open(const std::string& path); \ No newline at end of file diff --git a/src/editor.cpp b/src/editor.cpp index ffc4abf..8479c79 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -17,6 +17,7 @@ void editor_draw(Editor* self) vec4& gridColor = self->settings->editorGridColor; GLuint& shaderLine = self->resources->shaders[SHADER_LINE]; GLuint& shaderTexture = self->resources->shaders[SHADER_TEXTURE]; + GLuint& shaderGrid = self->resources->shaders[SHADER_GRID]; mat4 transform = canvas_transform_get(&self->canvas, self->settings->editorPan, self->settings->editorZoom, ORIGIN_TOP_LEFT); canvas_texture_set(&self->canvas); @@ -48,7 +49,7 @@ void editor_draw(Editor* self) } if (self->settings->editorIsGrid) - canvas_grid_draw(&self->canvas, shaderLine, transform, self->settings->editorZoom, gridSize, gridOffset, gridColor); + canvas_grid_draw(&self->canvas, shaderGrid, transform, gridSize, gridOffset, gridColor); canvas_unbind(); } diff --git a/src/editor.h b/src/editor.h index 2d9c485..8963e0b 100644 --- a/src/editor.h +++ b/src/editor.h @@ -34,7 +34,7 @@ struct Editor GLuint textureVBO; GLuint borderVAO; GLuint borderVBO; - s32 spritesheetID = -1; + s32 spritesheetID = ID_NONE; }; void editor_init(Editor* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings); diff --git a/src/generate_preview.cpp b/src/generate_preview.cpp index a4acb5e..785c9a0 100644 --- a/src/generate_preview.cpp +++ b/src/generate_preview.cpp @@ -12,87 +12,40 @@ void generate_preview_init(GeneratePreview* self, Anm2* anm2, Anm2Reference* ref void generate_preview_draw(GeneratePreview* self) { - - /* TODO - f32& zoom = self->settings->previewZoom; - GLuint& shaderLine = self->resources->shaders[SHADER_LINE]; - GLuint& shaderTexture = self->resources->shaders[SHADER_TEXTURE]; - mat4 transform = canvas_transform_get(&self->canvas, self->settings->previewPan, self->settings->previewZoom, ORIGIN_CENTER); + static auto& columns = self->settings->generateColumns; + static auto& count = self->settings->generateCount; + static GLuint& shaderTexture = self->resources->shaders[SHADER_TEXTURE]; + const mat4 transform = canvas_transform_get(&self->canvas, {}, CANVAS_ZOOM_DEFAULT, ORIGIN_CENTER); + vec2 startPosition = {self->settings->generateStartPosition.x, self->settings->generateStartPosition.y}; + vec2 size = {self->settings->generateSize.x, self->settings->generateSize.y}; + vec2 pivot = {self->settings->generatePivot.x, self->settings->generatePivot.y}; + canvas_bind(&self->canvas); canvas_viewport_set(&self->canvas); canvas_clear(self->settings->previewBackgroundColor); - - self->time = std::clamp(self->time, 0.0f, 1.0f); - - Anm2Animation* animation = anm2_animation_from_reference(self->anm2, self->reference); - s32& animationID = self->reference->animationID; - - if (animation) - { - Anm2Frame root; - mat4 rootModel = mat4(1.0f); + + Anm2Item* item = anm2_item_from_reference(self->anm2, self->reference); + Texture* texture = map_find(self->resources->textures, self->anm2->layers[self->reference->itemID].spritesheetID); - anm2_frame_from_time(self->anm2, &root, Anm2Reference{animationID, ANM2_ROOT}, self->time); + if (item && texture && !texture->isInvalid) + { + const s32 index = std::clamp((s32)(self->time * count), 0, count); + const s32 row = index / columns; + const s32 column = index % columns; + vec2 crop = startPosition + vec2(size.x * column, size.y * row); - if (self->settings->previewIsRootTransform) - rootModel = quad_parent_model_get(root.position, vec2(0.0f), root.rotation, PERCENT_TO_UNIT(root.scale)); + vec2 uvMin = crop / vec2(texture->size); + vec2 uvMax = (crop + size) / vec2(texture->size); + f32 vertices[] = UV_VERTICES(uvMin, uvMax); - // Root - if (self->settings->previewIsTargets && animation->rootAnimation.isVisible && root.isVisible) - { - mat4 model = quad_model_get(PREVIEW_TARGET_SIZE, root.position, PREVIEW_TARGET_SIZE * 0.5f, root.rotation, PERCENT_TO_UNIT(root.scale)); - mat4 rootTransform = transform * model; - f32 vertices[] = ATLAS_UV_VERTICES(ATLAS_TARGET); - canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, rootTransform, vertices, PREVIEW_ROOT_COLOR); - } - - // Layers - for (auto [i, id] : self->anm2->layerMap) - { - Anm2Frame frame; - Anm2Item& layerAnimation = animation->layerAnimations[id]; - - if (!layerAnimation.isVisible || layerAnimation.frames.size() <= 0) - continue; - - anm2_frame_from_time(self->anm2, &frame, Anm2Reference{animationID, ANM2_LAYER, id}, self->time); - - if (!frame.isVisible) - continue; - - Texture* texture = map_find(self->resources->textures, self->anm2->layers[id].spritesheetID); - - if (!texture || texture->isInvalid) - continue; - - vec2 uvMin = frame.crop / vec2(texture->size); - vec2 uvMax = (frame.crop + frame.size) / vec2(texture->size); - f32 vertices[] = UV_VERTICES(uvMin, uvMax); - - mat4 model = quad_model_get(frame.size, frame.position, frame.pivot, frame.rotation, PERCENT_TO_UNIT(frame.scale)); - mat4 layerTransform = transform * (rootModel * model); - - canvas_texture_draw(&self->canvas, shaderTexture, texture->id, layerTransform, vertices, frame.tintRGBA, frame.offsetRGB); - - if (self->settings->previewIsBorder) - canvas_rect_draw(&self->canvas, shaderLine, layerTransform, PREVIEW_BORDER_COLOR); - - if (self->settings->previewIsPivots) - { - f32 vertices[] = ATLAS_UV_VERTICES(ATLAS_PIVOT); - mat4 pivotModel = quad_model_get(CANVAS_PIVOT_SIZE, frame.position, CANVAS_PIVOT_SIZE * 0.5f, frame.rotation, PERCENT_TO_UNIT(frame.scale)); - mat4 pivotTransform = transform * (rootModel * pivotModel); - canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, pivotTransform, vertices, PREVIEW_PIVOT_COLOR); - } - } - - - s32& animationOverlayID = self->animationOverlayID; - Anm2Animation* animationOverlay = map_find(self->anm2->animations, animationOverlayID); + mat4 model = quad_model_get(size, {}, pivot, {}, CANVAS_SCALE_DEFAULT); + mat4 generateTransform = transform * model; + canvas_texture_draw(&self->canvas, shaderTexture, texture->id, generateTransform, vertices, COLOR_OPAQUE, COLOR_OFFSET_NONE); + } + canvas_unbind(); - */ } void generate_preview_free(GeneratePreview* self) diff --git a/src/generate_preview.h b/src/generate_preview.h index b32e940..fa1b576 100644 --- a/src/generate_preview.h +++ b/src/generate_preview.h @@ -5,6 +5,9 @@ #include "settings.h" #include "canvas.h" +#define GENERATE_PREVIEW_TIME_MIN 0.0f +#define GENERATE_PREVIEW_TIME_MAX 1.0f + const vec2 GENERATE_PREVIEW_SIZE = {325, 215}; struct GeneratePreview diff --git a/src/imgui.cpp b/src/imgui.cpp index dbaa686..73dcf5b 100644 --- a/src/imgui.cpp +++ b/src/imgui.cpp @@ -19,6 +19,34 @@ static bool _imgui_window_color_from_position_get(SDL_Window* self, const vec2& return true; } +static void _imgui_anm2_new(Imgui* self, const std::string& path) +{ + *self->reference = Anm2Reference{}; + resources_textures_free(self->resources); + if (anm2_deserialize(self->anm2, self->resources, path)) + { + window_title_from_path_set(self->window, path); + snapshots_reset(self->snapshots); + imgui_log_push(self, std::format(IMGUI_LOG_FILE_OPEN_FORMAT, path)); + } + else + imgui_log_push(self, std::format(IMGUI_LOG_FILE_OPEN_FORMAT, path)); +} + +static void _imgui_spritesheet_add(Imgui* self, const std::string& path) +{ + std::filesystem::path workingPath = std::filesystem::current_path(); + std::string anm2WorkingPath = working_directory_from_file_set(self->anm2->path); + std::string spritesheetPath = std::filesystem::relative(path, anm2WorkingPath); + + s32 id = map_next_id_get(self->resources->textures); + self->anm2->spritesheets[id] = Anm2Spritesheet{}; + self->anm2->spritesheets[id].path = spritesheetPath; + resources_texture_init(self->resources, spritesheetPath, id); + + std::filesystem::current_path(workingPath); +} + template static void _imgui_clipboard_hovered_item_set(Imgui* self, const T& data) { @@ -107,6 +135,7 @@ static void _imgui_item_pre(const ImguiItem& self, ImguiItemType type) case IMGUI_INPUT_INT: case IMGUI_INPUT_TEXT: case IMGUI_DRAG_FLOAT: + case IMGUI_SLIDER_FLOAT: case IMGUI_COLOR_EDIT: ImGui::SetNextItemWidth(size.x); break; @@ -360,6 +389,8 @@ IMGUI_ITEM_VALUE_FUNCTION(_imgui_color_button, IMGUI_COLOR_BUTTON, vec4, ImGui:: IMGUI_ITEM_VALUE_FUNCTION(_imgui_checkbox, IMGUI_CHECKBOX, bool, ImGui::Checkbox(self.label_get(), &value)); IMGUI_ITEM_VALUE_CLAMP_FUNCTION(_imgui_input_int, IMGUI_INPUT_INT, s32, ImGui::InputInt(self.label.c_str(), &value, self.step, self.stepFast, self.flags)); IMGUI_ITEM_VALUE_CLAMP_FUNCTION(_imgui_input_int2, IMGUI_INPUT_INT, ivec2, ImGui::InputInt2(self.label.c_str(), value_ptr(value), self.flags)); +IMGUI_ITEM_VALUE_CLAMP_FUNCTION(_imgui_input_float, IMGUI_INPUT_FLOAT, f32, ImGui::InputFloat(self.label.c_str(), &value, self.step, self.stepFast, self.format_get(), self.flags)); +IMGUI_ITEM_VALUE_FUNCTION(_imgui_slider_float, IMGUI_SLIDER_FLOAT, f32, ImGui::SliderFloat(self.label_get(), &value, self.min, self.max, self.format_get(), self.flags)); IMGUI_ITEM_VALUE_FUNCTION(_imgui_drag_float, IMGUI_DRAG_FLOAT, f32, ImGui::DragFloat(self.label_get(), &value, self.speed, self.min, self.max, self.format_get())); IMGUI_ITEM_VALUE_FUNCTION(_imgui_drag_float2, IMGUI_DRAG_FLOAT, vec2, ImGui::DragFloat2(self.label_get(), value_ptr(value), self.speed, self.min, self.max, self.format_get())); IMGUI_ITEM_VALUE_FUNCTION(_imgui_color_edit3, IMGUI_COLOR_EDIT, vec3, ImGui::ColorEdit3(self.label_get(), value_ptr(value), self.flags)); @@ -522,6 +553,24 @@ static bool _imgui_option_popup(ImguiItem self, Imgui* imgui) return false; } +static void _imgui_context_menu(Imgui* self) +{ + if (!self->isContextualActionsEnabled) return; + + + if (_imgui_is_window_hovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + imgui_open_popup(IMGUI_CONTEXT_MENU.label_get()); + + if (imgui_begin_popup(IMGUI_CONTEXT_MENU.label_get(), self)) + { + _imgui_selectable(IMGUI_CUT, self); + _imgui_selectable(IMGUI_COPY, self); + _imgui_selectable(IMGUI_PASTE.copy({self->clipboard->item.type == CLIPBOARD_NONE}), self); + + imgui_end_popup(self); + } +} + static void _imgui_spritesheet_editor_set(Imgui* self, s32 id) { if (self->anm2->spritesheets.contains(id)) self->editor->spritesheetID = id; @@ -1036,6 +1085,11 @@ static void _imgui_timeline(Imgui* self) timeline_item_frames(Anm2Reference(animationID, ANM2_TRIGGERS), index); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, defaultItemSpacing); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, defaultWindowPadding); + _imgui_context_menu(self); + ImGui::PopStyleVar(2); + _imgui_end_child(); // IMGUI_TIMELINE_FRAMES_CHILD }; @@ -1056,6 +1110,7 @@ static void _imgui_timeline(Imgui* self) _imgui_end_child(); // IMGUI_TIMELINE_CHILD Anm2Frame* frame = anm2_frame_from_reference(self->anm2, self->reference); + Anm2Item* item = anm2_item_from_reference(self->anm2, self->reference); _imgui_begin_child(IMGUI_TIMELINE_ITEM_FOOTER_CHILD, self); _imgui_button(IMGUI_TIMELINE_ADD_ITEM, self); @@ -1066,7 +1121,7 @@ static void _imgui_timeline(Imgui* self) imgui_end_popup(self); } - if (_imgui_button(IMGUI_TIMELINE_REMOVE_ITEM.copy({itemType == ANM2_NONE || itemType == ANM2_ROOT || itemType == ANM2_TRIGGERS}), self)) + if (_imgui_button(IMGUI_TIMELINE_REMOVE_ITEM.copy({!item || itemType == ANM2_ROOT || itemType == ANM2_TRIGGERS}), self)) { switch (itemType) { @@ -1098,8 +1153,13 @@ static void _imgui_timeline(Imgui* self) self->preview->isPlaying = !self->preview->isPlaying; } - if (_imgui_button(IMGUI_ADD_FRAME, self)) - anm2_frame_add(self->anm2, nullptr, self->reference, (s32)time); + if (_imgui_button(IMGUI_ADD_FRAME.copy({!item}), self)) + { + Anm2Reference frameReference = *self->reference; + frameReference.frameIndex = std::clamp(frameReference.frameIndex, 0, (s32)item->frames.size() - 1); + Anm2Frame* addFrame = anm2_frame_from_reference(self->anm2, &frameReference); + anm2_frame_add(self->anm2, addFrame, &frameReference); + } if(_imgui_button(IMGUI_REMOVE_FRAME.copy({!frame}), self)) { @@ -1163,23 +1223,18 @@ static void _imgui_taskbar(Imgui* self) if (imgui_begin_popup(IMGUI_FILE.popup, self)) { - _imgui_selectable(IMGUI_NEW, self); // imgui_file_new - _imgui_selectable(IMGUI_OPEN, self); // imgui_file_open - - _imgui_selectable(IMGUI_SAVE, self); // imgui_file_save - _imgui_selectable(IMGUI_SAVE_AS, self); // imgui_file_save_as - + _imgui_selectable(IMGUI_NEW, self); + _imgui_selectable(IMGUI_OPEN, self); + _imgui_selectable(IMGUI_SAVE, self); + _imgui_selectable(IMGUI_SAVE_AS, self); + _imgui_selectable(IMGUI_EXPLORE_ANM2_LOCATION, self); + _imgui_selectable(IMGUI_EXIT, self); imgui_end_popup(self); } if (self->dialog->isSelected && self->dialog->type == DIALOG_ANM2_OPEN) { - *self->reference = Anm2Reference{}; - resources_textures_free(self->resources); - anm2_deserialize(self->anm2, self->resources, self->dialog->path); - window_title_from_path_set(self->window, self->dialog->path); - snapshots_reset(self->snapshots); - imgui_log_push(self, std::format(IMGUI_LOG_FILE_OPEN_FORMAT, self->dialog->path)); + _imgui_anm2_new(self, self->dialog->path); dialog_reset(self->dialog); } @@ -1190,13 +1245,21 @@ static void _imgui_taskbar(Imgui* self) imgui_log_push(self, std::format(IMGUI_LOG_FILE_SAVE_FORMAT, self->dialog->path)); dialog_reset(self->dialog); } - + + if (self->isTryQuit) imgui_open_popup(IMGUI_EXIT_CONFIRMATION.label); + + if (_imgui_option_popup(IMGUI_EXIT_CONFIRMATION, self)) + self->isQuit = true; + else + self->isTryQuit = false; + _imgui_selectable(IMGUI_WIZARD.copy({}), self); if (imgui_begin_popup(IMGUI_WIZARD.popup, self)) { _imgui_selectable(IMGUI_GENERATE_ANIMATION_FROM_GRID.copy({!item || (self->reference->itemType != ANM2_LAYER)}), self); _imgui_selectable(IMGUI_CHANGE_ALL_FRAME_PROPERTIES.copy({!item}), self); + _imgui_selectable(IMGUI_SCALE_ANM2.copy({self->anm2->animations.empty()}), self); _imgui_selectable(IMGUI_RENDER_ANIMATION.copy({!animation}), self); imgui_end_popup(self); @@ -1204,27 +1267,44 @@ static void _imgui_taskbar(Imgui* self) if (imgui_begin_popup_modal(IMGUI_GENERATE_ANIMATION_FROM_GRID.popup, self, IMGUI_GENERATE_ANIMATION_FROM_GRID.popupSize)) { + static auto& startPosition = self->settings->generateStartPosition; + static auto& size = self->settings->generateSize; + static auto& pivot = self->settings->generatePivot; + static auto& rows = self->settings->generateRows; + static auto& columns = self->settings->generateColumns; + static auto& count = self->settings->generateCount; + static auto& delay = self->settings->generateDelay; + static auto& time = self->generatePreview->time; + _imgui_begin_child(IMGUI_GENERATE_ANIMATION_FROM_GRID_OPTIONS_CHILD, self); - _imgui_input_int2(IMGUI_GENERATE_ANIMATION_FROM_GRID_START_POSITION, self, self->settings->generateStartPosition); - _imgui_input_int2(IMGUI_GENERATE_ANIMATION_FROM_GRID_FRAME_SIZE, self, self->settings->generateFrameSize); - _imgui_input_int2(IMGUI_GENERATE_ANIMATION_FROM_GRID_PIVOT, self, self->settings->generatePivot); - _imgui_input_int(IMGUI_GENERATE_ANIMATION_FROM_GRID_ROWS, self, self->settings->generateRows); - _imgui_input_int(IMGUI_GENERATE_ANIMATION_FROM_GRID_COLUMNS, self, self->settings->generateColumns); - _imgui_input_int - ( - IMGUI_GENERATE_ANIMATION_FROM_GRID_FRAME_COUNT.copy({.max = self->settings->generateRows * self->settings->generateColumns}), - self, self->settings->generateFrameCount - ); - _imgui_input_int(IMGUI_GENERATE_ANIMATION_FROM_GRID_DELAY, self, self->settings->generateDelay); + _imgui_input_int2(IMGUI_GENERATE_ANIMATION_FROM_GRID_START_POSITION, self, startPosition); + _imgui_input_int2(IMGUI_GENERATE_ANIMATION_FROM_GRID_SIZE, self, size); + _imgui_input_int2(IMGUI_GENERATE_ANIMATION_FROM_GRID_PIVOT, self, pivot); + _imgui_input_int(IMGUI_GENERATE_ANIMATION_FROM_GRID_ROWS, self, rows); + _imgui_input_int(IMGUI_GENERATE_ANIMATION_FROM_GRID_COLUMNS, self, columns); + _imgui_input_int(IMGUI_GENERATE_ANIMATION_FROM_GRID_COUNT.copy({.max = rows * columns}), self, count); + _imgui_input_int(IMGUI_GENERATE_ANIMATION_FROM_GRID_DELAY, self, delay); _imgui_end_child(); //IMGUI_GENERATE_ANIMATION_FROM_GRID_OPTIONS_CHILD ImGui::SameLine(); _imgui_begin_child(IMGUI_GENERATE_ANIMATION_FROM_GRID_PREVIEW_CHILD, self); + + generate_preview_draw(self->generatePreview); + ImGui::Image(self->generatePreview->canvas.texture, GENERATE_PREVIEW_SIZE); + + _imgui_begin_child(IMGUI_GENERATE_ANIMATION_FROM_GRID_SLIDER_CHILD, self); + _imgui_slider_float(IMGUI_GENERATE_ANIMATION_FROM_GRID_SLIDER, self, time); + _imgui_end_child(); // IMGUI_GENERATE_ANIMATION_FROM_GRID_SLIDER_CHILD + _imgui_end_child(); //IMGUI_GENERATE_ANIMATION_FROM_GRID_PREVIEW_CHILD _imgui_begin_child(IMGUI_FOOTER_CHILD, self); - if (_imgui_button(IMGUI_GENERATE_ANIMATION_FROM_GRID_GENERATE, self)) imgui_close_current_popup(self); + if (_imgui_button(IMGUI_GENERATE_ANIMATION_FROM_GRID_GENERATE, self)) + { + anm2_generate_from_grid(self->anm2, self->reference, startPosition, size, pivot, columns, count, delay); + imgui_close_current_popup(self); + } if (_imgui_button(IMGUI_POPUP_CANCEL, self)) imgui_close_current_popup(self); _imgui_end_child(); // IMGUI_FOOTER_CHILD @@ -1316,6 +1396,24 @@ static void _imgui_taskbar(Imgui* self) imgui_end_popup(self); } + if (imgui_begin_popup_modal(IMGUI_SCALE_ANM2.popup, self, IMGUI_SCALE_ANM2.popupSize)) + { + _imgui_begin_child(IMGUI_SCALE_ANM2_OPTIONS_CHILD, self); + _imgui_input_float(IMGUI_SCALE_ANM2_VALUE, self, self->settings->scaleValue); + _imgui_end_child(); // IMGUI_SCALE_ANM2_OPTIONS_CHILD + + _imgui_begin_child(IMGUI_FOOTER_CHILD, self); + if (_imgui_button(IMGUI_SCALE_ANM2_SCALE, self)) + { + anm2_scale(self->anm2, self->settings->scaleValue); + imgui_close_current_popup(self); + } + if (_imgui_button(IMGUI_POPUP_CANCEL, self)) imgui_close_current_popup(self); + _imgui_end_child(); // IMGUI_FOOTER_CHILD + + imgui_end_popup(self); + } + if (imgui_begin_popup_modal(IMGUI_RENDER_ANIMATION.popup, self, IMGUI_RENDER_ANIMATION.popupSize)) { _imgui_begin_child(IMGUI_RENDER_ANIMATION_CHILD, self); @@ -1331,7 +1429,11 @@ static void _imgui_taskbar(Imgui* self) } } - if (self->dialog->isSelected && self->dialog->type == DIALOG_RENDER_PATH_SET) + if + ( + self->dialog->isSelected && + (self->dialog->type == DIALOG_RENDER_PATH_SET || self->dialog->type == DIALOG_RENDER_DIRECTORY_SET) + ) { self->settings->renderPath = self->dialog->path; dialog_reset(self->dialog); @@ -1356,24 +1458,33 @@ static void _imgui_taskbar(Imgui* self) { bool isRenderStart = true; - switch (self->settings->renderType) + if (!std::filesystem::exists(self->settings->ffmpegPath)) { - case RENDER_PNG: - if (!std::filesystem::is_directory(self->settings->renderPath)) - { - imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_DIRECTORY_ERROR); - isRenderStart = false; - } - break; - case RENDER_GIF: - case RENDER_WEBM: - if (!path_valid(self->settings->renderPath)) - { - imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_PATH_ERROR); - isRenderStart = false; - } - default: - break; + imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_FFMPEG_PATH_ERROR); + isRenderStart = false; + } + + if (isRenderStart) + { + switch (self->settings->renderType) + { + case RENDER_PNG: + if (!std::filesystem::is_directory(self->settings->renderPath)) + { + imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_DIRECTORY_ERROR); + isRenderStart = false; + } + break; + case RENDER_GIF: + case RENDER_WEBM: + if (!path_is_valid(self->settings->renderPath)) + { + imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_PATH_ERROR); + isRenderStart = false; + } + default: + break; + } } if (isRenderStart) @@ -1577,6 +1688,8 @@ static void _imgui_animations(Imgui* self) ImGui::PopID(); }; + _imgui_context_menu(self); + _imgui_end_child(); // animationsChild Anm2Animation* animation = anm2_animation_from_reference(self->anm2, self->reference); @@ -1800,7 +1913,7 @@ static void _imgui_spritesheets(Imgui* self) Texture* texture = &self->resources->textures[id]; bool isContains = selectedIDs.contains(id); - _imgui_begin_child(IMGUI_SPRITESHEET_CHILD.copy({.size = {windowSize.x, IMGUI_SPRITESHEET_CHILD.size.y}}), self); + _imgui_begin_child(IMGUI_SPRITESHEET_CHILD, self); if (_imgui_checkbox(IMGUI_SPRITESHEET_SELECTED, self, isContains)) { @@ -1864,17 +1977,8 @@ static void _imgui_spritesheets(Imgui* self) if (self->dialog->isSelected && self->dialog->type == DIALOG_SPRITESHEET_ADD) { - std::filesystem::path workingPath = std::filesystem::current_path(); - std::string anm2WorkingPath = working_directory_from_file_set(self->anm2->path); - std::string spritesheetPath = std::filesystem::relative(self->dialog->path, anm2WorkingPath); - - s32 id = map_next_id_get(self->resources->textures); - self->anm2->spritesheets[id] = Anm2Spritesheet{}; - self->anm2->spritesheets[id].path = spritesheetPath; - resources_texture_init(self->resources, spritesheetPath, id); + _imgui_spritesheet_add(self, self->dialog->path); dialog_reset(self->dialog); - - std::filesystem::current_path(workingPath); } if (_imgui_button(IMGUI_SPRITESHEETS_RELOAD.copy({selectedIDs.empty()}), self)) @@ -1935,10 +2039,10 @@ static void _imgui_spritesheets(Imgui* self) { for (auto& id : selectedIDs) { - std::filesystem::path workingPath = std::filesystem::current_path(); - working_directory_from_file_set(self->anm2->path); Anm2Spritesheet* spritesheet = &self->anm2->spritesheets[id]; Texture* texture = &self->resources->textures[id]; + std::filesystem::path workingPath = std::filesystem::current_path(); + working_directory_from_file_set(self->anm2->path); texture_from_gl_write(texture, spritesheet->path); imgui_log_push(self, std::format(IMGUI_LOG_SPRITESHEET_SAVE_FORMAT, id, spritesheet->path)); std::filesystem::current_path(workingPath); @@ -2064,7 +2168,7 @@ static void _imgui_animation_preview(Imgui* self) const bool isDown = ImGui::IsKeyPressed(IMGUI_INPUT_DOWN); const bool isMod = ImGui::IsKeyDown(IMGUI_INPUT_SHIFT); const bool isZoomIn = ImGui::IsKeyDown(IMGUI_INPUT_ZOOM_IN); - const bool isZoomOut = ImGui::IsKeyDown(IMGUI_INPUT_ZOOM_IN); + const bool isZoomOut = ImGui::IsKeyDown(IMGUI_INPUT_ZOOM_OUT); const bool isMouseClick = ImGui::IsMouseClicked(ImGuiMouseButton_Left); const bool isMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); const bool isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); @@ -2194,7 +2298,7 @@ static void _imgui_spritesheet_editor(Imgui* self) const bool isMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); const bool isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); const bool isZoomIn = ImGui::IsKeyDown(IMGUI_INPUT_ZOOM_IN); - const bool isZoomOut = ImGui::IsKeyDown(IMGUI_INPUT_ZOOM_IN); + const bool isZoomOut = ImGui::IsKeyDown(IMGUI_INPUT_ZOOM_OUT); const bool isMod = ImGui::IsKeyDown(IMGUI_INPUT_SHIFT); const f32 mouseWheel = ImGui::GetIO().MouseWheel; const ImVec2 mouseDelta = ImGui::GetIO().MouseDelta; @@ -2358,22 +2462,6 @@ static void _imgui_log(Imgui* self) } } -static void _imgui_persistent(Imgui* self) -{ - if (!self->isContextualActionsEnabled) return; - - if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - imgui_open_popup(IMGUI_CONTEXT_MENU.label_get()); - - if (imgui_begin_popup(IMGUI_CONTEXT_MENU.label_get(), self)) - { - _imgui_selectable(IMGUI_CUT, self); - _imgui_selectable(IMGUI_COPY, self); - _imgui_selectable(IMGUI_PASTE.copy({self->clipboard->item.type == CLIPBOARD_NONE}), self); - - imgui_end_popup(self); - } -} static void _imgui_dock(Imgui* self) { @@ -2408,6 +2496,7 @@ void imgui_init Anm2Reference* reference, Editor* editor, Preview* preview, + GeneratePreview* generatePreview, Settings* settings, Snapshots* snapshots, Clipboard* clipboard, @@ -2423,6 +2512,7 @@ void imgui_init self->reference = reference; self->editor = editor; self->preview = preview; + self->generatePreview = generatePreview; self->settings = settings; self->snapshots = snapshots; self->clipboard = clipboard; @@ -2441,7 +2531,7 @@ void imgui_init io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigWindowsMoveFromTitleBarOnly = true; - ImGui::LoadIniSettingsFromDisk(SETTINGS_PATH); + ImGui::LoadIniSettingsFromDisk(settings_path_get().c_str()); } void imgui_update(Imgui* self) @@ -2453,7 +2543,6 @@ void imgui_update(Imgui* self) _imgui_taskbar(self); _imgui_dock(self); _imgui_log(self); - _imgui_persistent(self); if (self->isContextualActionsEnabled) { @@ -2483,15 +2572,20 @@ void imgui_update(Imgui* self) switch (event.type) { + case SDL_EVENT_DROP_FILE: + { + const char* droppedFile = event.drop.data; + + if (path_is_extension(droppedFile, ANM2_EXTENSION)) + _imgui_anm2_new(self, droppedFile); + else if (path_is_extension(droppedFile, ANM2_SPRITESHEET_EXTENSION)) + _imgui_spritesheet_add(self, droppedFile); + + break; + } case SDL_EVENT_QUIT: - if (!self->snapshots->undoStack.is_empty()) - { - if (imgui_is_popup_open(IMGUI_EXIT_CONFIRMATION.label_get())) - self->isQuit = true; - else - imgui_open_popup(IMGUI_EXIT_CONFIRMATION.label_get()); - } - else + imgui_quit(self); + if (imgui_is_popup_open(IMGUI_EXIT_CONFIRMATION.popup)) self->isQuit = true; break; default: @@ -2499,8 +2593,6 @@ void imgui_update(Imgui* self) } } - if (_imgui_option_popup(IMGUI_EXIT_CONFIRMATION, self)) - self->isQuit = true; } void imgui_draw(void) @@ -2513,7 +2605,6 @@ void imgui_free(void) { ImGui_ImplSDL3_Shutdown(); ImGui_ImplOpenGL3_Shutdown(); - - ImGui::SaveIniSettingsToDisk(SETTINGS_PATH); + ImGui::SaveIniSettingsToDisk(settings_path_get().c_str()); ImGui::DestroyContext(); } \ No newline at end of file diff --git a/src/imgui.h b/src/imgui.h index 933808c..c96de93 100644 --- a/src/imgui.h +++ b/src/imgui.h @@ -5,6 +5,7 @@ #include "editor.h" #include "ffmpeg.h" #include "preview.h" +#include "generate_preview.h" #include "resources.h" #include "settings.h" #include "snapshots.h" @@ -67,13 +68,16 @@ #define IMGUI_ACTION_TRIGGER_MOVE "Trigger AtFrame" #define IMGUI_ACTION_MOVE_PLAYHEAD "Move Playhead" +#define IMGUI_POPUP_FLAGS ImGuiWindowFlags_NoMove +#define IMGUI_POPUP_MODAL_FLAGS ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize + #define IMGUI_LOG_FILE_OPEN_FORMAT "Opened anm2: {}" #define IMGUI_LOG_FILE_SAVE_FORMAT "Saved anm2 to: {}" #define IMGUI_LOG_RENDER_ANIMATION_FRAMES_SAVE_FORMAT "Saved rendered frames to: {}" #define IMGUI_LOG_RENDER_ANIMATION_SAVE_FORMAT "Saved rendered animation to: {}" #define IMGUI_LOG_RENDER_ANIMATION_NO_ANIMATION_ERROR "No animation selected; rendering cancelled." #define IMGUI_LOG_RENDER_ANIMATION_NO_FRAMES_ERROR "No frames to render; rendering cancelled." -#define IMGUI_LOG_RENDER_ANIMATION_DIRECTORY_ERROR "Invalid directory! Make sure it's valid and you have write permissions." +#define IMGUI_LOG_RENDER_ANIMATION_DIRECTORY_ERROR "Invalid directory! Make sure it exists and you have write permissions." #define IMGUI_LOG_RENDER_ANIMATION_PATH_ERROR "Invalid path! Make sure it's valid and you have write permissions." #define IMGUI_LOG_RENDER_ANIMATION_FFMPEG_PATH_ERROR "Invalid FFmpeg path! Make sure you have it installed and the path is correct." #define IMGUI_LOG_RENDER_ANIMATION_FFMPEG_ERROR "FFmpeg could not render animation! Check paths or your FFmpeg installation." @@ -116,7 +120,7 @@ const ImVec4 IMGUI_TIMELINE_HEADER_FRAME_MULTIPLE_INACTIVE_COLOR = {0.113, 0.184 const ImVec4 IMGUI_ACTIVE_COLOR = {1.0, 1.0, 1.0, 1.0}; const ImVec4 IMGUI_INACTIVE_COLOR = {1.0, 1.0, 1.0, 0.25}; -const ImVec2 IMGUI_SPRITESHEET_PREVIEW_SIZE = {125.0, 125.0}; +const ImVec2 IMGUI_SPRITESHEET_PREVIEW_SIZE = {50.0, 50.0}; const ImVec2 IMGUI_TOOLTIP_OFFSET = {16, 8}; const vec2 IMGUI_SPRITESHEET_EDITOR_CROP_FORGIVENESS = {1, 1}; @@ -168,6 +172,7 @@ struct Imgui Anm2Reference* reference = nullptr; Editor* editor = nullptr; Preview* preview = nullptr; + GeneratePreview* generatePreview = nullptr; Settings* settings = nullptr; Snapshots* snapshots = nullptr; Clipboard* clipboard = nullptr; @@ -182,6 +187,7 @@ struct Imgui bool isCursorSet = false; bool isContextualActionsEnabled = true; bool isQuit = false; + bool isTryQuit = false; }; typedef void(*ImguiFunction)(Imgui*); @@ -235,6 +241,21 @@ static inline void imgui_file_save_as(Imgui* self) dialog_anm2_save(self->dialog); } +static inline void imgui_quit(Imgui* self) +{ + if (!self->snapshots->undoStack.is_empty()) + self->isTryQuit = true; + else + self->isQuit = true; +} + +static inline void imgui_explore(Imgui* self) +{ + std::filesystem::path filePath = self->anm2->path; + std::filesystem::path parentPath = filePath.parent_path(); + dialog_explorer_open(parentPath); +} + static inline void imgui_undo_push(Imgui* self, const std::string& action = SNAPSHOT_ACTION) { Snapshot snapshot = {*self->anm2, *self->reference, self->preview->time, action}; @@ -377,7 +398,7 @@ static inline bool imgui_begin_popup(const std::string& label, Imgui* imgui, ImV { imgui_pending_popup_process(imgui); if (size != ImVec2()) ImGui::SetNextWindowSizeConstraints(size, ImVec2(FLT_MAX, FLT_MAX)); - bool isActivated = ImGui::BeginPopup(label.c_str()); + bool isActivated = ImGui::BeginPopup(label.c_str(), IMGUI_POPUP_FLAGS); return isActivated; } @@ -385,7 +406,7 @@ static inline bool imgui_begin_popup_modal(const std::string& label, Imgui* imgu { imgui_pending_popup_process(imgui); if (size != ImVec2()) ImGui::SetNextWindowSizeConstraints(size, ImVec2(FLT_MAX, FLT_MAX)); - bool isActivated = ImGui::BeginPopupModal(label.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize); + bool isActivated = ImGui::BeginPopupModal(label.c_str(), nullptr, IMGUI_POPUP_MODAL_FLAGS); if (isActivated) imgui_contextual_actions_disable(imgui); return isActivated; } @@ -402,6 +423,8 @@ static inline void imgui_end_popup(Imgui* imgui) imgui_pending_popup_process(imgui); } + + enum ImguiItemType { IMGUI_ITEM, @@ -417,6 +440,8 @@ enum ImguiItemType IMGUI_CHECKBOX, IMGUI_INPUT_INT, IMGUI_INPUT_TEXT, + IMGUI_INPUT_FLOAT, + IMGUI_SLIDER_FLOAT, IMGUI_DRAG_FLOAT, IMGUI_COLOR_EDIT, IMGUI_COMBO, @@ -470,14 +495,15 @@ struct ImguiItem f32 speed = 0.25f; s32 step = 1; s32 stepFast = 10; - s32 border{}; - s32 max{}; s32 min{}; + s32 max{}; s32 value{}; + vec2 atlasOffset; + s32 border{}; s32 flags{}; s32 windowFlags{}; s32 rowCount = 0; - vec2 atlasOffset; + void construct() { @@ -638,6 +664,28 @@ IMGUI_ITEM(IMGUI_SAVE_AS, self.isShortcutInLabel = true ); +IMGUI_ITEM(IMGUI_EXPLORE_ANM2_LOCATION, + self.label = "E&xplore Anm2 Location", + self.tooltip = "Open the system's file explorer in the anm2's path.", + self.function = imgui_explore, + self.isSizeToText = true, + self.isSeparator = true +); + +IMGUI_ITEM(IMGUI_EXIT, + self.label = "&Exit ", + self.tooltip = "Exits the program.", + self.function = imgui_quit, + self.chord = ImGuiMod_Alt | ImGuiKey_F4, + self.isSizeToText = true, + self.isShortcutInLabel = true +); + +IMGUI_ITEM(IMGUI_EXIT_CONFIRMATION, + self.label = "Exit Confirmation", + self.text = "Unsaved changes will be lost!\nAre you sure you want to exit?" +); + IMGUI_ITEM(IMGUI_WIZARD, self.label = "&Wizard", self.tooltip = "Opens the wizard menu, for neat functions related to the .anm2.", @@ -647,12 +695,17 @@ IMGUI_ITEM(IMGUI_WIZARD, self.isSameLine = true ); +#define IMGUI_GENERATE_ANIMATION_FROM_GRID_PADDING 40 IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID, self.label = "&Generate Animation from Grid", self.tooltip = "Generate a new animation from grid values.", self.popup = "Generate Animation from Grid", self.popupType = IMGUI_POPUP_CENTER_WINDOW, - self.popupSize = {650, 215} + self.popupSize = + { + (GENERATE_PREVIEW_SIZE.x * 2) + IMGUI_GENERATE_ANIMATION_FROM_GRID_PADDING, + GENERATE_PREVIEW_SIZE.y + (IMGUI_FOOTER_CHILD.size.y * 2) + (IMGUI_GENERATE_ANIMATION_FROM_GRID_PADDING / 2) + } ); IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_OPTIONS_CHILD, @@ -670,8 +723,8 @@ IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_START_POSITION, self.tooltip = "Set the starting position on the layer's spritesheet for the generated animation." ); -IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_FRAME_SIZE, - self.label = "Frame Size", +IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_SIZE, + self.label = "Size", self.tooltip = "Set the size of each frame in the generated animation." ); @@ -692,9 +745,10 @@ IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_COLUMNS, self.max = 1000 ); -IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_FRAME_COUNT, +IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_COUNT, self.label = "Count", - self.tooltip = "Set how many frames will be made for the generated animation." + self.tooltip = "Set how many frames will be made for the generated animation.", + self.value = ANM2_FRAME_NUM_MIN ); IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_DELAY, @@ -713,9 +767,30 @@ IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_PREVIEW_CHILD, self.flags = true ); +IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_SLIDER_CHILD, + self.label = "## Generate Animation From Grid Slider Child", + self.size = + { + (IMGUI_GENERATE_ANIMATION_FROM_GRID.popupSize.x / 2) - (IMGUI_GENERATE_ANIMATION_FROM_GRID_PADDING / 2), + IMGUI_FOOTER_CHILD.size.y + }, + self.flags = true +); + +IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_SLIDER, + self.label = "## Generate Animation From Grid Slider", + self.tooltip = "Change the time of the generated animation preview.", + self.min = GENERATE_PREVIEW_TIME_MIN, + self.max = GENERATE_PREVIEW_TIME_MAX, + self.value = GENERATE_PREVIEW_TIME_MIN, + self.rowCount = 1, + self.flags = ImGuiSliderFlags_NoInput +); + IMGUI_ITEM(IMGUI_GENERATE_ANIMATION_FROM_GRID_GENERATE, self.label = "Generate", self.tooltip = "Generate an animation with the used settings.", + self.undoAction = "Generate Animation from Grid", self.rowCount = IMGUI_OPTION_POPUP_ROW_COUNT, self.isSameLine = true ); @@ -725,8 +800,7 @@ IMGUI_ITEM(IMGUI_CHANGE_ALL_FRAME_PROPERTIES, self.tooltip = "Change all frame properties in the selected animation item (or selected frame).", self.popup = "Change All Frame Properties", self.popupType = IMGUI_POPUP_CENTER_WINDOW, - self.popupSize = {500, 380}, - self.isSeparator = true + self.popupSize = {500, 380} ); IMGUI_ITEM(IMGUI_CHANGE_ALL_FRAME_PROPERTIES_CHILD, @@ -796,6 +870,39 @@ IMGUI_ITEM(IMGUI_CHANGE_ALL_FRAME_PROPERTIES_CANCEL, self.rowCount = IMGUI_CHANGE_ALL_FRAME_PROPERTIES_OPTIONS_ROW_COUNT ); +IMGUI_ITEM(IMGUI_SCALE_ANM2, + self.label = "&Scale Anm2", + self.tooltip = "Scale up all size and position-related frame properties in the anm2.", + self.popup = "Scale Anm2", + self.popupType = IMGUI_POPUP_CENTER_WINDOW, + self.popupSize = {260, 72}, + self.isSizeToText = true, + self.isSeparator = true +); + +IMGUI_ITEM(IMGUI_SCALE_ANM2_OPTIONS_CHILD, + self.label = "## Scale Anm2 Options Child", + self.size = {IMGUI_SCALE_ANM2.popupSize.x, IMGUI_SCALE_ANM2.popupSize.y - IMGUI_FOOTER_CHILD.size.y}, + self.flags = true +); + +IMGUI_ITEM(IMGUI_SCALE_ANM2_VALUE, + self.label = "Value", + self.tooltip = "The size and position-related frame properties in the anm2 will be scaled by this value.", + self.format = "%.2f", + self.value = 1, + self.step = 0.25, + self.stepFast = 1 +); + +IMGUI_ITEM(IMGUI_SCALE_ANM2_SCALE, + self.label = "Scale", + self.tooltip = "Scale the anm2 with the value specified.", + self.undoAction = "Scale Anm2", + self.rowCount = IMGUI_OPTION_POPUP_ROW_COUNT, + self.isSameLine = true +); + IMGUI_ITEM(IMGUI_RENDER_ANIMATION, self.label = "&Render Animation", self.tooltip = "Renders the current animation preview; output options can be customized.", @@ -1043,7 +1150,8 @@ IMGUI_ITEM(IMGUI_SPRITESHEETS_CHILD, self.label = "## Spritesheets Child", self. IMGUI_ITEM(IMGUI_SPRITESHEET_CHILD, self.label = "## Spritesheet Child", - self.size = {0, 175}, + self.rowCount = 1, + self.size = {0, IMGUI_SPRITESHEET_PREVIEW_SIZE.y + 40}, self.flags = true ); @@ -1253,6 +1361,7 @@ IMGUI_ITEM(IMGUI_FRAME_PROPERTIES_POSITION, self.label = "Position", self.tooltip = "Change the position of the selected frame.", self.undoAction = "Frame Position", + self.isUseItemActivated = true, self.format = "%.0f" ); @@ -1260,6 +1369,7 @@ IMGUI_ITEM(IMGUI_FRAME_PROPERTIES_CROP, self.label = "Crop", self.tooltip = "Change the crop position of the selected frame.", self.undoAction = "Frame Crop", + self.isUseItemActivated = true, self.format = "%.0f" ); @@ -1267,6 +1377,7 @@ IMGUI_ITEM(IMGUI_FRAME_PROPERTIES_SIZE, self.label = "Size", self.tooltip = "Change the size of the crop of the selected frame.", self.undoAction = "Frame Size", + self.isUseItemActivated = true, self.format = "%.0f" ); @@ -1274,6 +1385,7 @@ IMGUI_ITEM(IMGUI_FRAME_PROPERTIES_PIVOT, self.label = "Pivot", self.tooltip = "Change the pivot of the selected frame.", self.undoAction = "Frame Pivot", + self.isUseItemActivated = true, self.format = "%.0f" ); @@ -1282,6 +1394,7 @@ IMGUI_ITEM(IMGUI_FRAME_PROPERTIES_SCALE, self.tooltip = "Change the scale of the selected frame.", self.undoAction = "Frame Scale", self.format = "%.0f", + self.isUseItemActivated = true, self.value = 100 ); @@ -1289,6 +1402,7 @@ IMGUI_ITEM(IMGUI_FRAME_PROPERTIES_ROTATION, self.label = "Rotation", self.tooltip = "Change the rotation of the selected frame.", self.undoAction = "Frame Rotation", + self.isUseItemActivated = true, self.format = "%.0f" ); @@ -1296,6 +1410,7 @@ IMGUI_ITEM(IMGUI_FRAME_PROPERTIES_DELAY, self.label = "Duration", self.tooltip = "Change the duration of the selected frame.", self.undoAction = "Frame Duration", + self.isUseItemActivated = true, self.min = ANM2_FRAME_NUM_MIN, self.max = ANM2_FRAME_NUM_MAX, self.value = ANM2_FRAME_NUM_MIN @@ -1305,6 +1420,7 @@ IMGUI_ITEM(IMGUI_FRAME_PROPERTIES_TINT, self.label = "Tint", self.tooltip = "Change the tint of the selected frame.", self.undoAction = "Frame Tint", + self.isUseItemActivated = true, self.value = 1 ); @@ -1312,6 +1428,7 @@ IMGUI_ITEM(IMGUI_FRAME_PROPERTIES_COLOR_OFFSET, self.label = "Color Offset", self.tooltip = "Change the color offset of the selected frame.", self.undoAction = "Frame Color Offset", + self.isUseItemActivated = true, self.value = 0 ); @@ -1898,10 +2015,7 @@ IMGUI_ITEM(IMGUI_CHANGE_INPUT_INT, self.step = 0 ); -IMGUI_ITEM(IMGUI_EXIT_CONFIRMATION, - self.label = "Exit Confirmation", - self.text = "Unsaved changes will be lost!\nAre you sure you want to exit?" -); + #define IMGUI_OPTION_POPUP_ROW_COUNT 2 IMGUI_ITEM(IMGUI_POPUP_OK, @@ -1938,6 +2052,7 @@ void imgui_init Anm2Reference* reference, Editor* editor, Preview* preview, + GeneratePreview* generatePreview, Settings* settings, Snapshots* snapshots, Clipboard* clipboard, diff --git a/src/main.cpp b/src/main.cpp index 2c26fcc..ef1765f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,12 +1,42 @@ #include "main.h" +static bool _anm2_rescale(const std::string& file, f32 scale) +{ + Anm2 anm2; + + if (!anm2_deserialize(&anm2, nullptr, file)) return false; + anm2_scale(&anm2, scale); + return anm2_serialize(&anm2, file); +} + s32 main(s32 argc, char* argv[]) { State state; if (argc > 0 && argv[1]) - state.argument = argv[1]; + { + if (std::string(argv[1]) == ARGUMENT_RESCALE) + { + if (argv[2] && argv[3]) + { + if (_anm2_rescale(std::string(argv[2]), atof(argv[3]))) + { + log_info(std::format(ARGUMENT_RESCALE_ANM2_INFO, argv[2], argv[3])); + return EXIT_SUCCESS; + } + else + log_error(ARGUMENT_RESCALE_ANM2_ERROR); + } + else + log_error(ARGUMENT_RESCALE_ARGUMENT_ERROR); + + return EXIT_FAILURE; + } + else + if (argv[1]) + state.argument = argv[1]; + } init(&state); diff --git a/src/main.h b/src/main.h index 02bedad..0f10f02 100644 --- a/src/main.h +++ b/src/main.h @@ -1,3 +1,8 @@ #pragma once +#define ARGUMENT_RESCALE "--rescale" +#define ARGUMENT_RESCALE_ARGUMENT_ERROR "--rescale: specify both anm2 and scale arguments" +#define ARGUMENT_RESCALE_ANM2_ERROR "Unable to rescale anm2 {} by value {}. Make sure the file is valid." +#define ARGUMENT_RESCALE_ANM2_INFO "Scaled anm2 {} by {}" + #include "state.h" \ No newline at end of file diff --git a/src/preview.cpp b/src/preview.cpp index 6873eeb..9617229 100644 --- a/src/preview.cpp +++ b/src/preview.cpp @@ -83,9 +83,9 @@ void preview_draw(Preview* self) ivec2& gridSize = self->settings->previewGridSize; ivec2& gridOffset = self->settings->previewGridOffset; vec4& gridColor = self->settings->previewGridColor; - f32& zoom = self->settings->previewZoom; GLuint& shaderLine = self->resources->shaders[SHADER_LINE]; GLuint& shaderTexture = self->resources->shaders[SHADER_TEXTURE]; + GLuint& shaderGrid = self->resources->shaders[SHADER_GRID]; mat4 transform = canvas_transform_get(&self->canvas, self->settings->previewPan, self->settings->previewZoom, ORIGIN_CENTER); canvas_texture_set(&self->canvas); @@ -95,7 +95,7 @@ void preview_draw(Preview* self) canvas_clear(self->settings->previewBackgroundColor); if (self->settings->previewIsGrid) - canvas_grid_draw(&self->canvas, shaderLine, transform, zoom, gridSize, gridOffset, gridColor); + canvas_grid_draw(&self->canvas, shaderGrid, transform, gridSize, gridOffset, gridColor); if (self->settings->previewIsAxes) canvas_axes_draw(&self->canvas, shaderLine, transform, self->settings->previewAxesColor); @@ -136,20 +136,20 @@ void preview_draw(Preview* self) if (!frame.isVisible) continue; - Texture* texture = map_find(self->resources->textures, self->anm2->layers[id].spritesheetID); - - if (!texture || texture->isInvalid) - continue; - - vec2 uvMin = frame.crop / vec2(texture->size); - vec2 uvMax = (frame.crop + frame.size) / vec2(texture->size); - f32 vertices[] = UV_VERTICES(uvMin, uvMax); - mat4 model = quad_model_get(frame.size, frame.position, frame.pivot, frame.rotation, PERCENT_TO_UNIT(frame.scale)); mat4 layerTransform = transform * (rootModel * model); - canvas_texture_draw(&self->canvas, shaderTexture, texture->id, layerTransform, vertices, frame.tintRGBA, frame.offsetRGB); + Texture* texture = map_find(self->resources->textures, self->anm2->layers[id].spritesheetID); + + if (texture && !texture->isInvalid) + { + vec2 uvMin = frame.crop / vec2(texture->size); + vec2 uvMax = (frame.crop + frame.size) / vec2(texture->size); + f32 vertices[] = UV_VERTICES(uvMin, uvMax); + canvas_texture_draw(&self->canvas, shaderTexture, texture->id, layerTransform, vertices, frame.tintRGBA, frame.offsetRGB); + } + if (self->settings->previewIsBorder) canvas_rect_draw(&self->canvas, shaderLine, layerTransform, PREVIEW_BORDER_COLOR); diff --git a/src/settings.cpp b/src/settings.cpp index cde70e2..67c441e 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -72,6 +72,14 @@ static void _settings_setting_load(Settings* self, const std::string& line) } } +std::string settings_path_get(void) +{ + char* path = SDL_GetPrefPath("", SETTINGS_FOLDER); + std::string filePath = std::string(path) + SETTINGS_PATH; + SDL_free(path); + return filePath; +} + static void _settings_setting_write(Settings* self, std::ostream& out, SettingsEntry entry) { u8* selfPointer = (u8*)self; @@ -136,59 +144,119 @@ static void _settings_setting_write(Settings* self, std::ostream& out, SettingsE void settings_save(Settings* self) { - std::ifstream input(SETTINGS_PATH); - std::string oldContents; + const std::string path = settings_path_get(); + const std::filesystem::path filesystemPath(path); + const std::filesystem::path directory = filesystemPath.parent_path(); - if (!input) + if (!directory.empty()) { - log_error(std::format(SETTINGS_INIT_ERROR, SETTINGS_PATH)); + std::error_code errorCode; + std::filesystem::create_directories(directory, errorCode); + if (errorCode) + { + log_error(std::format(SETTINGS_DIRECTORY_ERROR, directory.string(), errorCode.message())); + return; + } + } + + std::string data; + if (std::filesystem::exists(filesystemPath)) + { + if (std::ifstream in(path, std::ios::binary); in) + data.assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); + } + + std::filesystem::path temp = filesystemPath; + temp += SETTINGS_TEMPORARY_EXTENSION; + + std::ofstream out(temp, std::ios::binary | std::ios::trunc); + if (!out) + { + log_error(std::format(SETTINGS_INIT_ERROR, temp.string())); return; } - oldContents.assign((std::istreambuf_iterator(input)), std::istreambuf_iterator()); - input.close(); - - std::ofstream output(SETTINGS_PATH); - - if (!output) - { - log_error(std::format(SETTINGS_INIT_ERROR, SETTINGS_PATH)); - return; - } - - output << SETTINGS_SECTION << "\n"; - + out << SETTINGS_SECTION << "\n"; for (s32 i = 0; i < SETTINGS_COUNT; i++) - _settings_setting_write(self, output, SETTINGS_ENTRIES[i]); + _settings_setting_write(self, out, SETTINGS_ENTRIES[i]); - output << "\n" << SETTINGS_SECTION_IMGUI << "\n"; - output << oldContents; + out << "\n" << SETTINGS_SECTION_IMGUI << "\n"; + out << data; - output.close(); + out.flush(); + + if (!out.good()) + { + log_error(std::format(SETTINGS_SAVE_ERROR, temp.string())); + return; + } + + out.close(); + + std::error_code errorCode; + std::filesystem::rename(temp, filesystemPath, errorCode); + if (errorCode) + { + // Windows can block rename if target exists; try remove+rename + std::filesystem::remove(filesystemPath, errorCode); + errorCode = {}; + std::filesystem::rename(temp, filesystemPath, errorCode); + if (errorCode) + { + log_error(std::format(SETTINGS_SAVE_FINALIZE_ERROR, filesystemPath.string(), errorCode.message())); + std::filesystem::remove(temp); + return; + } + } + + log_info(std::format(SETTINGS_SAVE_INFO, path)); } void settings_init(Settings* self) { - std::ifstream file(SETTINGS_PATH); + const std::string path = settings_path_get(); + std::ifstream file(path, std::ios::binary); + std::istream* in = nullptr; + std::istringstream defaultSettings; - if (!file) + if (file) { - log_error(std::format(SETTINGS_INIT_ERROR, SETTINGS_PATH)); - return; + log_info(std::format(SETTINGS_INIT_INFO, path)); + in = &file; + } + else + { + log_error(std::format(SETTINGS_INIT_ERROR, path)); + log_info(SETTINGS_DEFAULT_INFO); + defaultSettings.str(SETTINGS_DEFAULT); + in = &defaultSettings; } std::string line; bool inSettingsSection = false; - while (std::getline(file, line)) - { + while (std::getline(*in, line)) + { if (line == SETTINGS_SECTION) - { - inSettingsSection = true; - continue; - } - + { + inSettingsSection = true; + continue; + } if (line == SETTINGS_SECTION_IMGUI) break; - if (inSettingsSection) _settings_setting_load(self, line); + if (inSettingsSection) _settings_setting_load(self, line); + } + + // Save default settings + if (!file) + { + std::ofstream out(path, std::ios::binary | std::ios::trunc); + if (out) + { + out << SETTINGS_DEFAULT; + out.flush(); + log_info(std::format(SETTINGS_SAVE_INFO, path)); + } + else + log_error(std::format(SETTINGS_DEFAULT_ERROR, path)); } } \ No newline at end of file diff --git a/src/settings.h b/src/settings.h index 1ec1664..3290028 100644 --- a/src/settings.h +++ b/src/settings.h @@ -8,9 +8,19 @@ #define SETTINGS_BUFFER_ITEM 0xFF #define SETTINGS_SECTION "[Settings]" #define SETTINGS_SECTION_IMGUI "# Dear ImGui" -#define SETTINGS_INIT_ERROR "Failed to read settings file! ({})" -#define SETTINGS_PATH "settings.ini" +#define SETTINGS_INIT_ERROR "Failed to read settings file: {}" +#define SETTINGS_DEFAULT_ERROR "Failed to write default settings file: {}" +#define SETTINGS_SAVE_ERROR "Failed to write settings file: {}" +#define SETTINGS_SAVE_FINALIZE_ERROR "Failed to write settings file: {} ({})" #define SETTINGS_FLOAT_FORMAT "{:.3f}" +#define SETTINGS_INIT_INFO "Initialized settings from: {}" +#define SETTINGS_DEFAULT_INFO "Using default settings" +#define SETTINGS_DIRECTORY_ERROR "Failed to create settings directory: {} ({})" +#define SETTINGS_SAVE_INFO "Saved settings to: {}" + +#define SETTINGS_FOLDER "anm2ed" +#define SETTINGS_PATH "settings.ini" +#define SETTINGS_TEMPORARY_EXTENSION ".tmp" struct SettingsEntry { @@ -48,6 +58,7 @@ struct Settings bool changeIsVisible{}; bool changeIsInterpolated{}; s32 changeNumberFrames = 1; + f32 scaleValue = 1.0f; bool previewIsAxes = true; bool previewIsGrid = true; bool previewIsRootTransform = false; @@ -64,11 +75,11 @@ struct Settings vec4 previewAxesColor = {1.0, 1.0, 1.0, 0.125}; vec4 previewBackgroundColor = {0.113, 0.184, 0.286, 1.0}; ivec2 generateStartPosition = {0, 0}; - ivec2 generateFrameSize = {64, 64}; + ivec2 generateSize = {64, 64}; ivec2 generatePivot = {32, 32}; s32 generateRows = 4; s32 generateColumns = 4; - s32 generateFrameCount = 16; + s32 generateCount = 16; s32 generateDelay = 1; bool editorIsGrid = true; bool editorIsGridSnap = true; @@ -89,7 +100,7 @@ struct Settings s32 renderType = RENDER_PNG; std::string renderPath = "."; std::string renderFormat = "{}.png"; - std::string ffmpegPath = "/usr/bin/ffmpeg"; + std::string ffmpegPath{}; }; const SettingsEntry SETTINGS_ENTRIES[] = @@ -121,6 +132,7 @@ const SettingsEntry SETTINGS_ENTRIES[] = {"changeIsVisible", TYPE_BOOL, offsetof(Settings, changeIsVisibleSet)}, {"changeIsInterpolated", TYPE_BOOL, offsetof(Settings, changeIsInterpolatedSet)}, {"changeNumberFrames", TYPE_INT, offsetof(Settings, changeNumberFrames)}, + {"scaleValue", TYPE_FLOAT, offsetof(Settings, scaleValue)}, {"previewIsAxes", TYPE_BOOL, offsetof(Settings, previewIsAxes)}, {"previewIsGrid", TYPE_BOOL, offsetof(Settings, previewIsGrid)}, {"previewIsRootTransform", TYPE_BOOL, offsetof(Settings, previewIsRootTransform)}, @@ -136,12 +148,12 @@ const SettingsEntry SETTINGS_ENTRIES[] = {"previewGridColor", TYPE_VEC4, offsetof(Settings, previewGridColor)}, {"previewAxesColor", TYPE_VEC4, offsetof(Settings, previewAxesColor)}, {"previewBackgroundColor", TYPE_VEC4, offsetof(Settings, previewBackgroundColor)}, - {"generateStartPosition", TYPE_VEC2, offsetof(Settings, generateStartPosition)}, - {"generateFrameSize", TYPE_VEC2, offsetof(Settings, generateFrameSize)}, - {"generatePivot", TYPE_VEC2, offsetof(Settings, generatePivot)}, + {"generateStartPosition", TYPE_IVEC2, offsetof(Settings, generateStartPosition)}, + {"generateSize", TYPE_IVEC2, offsetof(Settings, generateSize)}, + {"generatePivot", TYPE_IVEC2, offsetof(Settings, generatePivot)}, {"generateRows", TYPE_INT, offsetof(Settings, generateRows)}, {"generateColumns", TYPE_INT, offsetof(Settings, generateColumns)}, - {"generateFrameCount", TYPE_INT, offsetof(Settings, generateFrameCount)}, + {"generateCount", TYPE_INT, offsetof(Settings, generateCount)}, {"generateDelay", TYPE_INT, offsetof(Settings, generateDelay)}, {"editorIsGrid", TYPE_BOOL, offsetof(Settings, editorIsGrid)}, {"editorIsGridSnap", TYPE_BOOL, offsetof(Settings, editorIsGridSnap)}, @@ -166,5 +178,192 @@ const SettingsEntry SETTINGS_ENTRIES[] = }; constexpr s32 SETTINGS_COUNT = (s32)std::size(SETTINGS_ENTRIES); +const std::string SETTINGS_DEFAULT = R"( +[Settings] +windowX=1920 +windowY=1080 +playbackIsLoop=true +playbackIsClampPlayhead=false +changeIsCrop=false +changeIsSize=false +changeIsPosition=false +changeIsPivot=false +changeIsScale=false +changeIsRotation=false +changeIsDelay=false +changeIsTint=false +changeIsColorOffset=false +changeIsVisibleSet=false +changeIsInterpolatedSet=false +changeIsFromSelectedFrame=false +changeCropX=0.000 +changeCropY=0.000 +changeSizeX=0.000 +changeSizeY=0.000 +changePositionX=0.000 +changePositionY=0.000 +changePivotX=0.000 +changePivotY=0.000 +changeScaleX=0.000 +changeScaleY=0.000 +changeRotation=0.000 +changeDelay=1 +changeTintR=0.000 +changeTintG=0.000 +changeTintB=0.000 +changeTintA=0.000 +changeColorOffsetR=0.000 +changeColorOffsetG=0.000 +changeColorOffsetB=0.000 +changeIsVisible=false +changeIsInterpolated=false +changeNumberFrames=1 +scaleValue=1.000 +previewIsAxes=true +previewIsGrid=false +previewIsRootTransform=true +previewIsTriggers=false +previewIsPivots=false +previewIsTargets=true +previewIsBorder=false +previewOverlayTransparency=255.000 +previewZoom=400.000 +previewPanX=0.000 +previewPanY=0.000 +previewGridSizeX=32 +previewGridSizeY=32 +previewGridOffsetX=0 +previewGridOffsetY=0 +previewGridColorR=1.000 +previewGridColorG=1.000 +previewGridColorB=1.000 +previewGridColorA=0.125 +previewAxesColorR=1.000 +previewAxesColorG=1.000 +previewAxesColorB=1.000 +previewAxesColorA=0.125 +previewBackgroundColorR=0.114 +previewBackgroundColorG=0.184 +previewBackgroundColorB=0.286 +previewBackgroundColorA=1.000 +generateStartPositionX=0.000 +generateStartPositionY=0.000 +generateSizeX=0.000 +generateSizeY=0.000 +generatePivotX=0.000 +generatePivotY=0.000 +generateRows=4 +generateColumns=4 +generateCount=16 +generateDelay=1 +editorIsGrid=true +editorIsGridSnap=true +editorIsBorder=true +editorZoom=400.000 +editorPanX=0.000 +editorPanY=0.000 +editorGridSizeX=32 +editorGridSizeY=32 +editorGridOffsetX=0 +editorGridOffsetY=0 +editorGridColorR=1.000 +editorGridColorG=1.000 +editorGridColorB=1.000 +editorGridColorA=0.125 +editorBackgroundColorR=0.113 +editorBackgroundColorG=0.183 +editorBackgroundColorB=0.286 +editorBackgroundColorA=1.000 +mergeType=1 +mergeIsDeleteAnimationsAfter=false +bakeInterval=1 +bakeRoundScale=true +bakeRoundRotation=true +tool=0 +toolColorR=0.000 +toolColorG=0.000 +toolColorB=0.000 +toolColorA=1.000 +renderType=0 +renderPath=. +renderFormat={}.png +ffmpegPath=/usr/bin/ffmpeg + +# Dear ImGui +[Window][## Window] +Pos=0,32 +Size=1918,1032 +Collapsed=0 + +[Window][Debug##Default] +Pos=60,60 +Size=400,400 +Collapsed=0 + +[Window][Tools] +Pos=8,40 +Size=39,654 +Collapsed=0 +DockId=0x0000000B,0 + +[Window][Animations] +Pos=1452,388 +Size=458,306 +Collapsed=0 +DockId=0x0000000A,0 + +[Window][Events] +Pos=1025,348 +Size=425,346 +Collapsed=0 +DockId=0x00000008,0 + +[Window][Spritesheets] +Pos=1452,40 +Size=458,346 +Collapsed=0 +DockId=0x00000009,0 + +[Window][Animation Preview] +Pos=49,40 +Size=974,654 +Collapsed=0 +DockId=0x0000000C,0 + +[Window][Spritesheet Editor] +Pos=49,40 +Size=974,654 +Collapsed=0 +DockId=0x0000000C,1 + +[Window][Timeline] +Pos=8,696 +Size=1902,360 +Collapsed=0 +DockId=0x00000004,0 + +[Window][Frame Properties] +Pos=1025,40 +Size=425,306 +Collapsed=0 +DockId=0x00000007,0 + +[Docking][Data] +DockSpace ID=0xFC02A410 Window=0x0E46F4F7 Pos=8,40 Size=1902,1016 Split=Y + DockNode ID=0x00000003 Parent=0xFC02A410 SizeRef=1902,654 Split=X + DockNode ID=0x00000001 Parent=0x00000003 SizeRef=1442,1016 Split=X Selected=0x024430EF + DockNode ID=0x00000005 Parent=0x00000001 SizeRef=1015,654 Split=X Selected=0x024430EF + DockNode ID=0x0000000B Parent=0x00000005 SizeRef=39,654 Selected=0x18A5FDB9 + DockNode ID=0x0000000C Parent=0x00000005 SizeRef=974,654 CentralNode=1 Selected=0x024430EF + DockNode ID=0x00000006 Parent=0x00000001 SizeRef=425,654 Split=Y Selected=0x754E368F + DockNode ID=0x00000007 Parent=0x00000006 SizeRef=631,306 Selected=0x754E368F + DockNode ID=0x00000008 Parent=0x00000006 SizeRef=631,346 Selected=0x8A65D963 + DockNode ID=0x00000002 Parent=0x00000003 SizeRef=458,1016 Split=Y Selected=0x4EFD0020 + DockNode ID=0x00000009 Parent=0x00000002 SizeRef=634,346 Selected=0x4EFD0020 + DockNode ID=0x0000000A Parent=0x00000002 SizeRef=634,306 Selected=0xC1986EE2 + DockNode ID=0x00000004 Parent=0xFC02A410 SizeRef=1902,360 Selected=0x4F89F0DC +)"; + void settings_save(Settings* self); -void settings_init(Settings* self); \ No newline at end of file +void settings_init(Settings* self); +std::string settings_path_get(void); \ No newline at end of file diff --git a/src/state.cpp b/src/state.cpp index deb60b5..b33d0ed 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -24,9 +24,10 @@ static void _draw(State* self) void init(State* self) { - settings_init(&self->settings); - + log_info(STATE_INIT_INFO); + + settings_init(&self->settings); if (!SDL_Init(SDL_INIT_VIDEO)) { @@ -76,8 +77,6 @@ void init(State* self) SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); - glewInit(); - self->glContext = SDL_GL_CreateContext(self->window); if (!self->glContext) @@ -86,6 +85,8 @@ void init(State* self) quit(self); } + glewInit(); + log_info(std::format(STATE_GL_CONTEXT_INIT_INFO, (const char*)glGetString(GL_VERSION))); glEnable(GL_BLEND); @@ -100,6 +101,7 @@ void init(State* self) clipboard_init(&self->clipboard, &self->anm2); snapshots_init(&self->snapshots, &self->anm2, &self->reference, &self->preview); preview_init(&self->preview, &self->anm2, &self->reference, &self->resources, &self->settings); + generate_preview_init(&self->generatePreview, &self->anm2, &self->reference, &self->resources, &self->settings); editor_init(&self->editor, &self->anm2, &self->reference, &self->resources, &self->settings); imgui_init @@ -111,6 +113,7 @@ void init(State* self) &self->reference, &self->editor, &self->preview, + &self->generatePreview, &self->settings, &self->snapshots, &self->clipboard, @@ -118,7 +121,7 @@ void init(State* self) &self->glContext ); - if (self->is_argument()) + if (!self->argument.empty()) { anm2_deserialize(&self->anm2, &self->resources, self->argument); window_title_from_path_set(self->window, self->argument); @@ -152,6 +155,7 @@ void loop(State* self) void quit(State* self) { imgui_free(); + generate_preview_free(&self->generatePreview); preview_free(&self->preview); editor_free(&self->editor); resources_free(&self->resources); diff --git a/src/state.h b/src/state.h index 7e213fd..4e023c2 100644 --- a/src/state.h +++ b/src/state.h @@ -32,6 +32,7 @@ struct State Dialog dialog; Editor editor; Preview preview; + GeneratePreview generatePreview; Anm2 anm2; Anm2Reference reference; Resources resources; @@ -43,9 +44,6 @@ struct State u64 lastTick{}; u64 tick{}; bool isRunning = true; - - bool is_last_action() const { return !lastAction.empty(); } - bool is_argument() const { return !argument.empty(); } }; void init(State* state); diff --git a/src/texture.cpp b/src/texture.cpp index 89bd924..82d7b7d 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -35,12 +35,16 @@ bool texture_from_path_init(Texture* self, const std::string& path) { *self = Texture{}; u8* data = stbi_load(path.c_str(), &self->size.x, &self->size.y, &self->channels, TEXTURE_CHANNELS); - + if (!data) { - log_error(std::format(TEXTURE_INIT_ERROR, path)); - self->isInvalid = true; - return false; + data = stbi_load(path_canonical_resolve(path).c_str(), &self->size.x, &self->size.y, &self->channels, TEXTURE_CHANNELS); + if (!data) + { + log_error(std::format(TEXTURE_INIT_ERROR, path)); + self->isInvalid = true; + return false; + } } log_info(std::format(TEXTURE_INIT_INFO, path)); @@ -82,8 +86,16 @@ bool texture_from_rgba_init(Texture* self, ivec2 size, s32 channels, const u8* d bool texture_from_rgba_write(const std::string& path, const u8* data, ivec2 size) { + bool isSuccess = stbi_write_png(path.c_str(), size.x, size.y, TEXTURE_CHANNELS, data, size.x * TEXTURE_CHANNELS); + if (!isSuccess) + { + isSuccess = stbi_write_png(path_canonical_resolve(path).c_str(), size.x, size.y, TEXTURE_CHANNELS, data, size.x * TEXTURE_CHANNELS); + if (!isSuccess) log_info(std::format(TEXTURE_SAVE_ERROR, path)); + } + log_info(std::format(TEXTURE_SAVE_INFO, path)); - return (bool)stbi_write_png(path.c_str(), size.x, size.y, TEXTURE_CHANNELS, data, size.x * TEXTURE_CHANNELS); + + return isSuccess; } bool texture_from_gl_write(Texture* self, const std::string& path) diff --git a/src/texture.h b/src/texture.h index eb671ec..7553bbd 100644 --- a/src/texture.h +++ b/src/texture.h @@ -6,6 +6,7 @@ #define TEXTURE_INIT_INFO "Initialized texture from file: {}" #define TEXTURE_INIT_ERROR "Failed to initialize texture from file: {}" #define TEXTURE_SAVE_INFO "Saved texture to: {}" +#define TEXTURE_SAVE_ERROR "Failed to save texture to: {}" struct Texture {