The Omega Update(TM) Part 5 (Finishing)
This commit is contained in:
		@@ -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:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								src/COMMON.h
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								src/COMMON.h
									
									
									
									
									
								
							@@ -25,6 +25,8 @@
 | 
			
		||||
#include <unordered_set>                      
 | 
			
		||||
#include <vector>                  
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
typedef uint8_t u8;
 | 
			
		||||
typedef uint16_t u16;
 | 
			
		||||
typedef uint32_t u32;
 | 
			
		||||
@@ -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<char>(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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								src/PACKED.h
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								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}
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										61
									
								
								src/anm2.cpp
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								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{};
 | 
			
		||||
@@ -1127,3 +1119,52 @@ 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++;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/anm2.h
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								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);
 | 
			
		||||
@@ -280,3 +283,5 @@ void anm2_animation_length_set(Anm2Animation* self);
 | 
			
		||||
void anm2_animation_merge(Anm2* self, s32 animationID, const std::vector<s32>& 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);
 | 
			
		||||
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);
 | 
			
		||||
@@ -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<f32> 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								src/canvas.h
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								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);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
#include "dialog.h"
 | 
			
		||||
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
    #include <windows.h>
 | 
			
		||||
#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)
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
@@ -54,3 +54,4 @@ 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);
 | 
			
		||||
void dialog_explorer_open(const std::string& path);
 | 
			
		||||
@@ -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();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -12,87 +12,40 @@ void generate_preview_init(GeneratePreview* self, Anm2* anm2, Anm2Reference* ref
 | 
			
		||||
 | 
			
		||||
void generate_preview_draw(GeneratePreview* self)
 | 
			
		||||
{
 | 
			
		||||
    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);
 | 
			
		||||
 
 | 
			
		||||
    /* 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);
 | 
			
		||||
    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);
 | 
			
		||||
    Anm2Item* item = anm2_item_from_reference(self->anm2, self->reference);
 | 
			
		||||
    Texture* texture = map_find(self->resources->textures, self->anm2->layers[self->reference->itemID].spritesheetID);
 | 
			
		||||
        
 | 
			
		||||
    Anm2Animation* animation = anm2_animation_from_reference(self->anm2, self->reference);
 | 
			
		||||
    s32& animationID = self->reference->animationID;
 | 
			
		||||
 | 
			
		||||
    if (animation)
 | 
			
		||||
    if (item && texture && !texture->isInvalid)
 | 
			
		||||
    {
 | 
			
		||||
        Anm2Frame root;
 | 
			
		||||
        mat4 rootModel = mat4(1.0f);
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
        anm2_frame_from_time(self->anm2, &root, Anm2Reference{animationID, ANM2_ROOT}, self->time);
 | 
			
		||||
 | 
			
		||||
        if (self->settings->previewIsRootTransform)
 | 
			
		||||
            rootModel = quad_parent_model_get(root.position, vec2(0.0f), root.rotation, PERCENT_TO_UNIT(root.scale));
 | 
			
		||||
 | 
			
		||||
        // 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);
 | 
			
		||||
        vec2 uvMin = crop / vec2(texture->size);
 | 
			
		||||
        vec2 uvMax = (crop + 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);
 | 
			
		||||
        mat4 model = quad_model_get(size, {}, pivot, {}, CANVAS_SCALE_DEFAULT);
 | 
			
		||||
        mat4 generateTransform = transform * 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);
 | 
			
		||||
        canvas_texture_draw(&self->canvas, shaderTexture, texture->id, generateTransform, vertices, COLOR_OPAQUE, COLOR_OFFSET_NONE);
 | 
			
		||||
    }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    s32& animationOverlayID = self->animationOverlayID;
 | 
			
		||||
    Anm2Animation* animationOverlay = map_find(self->anm2->animations, animationOverlayID);
 | 
			
		||||
    
 | 
			
		||||
    canvas_unbind();
 | 
			
		||||
    */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void generate_preview_free(GeneratePreview* self)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										237
									
								
								src/imgui.cpp
									
									
									
									
									
								
							
							
						
						
									
										237
									
								
								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<typename T>
 | 
			
		||||
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);
 | 
			
		||||
	}			
 | 
			
		||||
 | 
			
		||||
@@ -1191,12 +1246,20 @@ static void _imgui_taskbar(Imgui* self)
 | 
			
		||||
		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,6 +1458,14 @@ static void _imgui_taskbar(Imgui* self)
 | 
			
		||||
		{
 | 
			
		||||
			bool isRenderStart = true;
 | 
			
		||||
 | 
			
		||||
			if (!std::filesystem::exists(self->settings->ffmpegPath))
 | 
			
		||||
			{
 | 
			
		||||
				imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_FFMPEG_PATH_ERROR);
 | 
			
		||||
				isRenderStart = false;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (isRenderStart)
 | 
			
		||||
			{
 | 
			
		||||
				switch (self->settings->renderType)
 | 
			
		||||
				{
 | 
			
		||||
					case RENDER_PNG:
 | 
			
		||||
@@ -1367,7 +1477,7 @@ static void _imgui_taskbar(Imgui* self)
 | 
			
		||||
						break;
 | 
			
		||||
					case RENDER_GIF:
 | 
			
		||||
					case RENDER_WEBM:
 | 
			
		||||
					if (!path_valid(self->settings->renderPath))
 | 
			
		||||
						if (!path_is_valid(self->settings->renderPath))
 | 
			
		||||
						{
 | 
			
		||||
							imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_PATH_ERROR);
 | 
			
		||||
							isRenderStart = false;
 | 
			
		||||
@@ -1375,6 +1485,7 @@ static void _imgui_taskbar(Imgui* self)
 | 
			
		||||
					default:
 | 
			
		||||
						break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (isRenderStart)
 | 
			
		||||
				preview_render_start(self->preview);
 | 
			
		||||
@@ -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_QUIT:
 | 
			
		||||
				if (!self->snapshots->undoStack.is_empty())
 | 
			
		||||
			case SDL_EVENT_DROP_FILE:
 | 
			
		||||
        	{
 | 
			
		||||
					if (imgui_is_popup_open(IMGUI_EXIT_CONFIRMATION.label_get()))
 | 
			
		||||
						self->isQuit = true;
 | 
			
		||||
					else
 | 
			
		||||
						imgui_open_popup(IMGUI_EXIT_CONFIRMATION.label_get());
 | 
			
		||||
            	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;
 | 
			
		||||
			}
 | 
			
		||||
				else
 | 
			
		||||
			case SDL_EVENT_QUIT:
 | 
			
		||||
				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();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										153
									
								
								src/imgui.h
									
									
									
									
									
								
							
							
						
						
									
										153
									
								
								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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								src/main.cpp
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								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])
 | 
			
		||||
	{
 | 
			
		||||
		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);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
@@ -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,19 +136,19 @@ void preview_draw(Preview* self)
 | 
			
		||||
            if (!frame.isVisible)
 | 
			
		||||
                continue;
 | 
			
		||||
 | 
			
		||||
            mat4 model = quad_model_get(frame.size, frame.position, frame.pivot, frame.rotation, PERCENT_TO_UNIT(frame.scale));
 | 
			
		||||
            mat4 layerTransform = transform * (rootModel * model);
 | 
			
		||||
 | 
			
		||||
            Texture* texture = map_find(self->resources->textures, self->anm2->layers[id].spritesheetID);
 | 
			
		||||
           
 | 
			
		||||
            if (!texture || texture->isInvalid)
 | 
			
		||||
                continue;
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										122
									
								
								src/settings.cpp
									
									
									
									
									
								
							
							
						
						
									
										122
									
								
								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<char>(in), std::istreambuf_iterator<char>());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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<char>(input)), std::istreambuf_iterator<char>());
 | 
			
		||||
    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; 
 | 
			
		||||
        } 
 | 
			
		||||
 | 
			
		||||
        if (line == SETTINGS_SECTION_IMGUI) break; 
 | 
			
		||||
        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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										217
									
								
								src/settings.h
									
									
									
									
									
								
							
							
						
						
									
										217
									
								
								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);
 | 
			
		||||
std::string settings_path_get(void);
 | 
			
		||||
@@ -24,10 +24,11 @@ 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))
 | 
			
		||||
	{
 | 
			
		||||
		log_error(std::format(STATE_SDL_INIT_ERROR, SDL_GetError()));
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -36,12 +36,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)
 | 
			
		||||
	{
 | 
			
		||||
		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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user