...Anm2Ed 2.0
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
#include "anm2.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
|
||||
#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<int, int> spritesheetRemap{};
|
||||
std::unordered_map<int, int> layerRemap{};
|
||||
std::unordered_map<int, int> nullRemap{};
|
||||
std::unordered_map<int, int> eventRemap{};
|
||||
std::unordered_map<int, int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
@@ -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 = {});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "anm2.h"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
#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<int> 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#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<unsigned char> Canvas::pixels_get() const
|
||||
{
|
||||
auto count = size.x * size.y * texture::CHANNELS;
|
||||
std::vector<unsigned char> 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);
|
||||
|
||||
21
src/canvas.h
21
src/canvas.h
@@ -3,6 +3,7 @@
|
||||
#include <glad/glad.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#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<unsigned char> pixels_get() const;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
88
src/framebuffer.cpp
Normal file
88
src/framebuffer.cpp
Normal file
@@ -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<uint8_t> Framebuffer::pixels_get() const
|
||||
{
|
||||
auto count = size.x * size.y * texture::CHANNELS;
|
||||
std::vector<uint8_t> 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); }
|
||||
}
|
||||
37
src/framebuffer.h
Normal file
37
src/framebuffer.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <glad/glad.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
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<uint8_t> pixels_get() const;
|
||||
bool is_valid() const;
|
||||
void bind() const;
|
||||
void unbind() const;
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <imgui/imgui_internal.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
@@ -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<long long>(std::floor(world / step));
|
||||
float first = minCoord - (world - static_cast<float>(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<int>*)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; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <imgui/imgui.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -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<int>
|
||||
{
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cfloat>
|
||||
#include <cstddef>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
#include <system_error>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
@@ -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.");
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include "animation_preview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <ranges>
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#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<uint8_t> 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();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <future>
|
||||
|
||||
#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<resource::Texture> renderFrames{};
|
||||
std::future<bool> renderFuture{};
|
||||
bool isRenderFutureValid{};
|
||||
std::string renderOutputPath{};
|
||||
|
||||
public:
|
||||
AnimationPreview();
|
||||
void tick(Manager&, Document&, Settings&);
|
||||
void tick(Manager&, Settings&);
|
||||
void update(Manager&, Settings&, Resources&);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "animations.h"
|
||||
|
||||
#include <ranges>
|
||||
#include <cstddef>
|
||||
|
||||
#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();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "spritesheet_editor.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <utility>
|
||||
|
||||
#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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "timeline.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <cstddef>
|
||||
|
||||
#include <imgui_internal.h>
|
||||
|
||||
@@ -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<int> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
|
||||
#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<std::filesystem::path> Manager::recent_files_ordered() const
|
||||
{
|
||||
std::vector<std::pair<std::string, std::size_t>> orderedEntries(recentFiles.begin(), recentFiles.end());
|
||||
std::sort(orderedEntries.begin(), orderedEntries.end(),
|
||||
[](const auto& lhs, const auto& rhs) { return lhs.second > rhs.second; });
|
||||
|
||||
std::vector<std::filesystem::path> 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<std::string> loaded{};
|
||||
std::unordered_set<std::string> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<Document> documents{};
|
||||
std::vector<std::filesystem::path> recentFiles{};
|
||||
std::map<std::string, std::size_t, std::less<>> recentFiles{};
|
||||
std::size_t recentFilesCounter{};
|
||||
std::vector<std::filesystem::path> autosaveFiles{};
|
||||
|
||||
int selected{-1};
|
||||
@@ -31,6 +37,14 @@ namespace anm2ed
|
||||
|
||||
ImGuiKeyChord chords[SHORTCUT_COUNT]{};
|
||||
|
||||
std::vector<std::filesystem::path> 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<int> 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<std::filesystem::path> recent_files_ordered() const;
|
||||
|
||||
void autosave_files_load();
|
||||
void autosave_files_open();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,10 +23,11 @@ namespace anm2ed::resource
|
||||
int channels{};
|
||||
std::vector<uint8_t> 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);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "resources.h"
|
||||
|
||||
#include <ranges>
|
||||
#include <cstddef>
|
||||
|
||||
#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()
|
||||
|
||||
@@ -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{};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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&);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "state.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <imgui/backends/imgui_impl_opengl3.h>
|
||||
#include <imgui/backends/imgui_impl_sdl3.h>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user