Merge branch 'staging'

This commit is contained in:
2025-11-11 00:06:51 -05:00
169 changed files with 24179 additions and 10914 deletions

BIN
.Icon.ico-autosave.kra Normal file

Binary file not shown.

11
.clang-format Normal file
View File

@@ -0,0 +1,11 @@
ColumnLimit: 120
PointerAlignment: Left
ReferenceAlignment: Left
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: true
CommentPragmas: '^'
BreakBeforeBraces: Allman
NamespaceIndentation: All
FixNamespaceComments: false
IndentCaseLabels: true
IndentPPDirectives: BeforeHash

6
.gitignore vendored
View File

@@ -4,8 +4,8 @@ release/
packed/
vcpkg_installed/
out/
include/imgui/
include/glm/
include/tinyxml2
external/
workshop/resources
cmake-build-debug/
.vs/
.idea/

26
.gitmodules vendored
View File

@@ -1,10 +1,22 @@
[submodule "include/imgui"]
path = include/imgui
[submodule "external/glm"]
path = external/glm
url = https://github.com/g-truc/glm
[submodule "external/imgui"]
path = external/imgui
url = https://github.com/ocornut/imgui
branch = docking
[submodule "include/glm"]
path = include/glm
url = https://github.com/g-truc/glm
[submodule "include/tinyxml2"]
path = include/tinyxml2
[submodule "external/tinyxml2"]
path = external/tinyxml2
url = https://github.com/leethomason/tinyxml2
[submodule "external/SDL"]
path = external/SDL
url = https://github.com/libsdl-org/SDL.git
[submodule "external/lunasvg"]
path = external/lunasvg
url = https://github.com/sammycage/lunasvg
[submodule "external/SDL_mixer"]
path = external/SDL_mixer
url = https://github.com/libsdl-org/SDL_mixer
[submodule "external/libxm"]
path = external/libxm
url = https://github.com/Artefact2/libxm

28
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug ANM2Ed",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/anm2ed",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing"
},
{
"description": "Set disassembly flavor to Intel",
"text": "-gdb-set disassembly-flavor intel"
}
]
}
]
}

17
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "cmake --build build --target anm2ed",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
}
]
}

View File

@@ -1,35 +1,78 @@
cmake_minimum_required(VERSION 3.15)
if(WIN32 AND DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
CACHE STRING "Vcpkg toolchain file")
endif()
cmake_minimum_required(VERSION 3.30)
project(anm2ed CXX)
find_package(SDL3 REQUIRED)
find_package(OpenGL REQUIRED)
if (WIN32 AND DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
CACHE STRING "Vcpkg toolchain file")
endif ()
set(GLAD_SRC
${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp
)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if (CMAKE_EXPORT_COMPILE_COMMANDS)
execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
${CMAKE_BINARY_DIR}/compile_commands.json
${CMAKE_SOURCE_DIR}/compile_commands.json
)
endif ()
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_HAPTIC OFF CACHE BOOL "" FORCE)
set(SDL_SENSOR OFF CACHE BOOL "" FORCE)
set(SDL_HIDAPI OFF CACHE BOOL "" FORCE)
set(SDL_CAMERA OFF CACHE BOOL "" FORCE)
set(SDL_TRAY OFF CACHE BOOL "" FORCE)
add_subdirectory(external/SDL EXCLUDE_FROM_ALL)
set(SDLMIXER_DEPS_SHARED OFF CACHE BOOL "" FORCE)
set(SDLMIXER_FLAC_LIBFLAC OFF CACHE BOOL "" FORCE)
set(SDLMIXER_GME OFF CACHE BOOL "" FORCE)
set(SDLMIXER_MOD_XMP OFF CACHE BOOL "" FORCE)
set(SDLMIXER_MP3_MPG123 OFF CACHE BOOL "" FORCE)
set(SDLMIXER_MIDI_FLUIDSYNTH OFF CACHE BOOL "" FORCE)
set(SDLMIXER_OPUS OFF CACHE BOOL "" FORCE)
set(SDLMIXER_VORBIS_VORBISFILE OFF CACHE BOOL "" FORCE)
set(SDLMIXER_VORBIS_TREMOR OFF CACHE BOOL "" FORCE)
set(SDLMIXER_WAVPACK OFF CACHE BOOL "" FORCE)
set(SDLMIXER_TEST OFF CACHE BOOL "" FORCE)
set(SDLMIXER_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(external/SDL_mixer EXCLUDE_FROM_ALL)
add_subdirectory(external/lunasvg)
set(GLAD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp)
set(IMGUI_SRC
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/imgui.cpp
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/imgui_draw.cpp
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/imgui_widgets.cpp
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/imgui_tables.cpp
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/backends/imgui_impl_sdl3.cpp
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/backends/imgui_impl_opengl3.cpp
external/imgui/imgui.cpp
external/imgui/imgui_draw.cpp
external/imgui/imgui_widgets.cpp
external/imgui/imgui_tables.cpp
external/imgui/backends/imgui_impl_sdl3.cpp
external/imgui/backends/imgui_impl_opengl3.cpp
)
set(TINYXML2_SRC
${CMAKE_CURRENT_SOURCE_DIR}/include/tinyxml2/tinyxml2.cpp
)
set(TINYXML2_SRC external/tinyxml2/tinyxml2.cpp)
file(GLOB PROJECT_SRC
${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/*.h
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
src/anm2/*.cpp
src/anm2/*.h
src/resource/*.cpp
src/resource/*.h
src/imgui/*.cpp
src/imgui/*.h
src/imgui/window/*.cpp
src/imgui/window/*.h
src/util/*.cpp
src/util/*.h
src/window/*.cpp
src/window/*.h
src/*.cpp
src/*.h
)
add_executable(${PROJECT_NAME}
@@ -39,37 +82,78 @@ add_executable(${PROJECT_NAME}
${PROJECT_SRC}
)
if(WIN32)
if (WIN32)
enable_language(RC)
target_sources(${PROJECT_NAME} PRIVATE assets/Icon.rc)
target_sources(${PROJECT_NAME} PRIVATE Icon.rc)
set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
set_property(TARGET ${PROJECT_NAME} PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/include/glad
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui
${CMAKE_CURRENT_SOURCE_DIR}/include/tinyxml2
${CMAKE_CURRENT_SOURCE_DIR}/src
target_compile_options(${PROJECT_NAME} PRIVATE /EHsc)
target_link_options(${PROJECT_NAME} PRIVATE /STACK:0xffffff)
else ()
target_compile_options(${PROJECT_NAME} PRIVATE
-Wall -Wextra -pedantic
)
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(${PROJECT_NAME} PRIVATE DEBUG)
target_compile_options(${PROJECT_NAME} PRIVATE -O0 -pg)
else ()
set(CMAKE_BUILD_TYPE "Release")
target_compile_options(${PROJECT_NAME} PRIVATE -Os)
endif ()
target_link_libraries(${PROJECT_NAME} PRIVATE m)
endif ()
target_compile_definitions(${PROJECT_NAME} PRIVATE
IMGUI_DISABLE_OBSOLETE_FUNCTIONS
IMGUI_DEBUG_PARANOID
IMGUI_ENABLE_DOCKING
)
if(MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE /std:c++latest /EHsc)
target_link_options(${PROJECT_NAME} PRIVATE /STACK:0xffffff)
else()
target_compile_options(${PROJECT_NAME} PRIVATE -O2 -std=c++23 -Wall -Wextra -pedantic -fmax-errors=1)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_options(${PROJECT_NAME} PRIVATE -DDEBUG -g)
else()
set(CMAKE_BUILD_TYPE "Release")
endif()
target_link_libraries(${PROJECT_NAME} PRIVATE m)
endif()
target_include_directories(${PROJECT_NAME} PRIVATE
external
external/imgui
external/glm
external/tinyxml2
external/lunasvg
external/SDL
external/SDL_mixer
include
include/glad
src
src/imgui
src/resource
src/util
)
target_link_libraries(${PROJECT_NAME} PRIVATE OpenGL::GL SDL3::SDL3)
target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static SDL3_mixer::SDL3_mixer lunasvg)
message("System: ${CMAKE_SYSTEM_NAME}")
message("Project: ${PROJECT_NAME}")
message("Build: ${CMAKE_BUILD_TYPE}")
message(STATUS "System: ${CMAKE_SYSTEM_NAME}")
message(STATUS "Project: ${PROJECT_NAME}")
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER}")
get_target_property(PROJECT_COMPILE_OPTIONS ${PROJECT_NAME} COMPILE_OPTIONS)
if (NOT PROJECT_COMPILE_OPTIONS)
set(PROJECT_COMPILE_OPTIONS "<none>")
endif ()
string(TOUPPER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_UPPER)
set(EFFECTIVE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
if (BUILD_TYPE_UPPER)
set(CONFIG_FLAGS_VAR "CMAKE_CXX_FLAGS_${BUILD_TYPE_UPPER}")
if (DEFINED ${CONFIG_FLAGS_VAR})
string(APPEND EFFECTIVE_CXX_FLAGS " ${${CONFIG_FLAGS_VAR}}")
endif ()
endif ()
string(STRIP "${EFFECTIVE_CXX_FLAGS}" EFFECTIVE_CXX_FLAGS)
if (EFFECTIVE_CXX_FLAGS STREQUAL "")
set(EFFECTIVE_CXX_FLAGS "<none>")
endif ()
message(STATUS "Compiler Flags: ${EFFECTIVE_CXX_FLAGS}")
message(STATUS "Target Compile Options: ${PROJECT_COMPILE_OPTIONS}")
message(STATUS "Build: ${CMAKE_BUILD_TYPE}")

View File

@@ -1,26 +0,0 @@
{
"configurations": [
{
"name": "x64-Debug",
"generator": "Ninja",
"configurationType": "Debug",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\build\\${name}",
"installRoot": "${projectDir}\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": ""
},
{
"name": "x64-Release",
"generator": "Ninja",
"configurationType": "Release",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\build\\${name}",
"installRoot": "${projectDir}\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": ""
}
]
}

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,66 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
INPUT="atlas.png"
OUTPUT="../src/PACKED.h"
TMP="atlas.bytes"
# Ensure deps
command -v optipng >/dev/null 2>&1 || { echo "error: optipng not found in PATH" >&2; exit 1; }
command -v xxd >/dev/null 2>&1 || { echo "error: xxd not found in PATH" >&2; exit 1; }
# 1) Optimize PNG in place
optipng -o7 "$INPUT" >/dev/null
# 2) Extract ONLY the bytes from xxd -i (between '= {' and '};')
# Using awk avoids the earlier '{' vs '= {' mismatch bug.
xxd -i "$INPUT" \
| awk '
/= *\{/ {inside=1; next}
inside && /};/ {inside=0; exit}
inside {print}
' > "$TMP"
# Sanity check: make sure we got something
if ! [ -s "$TMP" ]; then
echo "error: failed to extract bytes from xxd output" >&2
exit 1
fi
# 3) Replace ONLY the bytes inside TEXTURE_ATLAS[] initializer
# - Find the exact declaration line for TEXTURE_ATLAS
# - Print that line
# - On the following line with just '{', print it, then insert bytes,
# then skip everything until the matching '};' and print that once.
awk -v tmpfile="$TMP" '
BEGIN { state=0 } # 0=normal, 1=after decl waiting for {, 2=skipping old bytes until };
# Match the TEXTURE_ATLAS declaration line precisely
$0 ~ /^[[:space:]]*const[[:space:]]+u8[[:space:]]+TEXTURE_ATLAS\[\][[:space:]]*=/ {
print; state=1; next
}
# After the decl, the next line with a lone "{" starts the initializer
state==1 && $0 ~ /^[[:space:]]*{[[:space:]]*$/ {
print # print the opening brace line
while ((getline line < tmpfile) > 0) print line # insert fresh bytes
close(tmpfile)
state=2 # now skip old initializer content until we hit the closing "};"
next
}
# While skipping, suppress lines until the closing "};", which we reprint once
state==2 {
if ($0 ~ /^[[:space:]]*};[[:space:]]*$/) {
print # print the closing brace+semicolon
state=0
}
next
}
# Default: pass through unchanged
{ print }
' "$OUTPUT" > "$OUTPUT.tmp" && mv "$OUTPUT.tmp" "$OUTPUT"
rm -f "$TMP"
echo "Updated $OUTPUT with optimized bytes from $INPUT."

1
compile_commands.json Symbolic link
View File

@@ -0,0 +1 @@
/home/anon/sda/Personal/Repos/anm2ed/build/compile_commands.json

1
external/SDL_mixer vendored Submodule

Submodule external/SDL_mixer added at 8c516fcd2e

View File

1
external/imgui vendored Submodule

Submodule external/imgui added at 0955992dd9

1
external/libxm vendored Submodule

Submodule external/libxm added at 9f599c4dd4

1
external/tinyxml2 vendored Submodule

Submodule external/tinyxml2 added at 36ff404c34

Submodule include/imgui deleted from d896eab166

3121
include/nanosvg.h Normal file

File diff suppressed because it is too large Load Diff

1461
include/nanosvgrast.h Normal file

File diff suppressed because it is too large Load Diff

Submodule include/tinyxml2 deleted from e6caeae857

View File

@@ -1,389 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
#include <glad/glad.h>
#include <GL/gl.h>
#include <glm/glm/glm.hpp>
#include <glm/glm/gtc/type_ptr.hpp>
#include <glm/glm/gtc/matrix_transform.hpp>
#include <tinyxml2.h>
#include <algorithm>
#include <chrono>
#include <climits>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <format>
#include <fstream>
#include <functional>
#include <iostream>
#include <map>
#include <optional>
#include <print>
#include <ranges>
#include <string>
#include <unordered_set>
#include <variant>
#include <vector>
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef int8_t s8;
typedef int16_t s16;
typedef int32_t s32;
typedef int64_t s64;
typedef float f32;
typedef double f64;
#define PI (GLM_PI)
#define TAU (PI * 2)
using namespace glm;
#define PREFERENCES_DIRECTORY "anm2ed"
#define ROUND_NEAREST_MULTIPLE(value, multiple) (roundf((value) / (multiple)) * (multiple))
#define FLOAT_TO_U8(x) (static_cast<u8>((x) * 255.0f))
#define U8_TO_FLOAT(x) ((x) / 255.0f)
#define PERCENT_TO_UNIT(x) (x / 100.0f)
#define UNIT_TO_PERCENT(x) (x * 100.0f)
#define SECOND 1000.0f
#define TICK_DELAY (SECOND / 30.0)
#define UPDATE_DELAY (SECOND / 120.0)
#define ID_NONE -1
#define INDEX_NONE -1
#define VALUE_NONE -1
#define TIME_NONE -1.0f
#define GL_ID_NONE 0
#ifdef _WIN32
#define POPEN _popen
#define PCLOSE _pclose
#define PWRITE_MODE "wb"
#define PREAD_MODE "r"
#else
#define POPEN popen
#define PCLOSE pclose
#define PWRITE_MODE "w"
#define PREAD_MODE "r"
#endif
static const GLuint GL_TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0};
static const vec4 COLOR_RED = {1.0f, 0.0f, 0.0f, 1.0f};
static const vec4 COLOR_GREEN = {0.0f, 1.0f, 0.0f, 1.0f};
static const vec4 COLOR_BLUE = {0.0f, 0.0f, 1.0f, 1.0f};
static const vec4 COLOR_PINK = {1.0f, 0.0f, 1.0f, 1.0f};
static const vec4 COLOR_OPAQUE = {1.0f, 1.0f, 1.0f, 1.0f};
static const vec4 COLOR_TRANSPARENT = {0.0f, 0.0f, 0.0f, 0.0f};
static const vec3 COLOR_OFFSET_NONE = {0.0f, 0.0f, 0.0f};
static inline std::string preferences_path_get(void)
{
char* preferencesPath = SDL_GetPrefPath("", PREFERENCES_DIRECTORY);
std::string preferencesPathString = preferencesPath;
SDL_free(preferencesPath);
return preferencesPathString;
}
static inline bool string_to_bool(const std::string& string)
{
if (string == "1") return true;
std::string lower = string;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
return lower == "true";
}
static inline std::string string_quote(const std::string& string)
{
return "\"" + string + "\"";
}
static inline std::string string_to_lowercase(std::string string) {
std::transform
(
string.begin(), string.end(), string.begin(),
[](u8 character){ return std::tolower(character); }
);
return string;
}
static inline std::string string_backslash_replace(std::string string)
{
for (char& character : string)
if (character == '\\')
character = '/';
return string;
}
#define FLOAT_FORMAT_MAX_DECIMALS 5
#define FLOAT_FORMAT_EPSILON 1e-5f
static constexpr f32 FLOAT_FORMAT_POW10[] = {
1.f,
10.f,
100.f,
1000.f,
10000.f,
100000.f
};
static inline s32 f32_decimals_needed(f32 value)
{
for (s32 decimalCount = 0; decimalCount <= FLOAT_FORMAT_MAX_DECIMALS; ++decimalCount)
{
f32 scale = FLOAT_FORMAT_POW10[decimalCount];
f32 rounded = roundf(value * scale) / scale;
if (fabsf(value - rounded) < FLOAT_FORMAT_EPSILON)
return decimalCount;
}
return FLOAT_FORMAT_MAX_DECIMALS;
}
static inline const char* f32_format_get(f32 value)
{
static std::string formatString;
const s32 decimalCount = f32_decimals_needed(value);
formatString = (decimalCount == 0)
? "%.0f"
: ("%." + std::to_string(decimalCount) + "f");
return formatString.c_str();
}
static inline const char* vec2_format_get(const vec2& value)
{
static std::string formatString;
const s32 decimalCountX = f32_decimals_needed(value.x);
const s32 decimalCountY = f32_decimals_needed(value.y);
const s32 decimalCount = (decimalCountX > decimalCountY) ? decimalCountX : decimalCountY;
formatString = (decimalCount == 0)
? "%.0f"
: ("%." + std::to_string(decimalCount) + "f");
return formatString.c_str();
}
static inline std::string working_directory_from_file_set(const std::string& path)
{
std::filesystem::path filePath = path;
std::filesystem::path parentPath = filePath.parent_path();
std::filesystem::current_path(parentPath);
return parentPath.string();
};
static inline std::string path_extension_change(const std::string& path, const std::string& extension)
{
std::filesystem::path filePath(path);
filePath.replace_extension(extension);
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_is_valid(const std::filesystem::path& pathCheck)
{
namespace fs = std::filesystem;
std::error_code ec;
if (fs::is_directory(pathCheck, ec)) return false;
fs::path parentDir = pathCheck.has_parent_path() ? pathCheck.parent_path() : fs::path(".");
if (!fs::is_directory(parentDir, ec)) return false;
bool existedBefore = fs::exists(pathCheck, ec);
std::ofstream testStream(pathCheck, std::ios::app | std::ios::binary);
bool isValid = testStream.is_open();
testStream.close();
if (!existedBefore && isValid)
fs::remove(pathCheck, ec);
return isValid;
}
static inline s32 string_to_enum(const std::string& string, const char* const* array, s32 n)
{
for (s32 i = 0; i < n; i++)
if (string == array[i])
return i;
return -1;
};
template <typename T>
T& dummy_value()
{
static T value{};
return value;
}
template<typename T>
static inline s32 map_next_id_get(const std::map<s32, T>& map)
{
s32 id = 0;
for (const auto& [key, value] : map)
{
if (key != id)
break;
++id;
}
return id;
}
template <typename Map>
static inline auto map_find(Map& map, typename Map::key_type id)
-> typename Map::mapped_type*
{
if (auto it = map.find(id); it != map.end())
return &it->second;
return nullptr;
}
template<typename Map, typename Key>
static inline void map_swap(Map& map, const Key& key1, const Key& key2)
{
if (key1 == key2)
return;
auto it1 = map.find(key1);
auto it2 = map.find(key2);
if (it1 != map.end() && it2 != map.end())
{
using std::swap;
swap(it1->second, it2->second);
}
else if (it1 != map.end())
{
map[key2] = std::move(it1->second);
map.erase(it1);
}
else if (it2 != map.end())
{
map[key1] = std::move(it2->second);
map.erase(it2);
}
};
template <typename T>
static inline void map_insert_shift(std::map<int, T>& map, s32 index, const T& value)
{
const s32 insertIndex = index + 1;
std::vector<std::pair<int, T>> toShift;
for (auto it = map.rbegin(); it != map.rend(); ++it)
{
if (it->first < insertIndex)
break;
toShift.emplace_back(it->first + 1, std::move(it->second));
}
for (const auto& [newKey, _] : toShift)
map.erase(newKey - 1);
for (auto& [newKey, val] : toShift)
map[newKey] = std::move(val);
map[insertIndex] = value;
}
template <typename T>
void vector_value_erase(std::vector<T>& v, const T& value)
{
v.erase(std::remove(v.begin(), v.end(), value), v.end());
}
template <typename T>
void vector_value_swap(std::vector<T>& v, const T& a, const T& b)
{
for (auto& element : v)
{
if (element == a) element = b;
else if (element == b) element = a;
}
}
static inline mat4 quad_model_get(vec2 size = {}, vec2 position = {}, vec2 pivot = {}, vec2 scale = vec2(1.0f), f32 rotation = {})
{
vec2 scaleAbsolute = glm::abs(scale);
vec2 scaleSign = glm::sign(scale);
vec2 pivotScaled = pivot * scaleAbsolute;
vec2 sizeScaled = size * scaleAbsolute;
mat4 model(1.0f);
model = glm::translate(model, vec3(position - pivotScaled, 0.0f));
model = glm::translate(model, vec3(pivotScaled, 0.0f));
model = glm::scale(model, vec3(scaleSign, 1.0f));
model = glm::rotate(model, glm::radians(rotation), vec3(0, 0, 1));
model = glm::translate(model, vec3(-pivotScaled, 0.0f));
model = glm::scale(model, vec3(sizeScaled, 1.0f));
return model;
}
static inline mat4 quad_model_parent_get(vec2 position = {}, vec2 pivot = {}, vec2 scale = vec2(1.0f), f32 rotation = {})
{
vec2 scaleSign = glm::sign(scale);
vec2 scaleAbsolute = glm::abs(scale);
f32 handedness = (scaleSign.x * scaleSign.y) < 0.0f ? -1.0f : 1.0f;
mat4 local(1.0f);
local = glm::translate(local, vec3(pivot, 0.0f));
local = glm::scale(local, vec3(scaleSign, 1.0f));
local = glm::rotate(local, glm::radians(rotation) * handedness, vec3(0, 0, 1));
local = glm::translate(local, vec3(-pivot, 0.0f));
local = glm::scale(local, vec3(scaleAbsolute, 1.0f));
return glm::translate(mat4(1.0f), vec3(position, 0.0f)) * local;
}
#define DEFINE_STRING_TO_ENUM_FUNCTION(function, enumType, stringArray, count) \
static inline enumType function(const std::string& string) \
{ \
return static_cast<enumType>(string_to_enum(string, stringArray, count)); \
};
#define DATATYPE_LIST \
X(TYPE_INT, s32) \
X(TYPE_BOOL, bool) \
X(TYPE_FLOAT, f32) \
X(TYPE_STRING, std::string) \
X(TYPE_IVEC2, ivec2) \
X(TYPE_IVEC2_WH, ivec2) \
X(TYPE_VEC2, vec2) \
X(TYPE_VEC2_WH, vec2) \
X(TYPE_VEC3, vec3) \
X(TYPE_VEC4, vec4)
enum DataType
{
#define X(symbol, ctype) symbol,
DATATYPE_LIST
#undef X
};
#define DATATYPE_TO_CTYPE(dt) DATATYPE_CTYPE_##dt
#define X(symbol, ctype) typedef ctype DATATYPE_CTYPE_##symbol;
DATATYPE_LIST
#undef X
enum OriginType
{
ORIGIN_TOP_LEFT,
ORIGIN_CENTER
};

View File

@@ -1,347 +0,0 @@
#pragma once
#include "COMMON.h"
const u8 TEXTURE_ATLAS[] =
{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xa0,
0x04, 0x03, 0x00, 0x00, 0x00, 0x01, 0x5e, 0x74, 0xbf, 0x00, 0x00, 0x00,
0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b,
0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x0f, 0x50, 0x4c,
0x54, 0x45, 0x00, 0x00, 0x00, 0x76, 0x76, 0x76, 0xff, 0xff, 0xff, 0x60,
0x60, 0x60, 0xff, 0xff, 0xff, 0x3e, 0xd5, 0x47, 0x6d, 0x00, 0x00, 0x00,
0x03, 0x74, 0x52, 0x4e, 0x53, 0x00, 0x00, 0x00, 0xfa, 0x76, 0xc4, 0xde,
0x00, 0x00, 0x04, 0x62, 0x49, 0x44, 0x41, 0x54, 0x58, 0xc3, 0xed, 0x59,
0x5b, 0x6e, 0x24, 0x37, 0x0c, 0x2c, 0x80, 0xbc, 0x40, 0xee, 0x90, 0x03,
0x10, 0x20, 0x0f, 0xc0, 0x80, 0x75, 0xff, 0x33, 0xe5, 0x83, 0xa2, 0x5a,
0x63, 0xcf, 0xae, 0xa7, 0x37, 0xb1, 0x03, 0x6c, 0xac, 0x0f, 0x77, 0x6b,
0x5a, 0xd5, 0x7c, 0x15, 0x49, 0xb5, 0x0c, 0xfc, 0xc2, 0x20, 0x9f, 0xff,
0xae, 0xd9, 0xd7, 0x28, 0x00, 0xe0, 0xb1, 0x9e, 0x04, 0x40, 0x9a, 0x92,
0x76, 0xac, 0x27, 0xf3, 0x78, 0x21, 0xbd, 0x00, 0x01, 0xa0, 0x04, 0x98,
0xd0, 0x46, 0x26, 0x00, 0x04, 0x49, 0xe8, 0x9e, 0x36, 0xc0, 0x95, 0x80,
0x2e, 0x51, 0x64, 0xdf, 0xee, 0x15, 0xa4, 0x92, 0x83, 0x58, 0x2a, 0x29,
0x4b, 0xd8, 0x00, 0x9d, 0x17, 0xf6, 0x02, 0xc9, 0x56, 0x68, 0xe6, 0x6d,
0x4b, 0xbf, 0xa2, 0x67, 0x4c, 0x4d, 0x35, 0xec, 0xb5, 0x89, 0x41, 0x24,
0x9e, 0x01, 0x94, 0xd0, 0xd4, 0xb6, 0x34, 0x35, 0x7b, 0x81, 0x78, 0x90,
0xe5, 0xf6, 0x08, 0x28, 0x40, 0x13, 0xcc, 0x13, 0x50, 0x08, 0x03, 0x20,
0xee, 0x51, 0xee, 0x06, 0x48, 0xfb, 0x8e, 0x61, 0x64, 0x02, 0x9a, 0x4a,
0x1c, 0x00, 0xe1, 0xf2, 0xb8, 0xb8, 0xbb, 0x1b, 0x80, 0x48, 0xad, 0xb6,
0x75, 0x59, 0xcf, 0x04, 0xda, 0x4b, 0x00, 0x12, 0x03, 0x30, 0x77, 0x37,
0x33, 0x21, 0x01, 0xd2, 0xae, 0xc0, 0x75, 0xd4, 0x2e, 0x40, 0xb0, 0x00,
0x40, 0x44, 0xc4, 0x45, 0xa0, 0x4c, 0x40, 0x99, 0x17, 0x60, 0x02, 0x37,
0x5e, 0xd2, 0x15, 0x5f, 0x98, 0x99, 0xd9, 0x13, 0xc0, 0xa2, 0x86, 0xb4,
0xdf, 0xed, 0x22, 0x8d, 0x88, 0x00, 0x10, 0x5a, 0xff, 0xe1, 0xc7, 0xe4,
0x83, 0xe1, 0x8b, 0xc6, 0xd0, 0x5c, 0xde, 0x48, 0x94, 0x07, 0x72, 0x5f,
0xf3, 0xa1, 0xf9, 0xdb, 0xe7, 0x93, 0x17, 0xc1, 0xa6, 0x80, 0x93, 0xa4,
0x5f, 0x9c, 0x92, 0xe5, 0x13, 0xbc, 0x71, 0x2f, 0x0a, 0x00, 0x02, 0x08,
0x02, 0xac, 0xcd, 0x5a, 0x09, 0xba, 0xb3, 0xec, 0x12, 0x45, 0x77, 0x27,
0x81, 0x26, 0x28, 0x40, 0xeb, 0x3c, 0x19, 0x40, 0x3f, 0xb7, 0x2d, 0x4a,
0xe8, 0x1e, 0xa4, 0x0d, 0x40, 0x3a, 0x4f, 0x6c, 0xab, 0xd4, 0xcf, 0x01,
0x68, 0xb9, 0x47, 0x42, 0xcb, 0x9d, 0x15, 0x39, 0x80, 0xc9, 0x93, 0xa1,
0xf9, 0x7e, 0x2e, 0x15, 0x51, 0x15, 0xa6, 0xe5, 0x41, 0x8f, 0x44, 0x42,
0xb3, 0x01, 0x4a, 0x68, 0x0e, 0xcd, 0xf7, 0x73, 0x9a, 0x6a, 0xa6, 0x30,
0xd2, 0x59, 0xae, 0xd5, 0x00, 0xd9, 0x79, 0x32, 0x34, 0xd7, 0x72, 0x96,
0x47, 0x42, 0x52, 0x4d, 0x52, 0x4d, 0x4b, 0x09, 0x8c, 0x4a, 0x57, 0x9e,
0xc0, 0xdc, 0x1d, 0x80, 0x66, 0xd0, 0xa1, 0x09, 0x28, 0x48, 0x24, 0x84,
0x2c, 0xf7, 0x65, 0x34, 0x09, 0x21, 0x13, 0x4e, 0x53, 0x29, 0xb1, 0x9e,
0x97, 0x39, 0x01, 0x48, 0x01, 0x61, 0x00, 0xe9, 0xee, 0xed, 0x56, 0x8d,
0x4a, 0x90, 0x10, 0xa7, 0x81, 0x66, 0x6b, 0x6e, 0xe2, 0xb4, 0x0e, 0x6d,
0x01, 0x50, 0xba, 0xaf, 0xc0, 0x95, 0x66, 0x20, 0x08, 0x67, 0x39, 0x28,
0xc7, 0x3c, 0xfd, 0x19, 0x35, 0x4c, 0x4d, 0xb2, 0xa9, 0x01, 0x53, 0x24,
0xd4, 0x24, 0x05, 0x24, 0xf9, 0xc0, 0xbf, 0x83, 0x5c, 0x64, 0xcf, 0xdd,
0xcf, 0x8c, 0xb9, 0xe6, 0xbf, 0xca, 0xf7, 0xb9, 0xbe, 0xdc, 0x1f, 0xd6,
0x75, 0xa9, 0xa6, 0x49, 0x4d, 0x5e, 0x57, 0x00, 0x40, 0x57, 0xc2, 0x29,
0xa1, 0x47, 0xed, 0xc6, 0x94, 0x9d, 0x7d, 0x5d, 0xc5, 0xcd, 0xde, 0xab,
0xb6, 0xd8, 0xa9, 0xfd, 0x86, 0x5d, 0x11, 0x01, 0x40, 0xcd, 0xed, 0xf4,
0xce, 0x2a, 0xba, 0xec, 0x6a, 0xd7, 0x45, 0x71, 0xcc, 0x4a, 0x00, 0x28,
0x0a, 0x80, 0x0c, 0x93, 0x52, 0x6c, 0x96, 0xae, 0xc2, 0xa5, 0x00, 0xa2,
0x25, 0x6d, 0x0d, 0x82, 0x06, 0x68, 0x16, 0x10, 0x9a, 0x9a, 0x88, 0x82,
0xa6, 0x76, 0xfa, 0xa5, 0x2e, 0x09, 0x87, 0x4a, 0xc5, 0x4a, 0x80, 0x99,
0x9a, 0xba, 0x12, 0x91, 0xa4, 0x76, 0x8c, 0xba, 0x09, 0xc5, 0xa9, 0x92,
0x06, 0x03, 0xcf, 0x01, 0xd9, 0x12, 0x98, 0x38, 0x55, 0x6a, 0x01, 0x4f,
0x55, 0xa2, 0x32, 0xb5, 0x99, 0xd5, 0x2a, 0x69, 0x26, 0xd0, 0x02, 0xde,
0x19, 0x2d, 0x4c, 0xb0, 0x7b, 0x76, 0x8e, 0x77, 0x3a, 0x6f, 0xb4, 0xf2,
0x0d, 0xe9, 0xc6, 0xad, 0xb9, 0xda, 0xf4, 0x14, 0x9c, 0xbe, 0x67, 0xc6,
0x73, 0x4e, 0x29, 0x93, 0xcc, 0x01, 0xe4, 0x05, 0xe0, 0xdb, 0x2d, 0xc2,
0xa6, 0xc6, 0x29, 0x61, 0xab, 0x24, 0x91, 0x3f, 0x22, 0x5f, 0xef, 0x08,
0x94, 0x4c, 0x6a, 0x02, 0xd4, 0xa4, 0xe6, 0x43, 0xe9, 0xbe, 0x4d, 0xef,
0x7f, 0x3a, 0x0e, 0x39, 0xba, 0xb3, 0xf7, 0xb1, 0x8b, 0x2d, 0xfd, 0xd4,
0xa2, 0x1e, 0x15, 0x5b, 0xf7, 0xf4, 0x01, 0x58, 0xff, 0xb0, 0xe6, 0x52,
0x5c, 0x3d, 0xe2, 0xec, 0x19, 0xd0, 0xea, 0x38, 0x09, 0x6b, 0x3d, 0x94,
0x58, 0x8b, 0x62, 0xf7, 0x83, 0x69, 0x03, 0x4c, 0xb0, 0x37, 0x0c, 0x10,
0xa6, 0x5a, 0x2f, 0xf4, 0xae, 0x64, 0x50, 0xb0, 0xb0, 0x38, 0x88, 0x7d,
0x33, 0xb5, 0x27, 0x3c, 0xb5, 0x2b, 0x9f, 0x04, 0xe9, 0x6d, 0xad, 0xda,
0x74, 0x9c, 0x01, 0x24, 0x9c, 0x4e, 0x27, 0x84, 0x4e, 0x76, 0x4a, 0x1b,
0xbb, 0x75, 0x69, 0xd7, 0xd9, 0xde, 0xb5, 0xf5, 0x0e, 0x46, 0x89, 0x01,
0x28, 0x49, 0x46, 0x02, 0x10, 0xea, 0x08, 0xb0, 0x8c, 0xe5, 0x19, 0x4d,
0x9d, 0xbd, 0x59, 0x1b, 0x0c, 0x05, 0xd9, 0xfc, 0x0e, 0xe6, 0x08, 0xd0,
0x1d, 0x87, 0x01, 0x00, 0x70, 0x77, 0x77, 0x87, 0x82, 0xac, 0x55, 0x41,
0x29, 0x23, 0xe0, 0x08, 0xc0, 0x0e, 0x87, 0x01, 0x02, 0x81, 0x92, 0x5e,
0xab, 0xc4, 0xb6, 0x1f, 0x1e, 0x0a, 0xc4, 0xb2, 0xe1, 0x04, 0xa4, 0x7a,
0x0d, 0xab, 0x97, 0x26, 0x79, 0xf1, 0x9d, 0xd7, 0x26, 0x76, 0x01, 0xba,
0x26, 0x74, 0xe0, 0xcb, 0x05, 0x86, 0x38, 0x13, 0xe4, 0x00, 0x2c, 0x1b,
0x00, 0x19, 0x80, 0xb8, 0xd9, 0x63, 0xc9, 0x9c, 0x48, 0x7f, 0x15, 0xdd,
0xf1, 0x3e, 0xbd, 0x24, 0x5f, 0x04, 0x0c, 0x68, 0x95, 0xd3, 0xb1, 0xc0,
0xba, 0xbe, 0x82, 0xf5, 0x03, 0xc0, 0x2a, 0xa7, 0xcc, 0x8f, 0x00, 0x57,
0x5d, 0x99, 0x52, 0xf1, 0x1a, 0x40, 0xb9, 0xdb, 0x74, 0xfe, 0x04, 0x70,
0xd4, 0xc6, 0xb2, 0x01, 0x74, 0x33, 0x69, 0x00, 0xd7, 0xde, 0x6f, 0xae,
0xdb, 0x06, 0xd9, 0x1f, 0x4d, 0x9d, 0x9b, 0x3f, 0x03, 0x6c, 0x22, 0xd4,
0x00, 0xea, 0xc7, 0x80, 0x63, 0x38, 0x73, 0x01, 0xca, 0x2e, 0x95, 0xdc,
0x59, 0xee, 0x57, 0x3e, 0x1c, 0x05, 0x79, 0xdc, 0xba, 0x32, 0xf5, 0xb9,
0xd1, 0xba, 0xea, 0xed, 0xe9, 0xd6, 0x36, 0xfe, 0x63, 0xc0, 0xaa, 0x83,
0x6b, 0xfd, 0x0b, 0x80, 0xfe, 0xd4, 0xc0, 0xec, 0x0a, 0xc4, 0x6d, 0xed,
0x10, 0x1e, 0x36, 0x3b, 0xa7, 0x4a, 0xaf, 0x91, 0xf5, 0xb1, 0x0b, 0xdd,
0x18, 0xfe, 0x66, 0xd8, 0xfb, 0x57, 0x3f, 0x07, 0x8c, 0x05, 0x1f, 0x02,
0x82, 0x13, 0xe2, 0xaf, 0x02, 0xe0, 0x1b, 0xf0, 0x7b, 0x00, 0xd6, 0xde,
0xef, 0x65, 0x80, 0xae, 0xae, 0xfd, 0x32, 0x80, 0xa9, 0x7c, 0xd8, 0x7e,
0x7c, 0x08, 0xe8, 0xcf, 0xbf, 0x97, 0x01, 0xfd, 0xbd, 0x78, 0x9e, 0x07,
0xfc, 0x7b, 0x00, 0x3e, 0x19, 0xff, 0x31, 0xe0, 0xf3, 0x8d, 0xfe, 0xc5,
0x38, 0xdc, 0x8f, 0xf4, 0x6d, 0x2e, 0xdd, 0x66, 0xeb, 0xef, 0x99, 0xd3,
0x73, 0x36, 0xf6, 0x32, 0x60, 0xce, 0x93, 0x5e, 0x07, 0xac, 0xd3, 0x9f,
0x97, 0xfb, 0xc3, 0x9c, 0x6b, 0xdc, 0x00, 0xf4, 0x97, 0xe2, 0x0d, 0x40,
0x9f, 0x59, 0xde, 0x01, 0x88, 0xe1, 0x5d, 0x37, 0xfd, 0x29, 0xe0, 0x7b,
0x7c, 0x8f, 0xaf, 0x19, 0x77, 0x3f, 0xb6, 0xf6, 0x3f, 0x15, 0x5e, 0x1c,
0xca, 0x9b, 0x04, 0x27, 0x70, 0xeb, 0xc0, 0x63, 0x8e, 0x1d, 0x3e, 0x0f,
0xb0, 0x56, 0xfe, 0x99, 0x77, 0x00, 0x7f, 0xdc, 0x05, 0xfc, 0xf5, 0xe9,
0x12, 0x6e, 0x01, 0x3e, 0xdf, 0xad, 0xb7, 0x23, 0x7d, 0x9f, 0x4b, 0xb7,
0xd9, 0x7a, 0x3f, 0x1f, 0xbe, 0xc7, 0xff, 0x75, 0xfc, 0x0d, 0x3b, 0xd4,
0xd5, 0x4b, 0x3b, 0xfe, 0xb6, 0x75, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
};
const u32 TEXTURE_ATLAS_LENGTH = (u32)std::size(TEXTURE_ATLAS);
const vec2 TEXTURE_ATLAS_SIZE = {96, 160};
enum AtlasType
{
ATLAS_NONE,
ATLAS_FOLDER,
ATLAS_ROOT,
ATLAS_LAYER,
ATLAS_NULL,
ATLAS_TRIGGERS,
ATLAS_VISIBLE,
ATLAS_INVISIBLE,
ATLAS_SHOW_RECT,
ATLAS_HIDE_RECT,
ATLAS_SHOW_UNUSED,
ATLAS_HIDE_UNUSED,
ATLAS_PAN,
ATLAS_MOVE,
ATLAS_ROTATE,
ATLAS_SCALE,
ATLAS_CROP,
ATLAS_DRAW,
ATLAS_ERASE,
ATLAS_COLOR_PICKER,
ATLAS_UNDO,
ATLAS_REDO,
ATLAS_ANIMATION,
ATLAS_SPRITESHEET,
ATLAS_EVENT,
ATLAS_PLAY,
ATLAS_PAUSE,
ATLAS_ADD,
ATLAS_REMOVE,
ATLAS_TRIGGER,
ATLAS_PIVOT,
ATLAS_SQUARE,
ATLAS_CIRCLE,
ATLAS_PICKER,
ATLAS_FRAME,
ATLAS_FRAME_ALT,
ATLAS_TARGET,
ATLAS_TARGET_ALT,
ATLAS_COUNT
};
struct AtlasEntry
{
vec2 position;
vec2 size;
};
const vec2 ATLAS_SIZE_SMALL = {8, 8};
const vec2 ATLAS_SIZE_NORMAL = {16, 16};
const vec2 ATLAS_SIZE_OBLONG = {16, 40};
const vec2 ATLAS_SIZE_BIG = {40, 40};
const inline AtlasEntry ATLAS_ENTRIES[ATLAS_COUNT] =
{
{{ 0, 0}, ATLAS_SIZE_NORMAL},
{{ 16, 0}, ATLAS_SIZE_NORMAL},
{{ 32, 0}, ATLAS_SIZE_NORMAL},
{{ 48, 0}, ATLAS_SIZE_NORMAL},
{{ 64, 0}, ATLAS_SIZE_NORMAL},
{{ 80, 0}, ATLAS_SIZE_NORMAL},
{{ 0, 16}, ATLAS_SIZE_NORMAL},
{{ 16, 16}, ATLAS_SIZE_NORMAL},
{{ 32, 16}, ATLAS_SIZE_NORMAL},
{{ 48, 16}, ATLAS_SIZE_NORMAL},
{{ 64, 16}, ATLAS_SIZE_NORMAL},
{{ 80, 16}, ATLAS_SIZE_NORMAL},
{{ 0, 32}, ATLAS_SIZE_NORMAL},
{{ 16, 32}, ATLAS_SIZE_NORMAL},
{{ 32, 32}, ATLAS_SIZE_NORMAL},
{{ 48, 32}, ATLAS_SIZE_NORMAL},
{{ 64, 32}, ATLAS_SIZE_NORMAL},
{{ 80, 32}, ATLAS_SIZE_NORMAL},
{{ 0, 48}, ATLAS_SIZE_NORMAL},
{{ 16, 48}, ATLAS_SIZE_NORMAL},
{{ 32, 48}, ATLAS_SIZE_NORMAL},
{{ 48, 48}, ATLAS_SIZE_NORMAL},
{{ 64, 48}, ATLAS_SIZE_NORMAL},
{{ 80, 48}, ATLAS_SIZE_NORMAL},
{{ 0, 64}, ATLAS_SIZE_NORMAL},
{{ 16, 64}, ATLAS_SIZE_NORMAL},
{{ 32, 64}, ATLAS_SIZE_NORMAL},
{{ 48, 64}, ATLAS_SIZE_NORMAL},
{{ 64, 64}, ATLAS_SIZE_NORMAL},
{{ 80, 64}, ATLAS_SIZE_SMALL },
{{ 88, 64}, ATLAS_SIZE_SMALL },
{{ 80, 72}, ATLAS_SIZE_SMALL },
{{ 88, 72}, ATLAS_SIZE_SMALL },
{{ 0, 80}, ATLAS_SIZE_OBLONG},
{{16, 80}, ATLAS_SIZE_OBLONG},
{{32, 80}, ATLAS_SIZE_OBLONG},
{{48, 80}, ATLAS_SIZE_BIG},
{{48, 120}, ATLAS_SIZE_BIG}
};
#define ATLAS_POSITION(type) ATLAS_ENTRIES[type].position
#define ATLAS_SIZE(type) ATLAS_ENTRIES[type].size
#define ATLAS_UV_MIN(type) (ATLAS_POSITION(type) / TEXTURE_ATLAS_SIZE)
#define ATLAS_UV_MAX(type) ((ATLAS_POSITION(type) + ATLAS_SIZE(type)) / TEXTURE_ATLAS_SIZE)
#define ATLAS_UV_ARGS(type) ATLAS_UV_MIN(type), ATLAS_UV_MAX(type)
#define ATLAS_UV_VERTICES(type) UV_VERTICES(ATLAS_UV_MIN(type), ATLAS_UV_MAX(type))
struct ShaderData
{
std::string vertex;
std::string fragment;
};
enum ShaderType
{
SHADER_LINE,
SHADER_TEXTURE,
SHADER_AXIS,
SHADER_GRID,
SHADER_COUNT
};
const std::string SHADER_VERTEX = R"(
#version 330 core
layout (location = 0) in vec2 i_position;
layout (location = 1) in vec2 i_uv;
out vec2 i_uv_out;
uniform mat4 u_transform;
void main()
{
i_uv_out = i_uv;
gl_Position = u_transform * vec4(i_position, 0.0, 1.0);
}
)";
const std::string SHADER_AXIS_VERTEX = R"(
#version 330 core
layout (location = 0) in vec2 i_position; // full screen line segment: -1..1
uniform vec2 u_origin_ndc; // world origin in NDC
uniform int u_axis; // 0 = X axis, 1 = Y axis
void main()
{
vec2 pos = (u_axis == 0)
? vec2(i_position.x, u_origin_ndc.y) // horizontal line across screen
: vec2(u_origin_ndc.x, i_position.x); // vertical line across screen;
gl_Position = vec4(pos, 0.0, 1.0);
}
)";
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_FRAGMENT = R"(
#version 330 core
out vec4 o_fragColor;
uniform vec4 u_color;
void main()
{
o_fragColor = u_color;
}
)";
const std::string SHADER_TEXTURE_FRAGMENT = R"(
#version 330 core
in vec2 i_uv_out;
uniform sampler2D u_texture;
uniform vec4 u_tint;
uniform vec3 u_color_offset;
out vec4 o_fragColor;
void main()
{
vec4 texColor = texture(u_texture, i_uv_out);
texColor *= u_tint;
texColor.rgb += u_color_offset;
o_fragColor = texColor;
}
)";
const std::string SHADER_GRID_FRAGMENT = R"(
#version 330 core
in vec2 clip;
uniform mat4 u_model;
uniform vec2 u_size;
uniform vec2 u_offset;
uniform vec4 u_color;
out vec4 o_fragColor;
void main()
{
vec4 w = u_model * vec4(clip, 0.0, 1.0);
w /= w.w;
vec2 world = w.xy;
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_AXIS "u_axis"
#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_ORIGIN_NDC "u_origin_ndc"
#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_AXIS_VERTEX, SHADER_FRAGMENT},
{SHADER_GRID_VERTEX, SHADER_GRID_FRAGMENT}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,308 +0,0 @@
#pragma once
#include "resources.h"
#define ANM2_SCALE_CONVERT(x) ((f32)x / 100.0f)
#define ANM2_TINT_CONVERT(x) ((f32)x / 255.0f)
#define ANM2_FPS_MIN 0
#define ANM2_FPS_DEFAULT 30
#define ANM2_FPS_MAX 120
#define ANM2_FRAME_NUM_MIN 1
#define ANM2_FRAME_NUM_MAX 1000000
#define ANM2_FRAME_DELAY_MIN 1
#define ANM2_STRING_MAX 0xFF
#define ANM2_EMPTY_ERROR "No path given for anm2"
#define ANM2_READ_ERROR "Failed to read anm2 from file: {}"
#define ANM2_PARSE_ERROR "Failed to parse anm2: {} ({})"
#define ANM2_FRAME_PARSE_ERROR "Failed to parse frame: {} ({})"
#define ANM2_ANIMATION_PARSE_ERROR "Failed to parse frame: {} ({})"
#define ANM2_READ_INFO "Read anm2 from file: {}"
#define ANM2_WRITE_ERROR "Failed to write anm2 to file: {}"
#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"
#define ANM2_ELEMENT_LIST \
X(ANIMATED_ACTOR, "AnimatedActor") \
X(INFO, "Info") \
X(CONTENT, "Content") \
X(SPRITESHEETS, "Spritesheets") \
X(SPRITESHEET, "Spritesheet") \
X(LAYERS, "Layers") \
X(LAYER, "Layer") \
X(NULLS, "Nulls") \
X(NULL, "Null") \
X(EVENTS, "Events") \
X(EVENT, "Event") \
X(ANIMATIONS, "Animations") \
X(ANIMATION, "Animation") \
X(ROOT_ANIMATION, "RootAnimation") \
X(FRAME, "Frame") \
X(LAYER_ANIMATIONS, "LayerAnimations") \
X(LAYER_ANIMATION, "LayerAnimation") \
X(NULL_ANIMATIONS, "NullAnimations") \
X(NULL_ANIMATION, "NullAnimation") \
X(TRIGGERS, "Triggers") \
X(TRIGGER, "Trigger")
typedef enum
{
#define X(name, str) ANM2_ELEMENT_##name,
ANM2_ELEMENT_LIST
#undef X
ANM2_ELEMENT_COUNT
} Anm2Element;
const inline char* ANM2_ELEMENT_STRINGS[] =
{
#define X(name, str) str,
ANM2_ELEMENT_LIST
#undef X
};
DEFINE_STRING_TO_ENUM_FUNCTION(ANM2_ELEMENT_STRING_TO_ENUM, Anm2Element, ANM2_ELEMENT_STRINGS, ANM2_ELEMENT_COUNT)
#define ANM2_ATTRIBUTE_LIST \
X(CREATED_BY, "CreatedBy") \
X(CREATED_ON, "CreatedOn") \
X(VERSION, "Version") \
X(FPS, "Fps") \
X(ID, "Id") \
X(PATH, "Path") \
X(NAME, "Name") \
X(SPRITESHEET_ID, "SpritesheetId") \
X(SHOW_RECT, "ShowRect") \
X(DEFAULT_ANIMATION, "DefaultAnimation") \
X(FRAME_NUM, "FrameNum") \
X(LOOP, "Loop") \
X(X_POSITION, "XPosition") \
X(Y_POSITION, "YPosition") \
X(X_PIVOT, "XPivot") \
X(Y_PIVOT, "YPivot") \
X(X_CROP, "XCrop") \
X(Y_CROP, "YCrop") \
X(WIDTH, "Width") \
X(HEIGHT, "Height") \
X(X_SCALE, "XScale") \
X(Y_SCALE, "YScale") \
X(DELAY, "Delay") \
X(VISIBLE, "Visible") \
X(RED_TINT, "RedTint") \
X(GREEN_TINT, "GreenTint") \
X(BLUE_TINT, "BlueTint") \
X(ALPHA_TINT, "AlphaTint") \
X(RED_OFFSET, "RedOffset") \
X(GREEN_OFFSET, "GreenOffset") \
X(BLUE_OFFSET, "BlueOffset") \
X(ROTATION, "Rotation") \
X(INTERPOLATED, "Interpolated") \
X(LAYER_ID, "LayerId") \
X(NULL_ID, "NullId") \
X(EVENT_ID, "EventId") \
X(AT_FRAME, "AtFrame")
typedef enum
{
#define X(name, str) ANM2_ATTRIBUTE_##name,
ANM2_ATTRIBUTE_LIST
#undef X
ANM2_ATTRIBUTE_COUNT
} Anm2Attribute;
static const char* ANM2_ATTRIBUTE_STRINGS[] =
{
#define X(name, str) str,
ANM2_ATTRIBUTE_LIST
#undef X
};
DEFINE_STRING_TO_ENUM_FUNCTION(ANM2_ATTRIBUTE_STRING_TO_ENUM, Anm2Attribute, ANM2_ATTRIBUTE_STRINGS, ANM2_ATTRIBUTE_COUNT)
enum Anm2Type
{
ANM2_NONE,
ANM2_ROOT,
ANM2_LAYER,
ANM2_NULL,
ANM2_TRIGGERS,
ANM2_COUNT
};
struct Anm2Spritesheet
{
std::string path{};
Texture texture;
std::vector<u8> pixels;
auto operator<=>(const Anm2Spritesheet&) const = default;
};
struct Anm2Layer
{
std::string name = "New Layer";
s32 spritesheetID{};
auto operator<=>(const Anm2Layer&) const = default;
};
struct Anm2Null
{
std::string name = "New Null";
bool isShowRect = false;
auto operator<=>(const Anm2Null&) const = default;
};
struct Anm2Event
{
std::string name = "New Event";
auto operator<=>(const Anm2Event&) const = default;
};
struct Anm2Frame
{
bool isVisible = true;
bool isInterpolated = false;
f32 rotation{};
s32 delay = ANM2_FRAME_DELAY_MIN;
s32 atFrame = INDEX_NONE;
s32 eventID = ID_NONE;
vec2 crop{};
vec2 pivot{};
vec2 position{};
vec2 size{};
vec2 scale = {100, 100};
vec3 offsetRGB = COLOR_OFFSET_NONE;
vec4 tintRGBA = COLOR_OPAQUE;
auto operator<=>(const Anm2Frame&) const = default;
};
struct Anm2Item
{
bool isVisible = true;
std::vector<Anm2Frame> frames;
auto operator<=>(const Anm2Item&) const = default;
};
struct Anm2Animation
{
s32 frameNum = ANM2_FRAME_NUM_MIN;
std::string name = "New Animation";
bool isLoop = true;
Anm2Item rootAnimation;
std::unordered_map<s32, Anm2Item> layerAnimations;
std::vector<s32> layerOrder;
std::map<s32, Anm2Item> nullAnimations;
Anm2Item triggers;
auto operator<=>(const Anm2Animation&) const = default;
};
struct Anm2
{
std::string path{};
std::string createdBy = "robot";
std::string createdOn{};
std::map<s32, Anm2Spritesheet> spritesheets;
std::map<s32, Anm2Layer> layers;
std::map<s32, Anm2Null> nulls;
std::map<s32, Anm2Event> events;
std::map<s32, Anm2Animation> animations;
s32 defaultAnimationID = ID_NONE;
s32 fps = ANM2_FPS_DEFAULT;
s32 version{};
auto operator<=>(const Anm2&) const = default;
};
struct Anm2Reference
{
s32 animationID = ID_NONE;
Anm2Type itemType = ANM2_NONE;
s32 itemID = ID_NONE;
s32 frameIndex = INDEX_NONE;
f32 time = VALUE_NONE;
auto operator<=>(const Anm2Reference&) const = default;
};
struct Anm2FrameChange
{
std::optional<bool> isVisible;
std::optional<bool> isInterpolated;
std::optional<f32> rotation;
std::optional<s32> delay;
std::optional<vec2> crop;
std::optional<vec2> pivot;
std::optional<vec2> position;
std::optional<vec2> size;
std::optional<vec2> scale;
std::optional<vec3> offsetRGB;
std::optional<vec4> tintRGBA;
};
enum Anm2MergeType
{
ANM2_MERGE_APPEND,
ANM2_MERGE_REPLACE,
ANM2_MERGE_PREPEND,
ANM2_MERGE_IGNORE
};
enum Anm2ChangeType
{
ANM2_CHANGE_ADD,
ANM2_CHANGE_SUBTRACT,
ANM2_CHANGE_SET
};
enum OnionskinDrawOrder
{
ONIONSKIN_BELOW,
ONIONSKIN_ABOVE
};
Anm2Animation* anm2_animation_from_reference(Anm2* self, Anm2Reference* reference);
Anm2Frame* anm2_frame_add(Anm2* self, Anm2Frame* frame, Anm2Reference* reference);
Anm2Frame* anm2_frame_from_reference(Anm2* self, Anm2Reference* reference);
Anm2Item* anm2_item_from_reference(Anm2* self, Anm2Reference* reference);
bool anm2_animation_deserialize_from_xml(Anm2Animation* animation, const std::string& xml);
bool anm2_deserialize(Anm2* self, const std::string& path, bool isTextures = true);
bool anm2_frame_deserialize_from_xml(Anm2Frame* frame, const std::string& xml);
bool anm2_serialize(Anm2* self, const std::string& path);
s32 anm2_animation_add(Anm2* self, Anm2Animation* animation = nullptr, s32 id = ID_NONE);
s32 anm2_animation_length_get(Anm2Animation* self);
s32 anm2_frame_index_from_time(Anm2* self, Anm2Reference reference, f32 time);
s32 anm2_layer_add(Anm2* self);
s32 anm2_null_add(Anm2* self);
vec4 anm2_animation_rect_get(Anm2* anm2, Anm2Reference* reference, bool isRootTransform);
void anm2_animation_layer_animation_add(Anm2Animation* animation, s32 id);
void anm2_animation_layer_animation_remove(Anm2Animation* animation, s32 id);
void anm2_animation_length_set(Anm2Animation* self);
void anm2_animation_merge(Anm2* self, s32 animationID, const std::vector<s32>& mergeIDs, Anm2MergeType type);
void anm2_animation_null_animation_add(Anm2Animation* animation, s32 id);
void anm2_animation_null_animation_remove(Anm2Animation* animation, s32 id);
void anm2_animation_remove(Anm2* self, s32 id);
void anm2_animation_serialize_to_string(Anm2Animation* animation, std::string* string);
void anm2_frame_bake(Anm2* self, Anm2Reference* reference, s32 interval, bool isRoundScale, bool isRoundRotation);
void anm2_frame_from_time(Anm2* self, Anm2Frame* frame, Anm2Reference reference, f32 time);
void anm2_frame_remove(Anm2* self, Anm2Reference* reference);
void anm2_frame_serialize_to_string(Anm2Frame* frame, Anm2Type type, std::string* string);
void anm2_free(Anm2* self);
void anm2_generate_from_grid(Anm2* self, Anm2Reference* reference, vec2 startPosition, vec2 size, vec2 pivot, s32 columns, s32 count, s32 delay);
void anm2_item_frame_set(Anm2* self, Anm2Reference* reference, const Anm2FrameChange& change, Anm2ChangeType type, s32 start, s32 count);
void anm2_layer_remove(Anm2* self, s32 id);
void anm2_new(Anm2* self);
void anm2_null_remove(Anm2* self, s32 id);
void anm2_reference_clear(Anm2Reference* self);
void anm2_reference_frame_clear(Anm2Reference* self);
void anm2_reference_item_clear(Anm2Reference* self);
void anm2_scale(Anm2* self, f32 scale);
void anm2_spritesheet_texture_pixels_download(Anm2* self);
void anm2_spritesheet_texture_pixels_upload(Anm2* self);

180
src/anm2/animation.cpp Normal file
View File

@@ -0,0 +1,180 @@
#include "animation.h"
#include "map_.h"
#include "math_.h"
#include "unordered_map_.h"
#include "xml_.h"
#include <ranges>
using namespace anm2ed::util;
using namespace glm;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Animation::Animation(XMLElement* element)
{
int id{};
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("FrameNum", &frameNum);
element->QueryBoolAttribute("Loop", &isLoop);
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
rootAnimation = Item(rootAnimationElement, ROOT);
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
{
for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child;
child = child->NextSiblingElement("LayerAnimation"))
{
layerAnimations[id] = Item(child, LAYER, &id);
layerOrder.push_back(id);
}
}
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
child = child->NextSiblingElement("NullAnimation"))
nullAnimations[id] = Item(child, NULL_, &id);
if (auto triggersElement = element->FirstChildElement("Triggers")) triggers = Item(triggersElement, TRIGGER);
}
Item* Animation::item_get(Type type, int id)
{
switch (type)
{
case ROOT:
return &rootAnimation;
case LAYER:
return unordered_map::find(layerAnimations, id);
case NULL_:
return map::find(nullAnimations, id);
case TRIGGER:
return &triggers;
default:
return nullptr;
}
return nullptr;
}
void Animation::item_remove(Type type, int id)
{
switch (type)
{
case LAYER:
layerAnimations.erase(id);
for (auto [i, value] : std::views::enumerate(layerOrder))
if (value == id) layerOrder.erase(layerOrder.begin() + i);
break;
case NULL_:
nullAnimations.erase(id);
break;
case ROOT:
case TRIGGER:
default:
break;
}
}
XMLElement* Animation::to_element(XMLDocument& document)
{
auto element = document.NewElement("Animation");
element->SetAttribute("Name", name.c_str());
element->SetAttribute("FrameNum", frameNum);
element->SetAttribute("Loop", isLoop);
rootAnimation.serialize(document, element, ROOT);
auto layerAnimationsElement = document.NewElement("LayerAnimations");
for (auto& i : layerOrder)
{
Item& layerAnimation = layerAnimations.at(i);
layerAnimation.serialize(document, layerAnimationsElement, LAYER, i);
}
element->InsertEndChild(layerAnimationsElement);
auto nullAnimationsElement = document.NewElement("NullAnimations");
for (auto& [id, nullAnimation] : nullAnimations)
nullAnimation.serialize(document, nullAnimationsElement, NULL_, id);
element->InsertEndChild(nullAnimationsElement);
triggers.serialize(document, element, TRIGGER);
return element;
}
void Animation::serialize(XMLDocument& document, XMLElement* parent) { parent->InsertEndChild(to_element(document)); }
std::string Animation::to_string()
{
XMLDocument document{};
document.InsertEndChild(to_element(document));
return xml::document_to_string(document);
}
int Animation::length()
{
int length{};
if (int rootAnimationLength = rootAnimation.length(ROOT); rootAnimationLength > length)
length = rootAnimationLength;
for (auto& layerAnimation : layerAnimations | std::views::values)
if (int layerAnimationLength = layerAnimation.length(LAYER); layerAnimationLength > length)
length = layerAnimationLength;
for (auto& nullAnimation : nullAnimations | std::views::values)
if (int nullAnimationLength = nullAnimation.length(NULL_); nullAnimationLength > length)
length = nullAnimationLength;
if (int triggersLength = triggers.length(TRIGGER); triggersLength > length) length = triggersLength;
return length;
}
vec4 Animation::rect(bool isRootTransform)
{
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
float minX = std::numeric_limits<float>::infinity();
float minY = std::numeric_limits<float>::infinity();
float maxX = -std::numeric_limits<float>::infinity();
float maxY = -std::numeric_limits<float>::infinity();
bool any = false;
for (float t = 0.0f; t < (float)frameNum; t += 1.0f)
{
mat4 transform(1.0f);
if (isRootTransform)
{
auto root = rootAnimation.frame_generate(t, ROOT);
transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation);
}
for (auto& [id, layerAnimation] : layerAnimations)
{
auto frame = layerAnimation.frame_generate(t, LAYER);
if (frame.size == vec2() || !frame.isVisible) continue;
auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot,
math::percent_to_unit(frame.scale), frame.rotation);
for (auto& corner : CORNERS)
{
vec4 world = layerTransform * vec4(corner, 0.0f, 1.0f);
minX = std::min(minX, world.x);
minY = std::min(minY, world.y);
maxX = std::max(maxX, world.x);
maxY = std::max(maxY, world.y);
any = true;
}
}
}
if (!any) return vec4(-1.0f);
return {minX, minY, maxX - minX, maxY - minY};
}
}

36
src/anm2/animation.h Normal file
View File

@@ -0,0 +1,36 @@
#pragma once
#include <map>
#include <string>
#include "item.h"
namespace anm2ed::anm2
{
constexpr auto FRAME_NUM_MIN = 1;
constexpr auto FRAME_NUM_MAX = FRAME_DELAY_MAX;
class Animation
{
public:
std::string name{"New Animation"};
int frameNum{FRAME_NUM_MIN};
bool isLoop{true};
Item rootAnimation;
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
std::map<int, Item> nullAnimations{};
Item triggers;
Animation() = default;
Animation(tinyxml2::XMLElement*);
Item* item_get(Type, int = -1);
void item_remove(Type, int = -1);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
std::string to_string();
int length();
glm::vec4 rect(bool);
};
}

43
src/anm2/animations.cpp Normal file
View File

@@ -0,0 +1,43 @@
#include "animations.h"
#include "xml_.h"
using namespace tinyxml2;
using namespace anm2ed::types;
using namespace anm2ed::util;
namespace anm2ed::anm2
{
Animations::Animations(XMLElement* element)
{
xml::query_string_attribute(element, "DefaultAnimation", &defaultAnimation);
for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation"))
items.push_back(Animation(child));
}
XMLElement* Animations::to_element(XMLDocument& document)
{
auto element = document.NewElement("Animations");
element->SetAttribute("DefaultAnimation", defaultAnimation.c_str());
for (auto& animation : items)
animation.serialize(document, element);
return element;
}
void Animations::serialize(XMLDocument& document, XMLElement* parent)
{
parent->InsertEndChild(to_element(document));
}
int Animations::length()
{
int length{};
for (auto& animation : items)
if (int animationLength = animation.length(); animationLength > length) length = animationLength;
return length;
}
}

20
src/anm2/animations.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "animation.h"
namespace anm2ed::anm2
{
constexpr auto MERGED_STRING = "(Merged)";
struct Animations
{
std::string defaultAnimation{};
std::vector<Animation> items{};
Animations() = default;
Animations(tinyxml2::XMLElement*);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
int length();
};
}

89
src/anm2/anm2.cpp Normal file
View File

@@ -0,0 +1,89 @@
#include "anm2.h"
#include "filesystem_.h"
#include "time_.h"
#include "vector_.h"
#include "xml_.h"
using namespace tinyxml2;
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace glm;
namespace anm2ed::anm2
{
Anm2::Anm2()
{
info.createdOn = time::get("%d-%B-%Y %I:%M:%S");
}
Anm2::Anm2(const std::string& path, std::string* errorString)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
if (errorString) *errorString = document.ErrorStr();
return;
}
filesystem::WorkingDirectory workingDirectory(path, true);
const XMLElement* element = document.RootElement();
if (auto infoElement = element->FirstChildElement("Info")) info = Info((XMLElement*)infoElement);
if (auto contentElement = element->FirstChildElement("Content")) content = Content((XMLElement*)contentElement);
if (auto animationsElement = element->FirstChildElement("Animations"))
animations = Animations((XMLElement*)animationsElement);
}
bool Anm2::serialize(const std::string& path, std::string* errorString)
{
XMLDocument document;
auto* element = document.NewElement("AnimatedActor");
document.InsertFirstChild(element);
info.serialize(document, element);
content.serialize(document, element);
animations.serialize(document, element);
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
{
if (errorString) *errorString = document.ErrorStr();
return false;
}
return true;
}
XMLElement* Anm2::to_element(XMLDocument& document)
{
auto element = document.NewElement("AnimatedActor");
document.InsertFirstChild(element);
info.serialize(document, element);
content.serialize(document, element);
animations.serialize(document, element);
return element;
}
std::string Anm2::to_string()
{
XMLDocument document{};
document.InsertEndChild(to_element(document));
return xml::document_to_string(document);
}
uint64_t Anm2::hash()
{
return std::hash<std::string>{}(to_string());
}
Frame* Anm2::frame_get(Reference reference)
{
if (auto item = item_get(reference); item)
if (vector::in_bounds(item->frames, reference.frameIndex)) return &item->frames[reference.frameIndex];
return nullptr;
}
}

78
src/anm2/anm2.h Normal file
View File

@@ -0,0 +1,78 @@
#pragma once
#include <string>
#include <tinyxml2/tinyxml2.h>
#include "types.h"
#include "animations.h"
#include "content.h"
#include "info.h"
namespace anm2ed::anm2
{
constexpr auto NO_PATH = "[No Path]";
struct Reference
{
int animationIndex{-1};
Type itemType{NONE};
int itemID{-1};
int frameIndex{-1};
int frameTime{-1};
auto operator<=>(const Reference&) const = default;
};
class Anm2
{
public:
Info info{};
Content content{};
Animations animations{};
Anm2();
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
bool serialize(const std::string&, std::string* = nullptr);
std::string to_string();
Anm2(const std::string&, std::string* = nullptr);
uint64_t hash();
Spritesheet* spritesheet_get(int);
bool spritesheet_add(const std::string&, const std::string&, int&);
void spritesheet_remove(int);
std::vector<std::string> spritesheet_labels_get();
std::set<int> spritesheets_unused();
bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type type, std::string*);
void layer_add(int&);
std::set<int> layers_unused(Reference = {});
bool layers_deserialize(const std::string&, types::merge::Type, std::string*);
void null_add(int&);
std::set<int> nulls_unused(Reference = {});
bool nulls_deserialize(const std::string&, types::merge::Type, std::string*);
void event_add(int&);
std::vector<std::string> event_labels_get();
std::set<int> events_unused(Reference = {});
bool events_deserialize(const std::string&, types::merge::Type, std::string*);
bool sound_add(const std::string& directory, const std::string& path, int& id);
std::vector<std::string> sound_labels_get();
std::set<int> sounds_unused();
bool sounds_deserialize(const std::string&, const std::string&, types::merge::Type, std::string*);
Animation* animation_get(Reference);
std::vector<std::string> animation_labels_get();
int animations_merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true);
bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr);
Item* item_get(Reference);
Reference layer_animation_add(Reference = {}, std::string = {}, int = 0,
types::locale::Type = types::locale::GLOBAL);
Reference null_animation_add(Reference = {}, std::string = {}, types::locale::Type = types::locale::GLOBAL);
Frame* frame_get(Reference);
};
}

View File

@@ -0,0 +1,139 @@
#include "anm2.h"
#include "vector_.h"
using namespace anm2ed::util;
using namespace anm2ed::types;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Animation* Anm2::animation_get(Reference reference)
{
return vector::find(animations.items, reference.animationIndex);
}
std::vector<std::string> Anm2::animation_labels_get()
{
std::vector<std::string> labels{};
labels.emplace_back("None");
for (auto& animation : animations.items)
labels.emplace_back(animation.name);
return labels;
}
bool Anm2::animations_deserialize(const std::string& string, int start, std::set<int>& indices,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
if (!document.FirstChildElement("Animation"))
{
if (errorString) *errorString = "No valid animation(s).";
return false;
}
int count{};
for (auto element = document.FirstChildElement("Animation"); element;
element = element->NextSiblingElement("Animation"))
{
auto index = start + count;
animations.items.insert(animations.items.begin() + start + count, Animation(element));
indices.insert(index);
count++;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
int Anm2::animations_merge(int target, std::set<int>& sources, merge::Type type, bool isDeleteAfter)
{
auto& items = animations.items;
auto& animation = animations.items.at(target);
if (!animation.name.ends_with(MERGED_STRING)) animation.name = animation.name + " " + MERGED_STRING;
auto merge_item = [&](Item& destination, Item& source)
{
switch (type)
{
case merge::APPEND:
destination.frames.insert(destination.frames.end(), source.frames.begin(), source.frames.end());
break;
case merge::PREPEND:
destination.frames.insert(destination.frames.begin(), source.frames.begin(), source.frames.end());
break;
case merge::REPLACE:
if (destination.frames.size() < source.frames.size()) destination.frames.resize(source.frames.size());
for (int i = 0; i < (int)source.frames.size(); i++)
destination.frames[i] = source.frames[i];
break;
case merge::IGNORE:
default:
break;
}
};
for (auto& i : sources)
{
if (i == target) continue;
if (i < 0 || i >= (int)items.size()) continue;
auto& source = items.at(i);
merge_item(animation.rootAnimation, source.rootAnimation);
for (auto& [id, layerAnimation] : source.layerAnimations)
{
if (!animation.layerAnimations.contains(id))
{
animation.layerAnimations[id] = layerAnimation;
animation.layerOrder.emplace_back(id);
}
merge_item(animation.layerAnimations[id], layerAnimation);
}
for (auto& [id, nullAnimation] : source.nullAnimations)
{
if (!animation.nullAnimations.contains(id)) animation.nullAnimations[id] = nullAnimation;
merge_item(animation.nullAnimations[id], nullAnimation);
}
merge_item(animation.triggers, source.triggers);
}
if (isDeleteAfter)
{
for (auto& source : std::ranges::reverse_view(sources))
{
if (source == target) continue;
items.erase(items.begin() + source);
}
}
int finalIndex = target;
if (isDeleteAfter)
{
int numDeletedBefore = 0;
for (auto& idx : sources)
{
if (idx == target) continue;
if (idx >= 0 && idx < target) ++numDeletedBefore;
}
finalIndex -= numDeletedBefore;
}
animation.frameNum = animation.length();
return finalIndex;
}
}

75
src/anm2/anm2_events.cpp Normal file
View File

@@ -0,0 +1,75 @@
#include "anm2.h"
#include <ranges>
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
void Anm2::event_add(int& id)
{
id = map::next_id_get(content.events);
content.events[id] = Event();
}
std::vector<std::string> Anm2::event_labels_get()
{
std::vector<std::string> labels{};
labels.emplace_back("None");
for (auto& event : content.events | std::views::values)
labels.emplace_back(event.name);
return labels;
}
std::set<int> Anm2::events_unused(Reference reference)
{
std::set<int> used{};
if (auto animation = animation_get(reference); animation)
for (auto& frame : animation->triggers.frames)
used.insert(frame.eventID);
else
for (auto& animation : animations.items)
for (auto& frame : animation.triggers.frames)
used.insert(frame.eventID);
std::set<int> unused{};
for (auto& id : content.events | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::events_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Event"))
{
if (errorString) *errorString = "No valid event(s).";
return false;
}
for (auto element = document.FirstChildElement("Event"); element; element = element->NextSiblingElement("Event"))
{
auto event = Event(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.events);
content.events[id] = event;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

83
src/anm2/anm2_items.cpp Normal file
View File

@@ -0,0 +1,83 @@
#include "anm2.h"
#include "map_.h"
#include "types.h"
#include "unordered_map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
namespace anm2ed::anm2
{
Item* Anm2::item_get(Reference reference)
{
if (Animation* animation = animation_get(reference))
{
switch (reference.itemType)
{
case ROOT:
return &animation->rootAnimation;
case LAYER:
return unordered_map::find(animation->layerAnimations, reference.itemID);
case NULL_:
return map::find(animation->nullAnimations, reference.itemID);
case TRIGGER:
return &animation->triggers;
default:
return nullptr;
}
}
return nullptr;
}
Reference Anm2::layer_animation_add(Reference reference, std::string name, int spritesheetID, locale::Type locale)
{
auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID;
auto& layer = content.layers[id];
layer.name = !name.empty() ? name : layer.name;
layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0;
auto add = [&](Animation* animation, int id)
{
animation->layerAnimations[id] = Item();
animation->layerOrder.push_back(id);
};
if (locale == locale::GLOBAL)
{
for (auto& animation : animations.items)
if (!animation.layerAnimations.contains(id)) add(&animation, id);
}
else if (locale == locale::LOCAL)
{
if (auto animation = animation_get(reference))
if (!animation->layerAnimations.contains(id)) add(animation, id);
}
return {reference.animationIndex, LAYER, id};
}
Reference Anm2::null_animation_add(Reference reference, std::string name, locale::Type locale)
{
auto id = reference.itemID == -1 ? map::next_id_get(content.nulls) : reference.itemID;
auto& null = content.nulls[id];
null.name = !name.empty() ? name : null.name;
auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); };
if (locale == locale::GLOBAL)
{
for (auto& animation : animations.items)
if (!animation.nullAnimations.contains(id)) add(&animation, id);
}
else if (locale == locale::LOCAL)
{
if (auto animation = animation_get(reference))
if (!animation->nullAnimations.contains(id)) add(animation, id);
}
return {reference.animationIndex, LAYER, id};
}
}

66
src/anm2/anm2_layers.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "anm2.h"
#include <ranges>
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
void Anm2::layer_add(int& id)
{
id = map::next_id_get(content.layers);
content.layers[id] = Layer();
}
std::set<int> Anm2::layers_unused(Reference reference)
{
std::set<int> used{};
std::set<int> unused{};
if (auto animation = animation_get(reference); animation)
for (auto& id : animation->layerAnimations | std::views::keys)
used.insert(id);
else
for (auto& animation : animations.items)
for (auto& id : animation.layerAnimations | std::views::keys)
used.insert(id);
for (auto& id : content.layers | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::layers_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Layer"))
{
if (errorString) *errorString = "No valid layer(s).";
return false;
}
for (auto element = document.FirstChildElement("Layer"); element; element = element->NextSiblingElement("Layer"))
{
auto layer = Layer(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.layers);
content.layers[id] = layer;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

66
src/anm2/anm2_nulls.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "anm2.h"
#include <ranges>
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
void Anm2::null_add(int& id)
{
id = map::next_id_get(content.nulls);
content.nulls[id] = Null();
}
std::set<int> Anm2::nulls_unused(Reference reference)
{
std::set<int> used{};
std::set<int> unused{};
if (auto animation = animation_get(reference); animation)
for (auto& id : animation->nullAnimations | std::views::keys)
used.insert(id);
else
for (auto& animation : animations.items)
for (auto& id : animation.nullAnimations | std::views::keys)
used.insert(id);
for (auto& id : content.nulls | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::nulls_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Null"))
{
if (errorString) *errorString = "No valid null(s).";
return false;
}
for (auto element = document.FirstChildElement("Null"); element; element = element->NextSiblingElement("Null"))
{
auto null = Null(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.nulls);
content.nulls[id] = null;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

75
src/anm2/anm2_sounds.cpp Normal file
View File

@@ -0,0 +1,75 @@
#include "anm2.h"
#include <ranges>
#include "filesystem_.h"
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
bool Anm2::sound_add(const std::string& directory, const std::string& path, int& id)
{
id = map::next_id_get(content.sounds);
content.sounds[id] = Sound(directory, path);
return true;
}
std::vector<std::string> Anm2::sound_labels_get()
{
std::vector<std::string> labels{};
labels.emplace_back("None");
for (auto& [id, sound] : content.sounds)
labels.emplace_back(sound.path.string());
return labels;
}
std::set<int> Anm2::sounds_unused()
{
std::set<int> used;
for (auto& animation : animations.items)
for (auto& trigger : animation.triggers.frames)
if (content.sounds.contains(trigger.soundID)) used.insert(trigger.soundID);
std::set<int> unused;
for (auto& id : content.sounds | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::sounds_deserialize(const std::string& string, const std::string& directory, merge::Type type,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Sound"))
{
if (errorString) *errorString = "No valid sound(s).";
return false;
}
filesystem::WorkingDirectory workingDirectory(directory);
for (auto element = document.FirstChildElement("Sound"); element; element = element->NextSiblingElement("Sound"))
{
auto sound = Sound(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.sounds);
content.sounds[id] = std::move(sound);
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

View File

@@ -0,0 +1,87 @@
#include "anm2.h"
#include <ranges>
#include "filesystem_.h"
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Spritesheet* Anm2::spritesheet_get(int id)
{
return map::find(content.spritesheets, id);
}
void Anm2::spritesheet_remove(int id)
{
content.spritesheets.erase(id);
}
bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id)
{
Spritesheet spritesheet(directory, path);
if (!spritesheet.is_valid()) return false;
id = map::next_id_get(content.spritesheets);
content.spritesheets[id] = std::move(spritesheet);
return true;
}
std::set<int> Anm2::spritesheets_unused()
{
std::set<int> used{};
for (auto& layer : content.layers | std::views::values)
if (layer.is_spritesheet_valid()) used.insert(layer.spritesheetID);
std::set<int> unused{};
for (auto& id : content.spritesheets | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
std::vector<std::string> Anm2::spritesheet_labels_get()
{
std::vector<std::string> labels{};
labels.emplace_back("None");
for (auto& [id, spritesheet] : content.spritesheets)
labels.emplace_back(std::format(SPRITESHEET_FORMAT, id, spritesheet.path.c_str()));
return labels;
}
bool Anm2::spritesheets_deserialize(const std::string& string, const std::string& directory, merge::Type type,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Spritesheet"))
{
if (errorString) *errorString = "No valid spritesheet(s).";
return false;
}
filesystem::WorkingDirectory workingDirectory(directory);
for (auto element = document.FirstChildElement("Spritesheet"); element;
element = element->NextSiblingElement("Spritesheet"))
{
auto spritesheet = Spritesheet(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.spritesheets);
content.spritesheets[id] = std::move(spritesheet);
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

84
src/anm2/anm2_type.h Normal file
View File

@@ -0,0 +1,84 @@
#pragma once
#include "icon.h"
#include <glm/glm/vec2.hpp>
#include <glm/glm/vec3.hpp>
#include <glm/glm/vec4.hpp>
namespace anm2ed::anm2
{
constexpr auto ROOT_COLOR = glm::vec4(0.140f, 0.310f, 0.560f, 1.000f);
constexpr auto ROOT_COLOR_ACTIVE = glm::vec4(0.240f, 0.520f, 0.880f, 1.000f);
constexpr auto ROOT_COLOR_HOVERED = glm::vec4(0.320f, 0.640f, 1.000f, 1.000f);
constexpr auto LAYER_COLOR = glm::vec4(0.640f, 0.320f, 0.110f, 1.000f);
constexpr auto LAYER_COLOR_ACTIVE = glm::vec4(0.840f, 0.450f, 0.170f, 1.000f);
constexpr auto LAYER_COLOR_HOVERED = glm::vec4(0.960f, 0.560f, 0.240f, 1.000f);
constexpr auto NULL_COLOR = glm::vec4(0.140f, 0.430f, 0.200f, 1.000f);
constexpr auto NULL_COLOR_ACTIVE = glm::vec4(0.250f, 0.650f, 0.350f, 1.000f);
constexpr auto NULL_COLOR_HOVERED = glm::vec4(0.350f, 0.800f, 0.480f, 1.000f);
constexpr auto TRIGGER_COLOR = glm::vec4(0.620f, 0.150f, 0.260f, 1.000f);
constexpr auto TRIGGER_COLOR_ACTIVE = glm::vec4(0.820f, 0.250f, 0.380f, 1.000f);
constexpr auto TRIGGER_COLOR_HOVERED = glm::vec4(0.950f, 0.330f, 0.490f, 1.000f);
#define TYPE_LIST \
X(NONE, "", "", resource::icon::NONE, glm::vec4(), glm::vec4(), glm::vec4()) \
X(ROOT, "Root", "RootAnimation", resource::icon::ROOT, ROOT_COLOR, ROOT_COLOR_ACTIVE, ROOT_COLOR_HOVERED) \
X(LAYER, "Layer", "LayerAnimation", resource::icon::LAYER, LAYER_COLOR, LAYER_COLOR_ACTIVE, LAYER_COLOR_HOVERED) \
X(NULL_, "Null", "NullAnimation", resource::icon::NULL_, NULL_COLOR, NULL_COLOR_ACTIVE, NULL_COLOR_HOVERED) \
X(TRIGGER, "Triggers", "Triggers", resource::icon::TRIGGERS, TRIGGER_COLOR, TRIGGER_COLOR_ACTIVE, \
TRIGGER_COLOR_HOVERED)
enum Type
{
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) symbol,
TYPE_LIST
#undef X
};
constexpr const char* TYPE_STRINGS[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) string,
TYPE_LIST
#undef X
};
constexpr const char* TYPE_ITEM_STRINGS[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) itemString,
TYPE_LIST
#undef X
};
constexpr resource::icon::Type TYPE_ICONS[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) icon,
TYPE_LIST
#undef X
};
constexpr glm::vec4 TYPE_COLOR[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) color,
TYPE_LIST
#undef X
};
constexpr glm::vec4 TYPE_COLOR_ACTIVE[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorActive,
TYPE_LIST
#undef X
};
constexpr glm::vec4 TYPE_COLOR_HOVERED[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorHovered,
TYPE_LIST
#undef X
};
enum ChangeType
{
ADD,
SUBTRACT,
ADJUST
};
}

56
src/anm2/content.cpp Normal file
View File

@@ -0,0 +1,56 @@
#include "content.h"
using namespace tinyxml2;
namespace anm2ed::anm2
{
Content::Content(XMLElement* element)
{
int id{};
if (auto spritesheetsElement = element->FirstChildElement("Spritesheets"))
for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
child = child->NextSiblingElement("Spritesheet"))
spritesheets[id] = Spritesheet(child, id);
if (auto layersElement = element->FirstChildElement("Layers"))
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
layers[id] = Layer(child, id);
if (auto nullsElement = element->FirstChildElement("Nulls"))
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
nulls[id] = Null(child, id);
if (auto eventsElement = element->FirstChildElement("Events"))
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
events[id] = Event(child, id);
}
void Content::serialize(XMLDocument& document, XMLElement* parent)
{
auto element = document.NewElement("Content");
auto spritesheetsElement = document.NewElement("Spritesheets");
for (auto& [id, spritesheet] : spritesheets)
spritesheet.serialize(document, spritesheetsElement, id);
element->InsertEndChild(spritesheetsElement);
auto layersElement = document.NewElement("Layers");
for (auto& [id, layer] : layers)
layer.serialize(document, layersElement, id);
element->InsertEndChild(layersElement);
auto nullsElement = document.NewElement("Nulls");
for (auto& [id, null] : nulls)
null.serialize(document, nullsElement, id);
element->InsertEndChild(nullsElement);
auto eventsElement = document.NewElement("Events");
for (auto& [id, event] : events)
event.serialize(document, eventsElement, id);
element->InsertEndChild(eventsElement);
parent->InsertEndChild(element);
}
}

26
src/anm2/content.h Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include <map>
#include "event.h"
#include "layer.h"
#include "null.h"
#include "sound.h"
#include "spritesheet.h"
namespace anm2ed::anm2
{
struct Content
{
std::map<int, Spritesheet> spritesheets{};
std::map<int, Layer> layers{};
std::map<int, Null> nulls{};
std::map<int, Event> events{};
std::map<int, Sound> sounds{};
Content() = default;
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
Content(tinyxml2::XMLElement*);
};
}

36
src/anm2/event.cpp Normal file
View File

@@ -0,0 +1,36 @@
#include "event.h"
#include "xml_.h"
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Event::Event(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
}
XMLElement* Event::to_element(XMLDocument& document, int id)
{
auto element = document.NewElement("Event");
element->SetAttribute("Id", id);
element->SetAttribute("Name", name.c_str());
return element;
}
void Event::serialize(XMLDocument& document, XMLElement* parent, int id)
{
parent->InsertEndChild(to_element(document, id));
}
std::string Event::to_string(int id)
{
XMLDocument document{};
document.InsertEndChild(to_element(document, id));
return xml::document_to_string(document);
}
}

20
src/anm2/event.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <string>
#include <tinyxml2/tinyxml2.h>
namespace anm2ed::anm2
{
class Event
{
public:
std::string name{"New Event"};
int soundID{-1};
Event() = default;
Event(tinyxml2::XMLElement*, int&);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
std::string to_string(int);
};
}

128
src/anm2/frame.cpp Normal file
View File

@@ -0,0 +1,128 @@
#include "frame.h"
#include "math_.h"
#include "xml_.h"
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Frame::Frame(XMLElement* element, Type type)
{
if (type != TRIGGER)
{
element->QueryFloatAttribute("XPosition", &position.x);
element->QueryFloatAttribute("YPosition", &position.y);
if (type == LAYER)
{
element->QueryFloatAttribute("XPivot", &pivot.x);
element->QueryFloatAttribute("YPivot", &pivot.y);
element->QueryFloatAttribute("XCrop", &crop.x);
element->QueryFloatAttribute("YCrop", &crop.y);
element->QueryFloatAttribute("Width", &size.x);
element->QueryFloatAttribute("Height", &size.y);
}
element->QueryFloatAttribute("XScale", &scale.x);
element->QueryFloatAttribute("YScale", &scale.y);
element->QueryIntAttribute("Delay", &delay);
element->QueryBoolAttribute("Visible", &isVisible);
xml::query_color_attribute(element, "RedTint", tint.r);
xml::query_color_attribute(element, "GreenTint", tint.g);
xml::query_color_attribute(element, "BlueTint", tint.b);
xml::query_color_attribute(element, "AlphaTint", tint.a);
xml::query_color_attribute(element, "RedOffset", colorOffset.r);
xml::query_color_attribute(element, "GreenOffset", colorOffset.g);
xml::query_color_attribute(element, "BlueOffset", colorOffset.b);
element->QueryFloatAttribute("Rotation", &rotation);
element->QueryBoolAttribute("Interpolated", &isInterpolated);
}
else
{
element->QueryIntAttribute("EventId", &eventID);
element->QueryIntAttribute("AtFrame", &atFrame);
}
}
XMLElement* Frame::to_element(XMLDocument& document, Type type)
{
auto element = document.NewElement(type == TRIGGER ? "Trigger" : "Frame");
switch (type)
{
case ROOT:
case NULL_:
element->SetAttribute("XPosition", position.x);
element->SetAttribute("YPosition", position.y);
element->SetAttribute("Delay", delay);
element->SetAttribute("Visible", isVisible);
element->SetAttribute("XScale", scale.x);
element->SetAttribute("YScale", scale.y);
element->SetAttribute("RedTint", math::float_to_uint8(tint.r));
element->SetAttribute("GreenTint", math::float_to_uint8(tint.g));
element->SetAttribute("BlueTint", math::float_to_uint8(tint.b));
element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a));
element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r));
element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g));
element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b));
element->SetAttribute("Rotation", rotation);
element->SetAttribute("Interpolated", isInterpolated);
break;
case LAYER:
element->SetAttribute("XPosition", position.x);
element->SetAttribute("YPosition", position.y);
element->SetAttribute("XPivot", pivot.x);
element->SetAttribute("YPivot", pivot.y);
element->SetAttribute("XCrop", crop.x);
element->SetAttribute("YCrop", crop.y);
element->SetAttribute("Width", size.x);
element->SetAttribute("Height", size.y);
element->SetAttribute("XScale", scale.x);
element->SetAttribute("YScale", scale.y);
element->SetAttribute("Delay", delay);
element->SetAttribute("Visible", isVisible);
element->SetAttribute("RedTint", math::float_to_uint8(tint.r));
element->SetAttribute("GreenTint", math::float_to_uint8(tint.g));
element->SetAttribute("BlueTint", math::float_to_uint8(tint.b));
element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a));
element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r));
element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g));
element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b));
element->SetAttribute("Rotation", rotation);
element->SetAttribute("Interpolated", isInterpolated);
break;
case TRIGGER:
element->SetAttribute("EventId", eventID);
element->SetAttribute("AtFrame", atFrame);
break;
default:
break;
}
return element;
}
void Frame::serialize(XMLDocument& document, XMLElement* parent, Type type)
{
parent->InsertEndChild(to_element(document, type));
}
std::string Frame::to_string(Type type)
{
XMLDocument document{};
document.InsertEndChild(to_element(document, type));
return xml::document_to_string(document);
}
void Frame::shorten() { delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); }
void Frame::extend() { delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); }
bool Frame::is_visible(Type type)
{
if (type == TRIGGER)
return isVisible && eventID > -1;
else
return isVisible;
}
}

57
src/anm2/frame.h Normal file
View File

@@ -0,0 +1,57 @@
#pragma once
#include <optional>
#include <string>
#include <tinyxml2/tinyxml2.h>
#include "anm2_type.h"
#include "types.h"
namespace anm2ed::anm2
{
constexpr auto FRAME_DELAY_MIN = 1;
constexpr auto FRAME_DELAY_MAX = 100000;
#define MEMBERS \
X(isVisible, bool, true) \
X(isInterpolated, bool, false) \
X(rotation, float, 0.0f) \
X(delay, int, FRAME_DELAY_MIN) \
X(atFrame, int, -1) \
X(eventID, int, -1) \
X(soundID, int, -1) \
X(pivot, glm::vec2, {}) \
X(crop, glm::vec2, {}) \
X(position, glm::vec2, {}) \
X(size, glm::vec2, {}) \
X(scale, glm::vec2, glm::vec2(100.0f)) \
X(colorOffset, glm::vec3, types::color::TRANSPARENT) \
X(tint, glm::vec4, types::color::WHITE)
class Frame
{
public:
#define X(name, type, ...) type name = __VA_ARGS__;
MEMBERS
#undef X
Frame() = default;
Frame(tinyxml2::XMLElement*, Type);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type);
std::string to_string(Type type);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type);
void shorten();
void extend();
bool is_visible(Type = NONE);
};
struct FrameChange
{
#define X(name, type, ...) std::optional<type> name{};
MEMBERS
#undef X
};
#undef MEMBERS
}

40
src/anm2/info.cpp Normal file
View File

@@ -0,0 +1,40 @@
#include "info.h"
#include "xml_.h"
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Info::Info(XMLElement* element)
{
if (!element) return;
xml::query_string_attribute(element, "CreatedBy", &createdBy);
xml::query_string_attribute(element, "CreatedOn", &createdOn);
element->QueryIntAttribute("Fps", &fps);
element->QueryIntAttribute("Version", &version);
}
XMLElement* Info::to_element(XMLDocument& document)
{
auto element = document.NewElement("Info");
element->SetAttribute("CreatedBy", createdBy.c_str());
element->SetAttribute("CreatedOn", createdOn.c_str());
element->SetAttribute("Fps", fps);
element->SetAttribute("Version", version);
return element;
}
void Info::serialize(XMLDocument& document, XMLElement* parent)
{
parent->InsertEndChild(to_element(document));
}
std::string Info::to_string()
{
XMLDocument document{};
document.InsertEndChild(to_element(document));
return xml::document_to_string(document);
}
}

25
src/anm2/info.h Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include <string>
#include <tinyxml2/tinyxml2.h>
namespace anm2ed::anm2
{
constexpr auto FPS_MIN = 1;
constexpr auto FPS_MAX = 120;
class Info
{
public:
std::string createdBy{"robot"};
std::string createdOn{};
int fps = 30;
int version{};
Info() = default;
Info(tinyxml2::XMLElement*);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument& document);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
std::string to_string();
};
}

252
src/anm2/item.cpp Normal file
View File

@@ -0,0 +1,252 @@
#include "item.h"
#include <ranges>
#include "vector_.h"
#include "xml_.h"
using namespace anm2ed::util;
using namespace tinyxml2;
using namespace glm;
namespace anm2ed::anm2
{
Item::Item(XMLElement* element, Type type, int* id)
{
if (type == LAYER && id) element->QueryIntAttribute("LayerId", id);
if (type == NULL_ && id) element->QueryIntAttribute("NullId", id);
element->QueryBoolAttribute("Visible", &isVisible);
for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
frames.push_back(Frame(child, type));
}
XMLElement* Item::to_element(XMLDocument& document, Type type, int id)
{
auto element = document.NewElement(TYPE_ITEM_STRINGS[type]);
if (type == LAYER) element->SetAttribute("LayerId", id);
if (type == NULL_) element->SetAttribute("NullId", id);
if (type == LAYER || type == NULL_) element->SetAttribute("Visible", isVisible);
for (auto& frame : frames)
frame.serialize(document, element, type);
return element;
}
void Item::serialize(XMLDocument& document, XMLElement* parent, Type type, int id)
{
parent->InsertEndChild(to_element(document, type, id));
}
std::string Item::to_string(Type type, int id)
{
XMLDocument document{};
document.InsertEndChild(to_element(document, type, id));
return xml::document_to_string(document);
}
int Item::length(Type type)
{
int length{};
if (type == TRIGGER)
for (auto& frame : frames)
length = frame.atFrame > length ? frame.atFrame : length;
else
for (auto& frame : frames)
length += frame.delay;
return length;
}
Frame Item::frame_generate(float time, Type type)
{
Frame frame{};
frame.isVisible = false;
if (frames.empty()) return frame;
Frame* frameNext = nullptr;
int delayCurrent = 0;
int delayNext = 0;
for (auto [i, iFrame] : std::views::enumerate(frames))
{
if (type == TRIGGER)
{
if ((int)time == iFrame.atFrame)
{
frame = iFrame;
break;
}
}
else
{
frame = iFrame;
delayNext += frame.delay;
if (time >= delayCurrent && time < delayNext)
{
if (i + 1 < (int)frames.size())
frameNext = &frames[i + 1];
else
frameNext = nullptr;
break;
}
delayCurrent += frame.delay;
}
}
if (type != TRIGGER && frame.isInterpolated && frameNext && frame.delay > 1)
{
auto interpolation = (time - delayCurrent) / (delayNext - delayCurrent);
frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation);
frame.position = glm::mix(frame.position, frameNext->position, interpolation);
frame.scale = glm::mix(frame.scale, frameNext->scale, interpolation);
frame.colorOffset = glm::mix(frame.colorOffset, frameNext->colorOffset, interpolation);
frame.tint = glm::mix(frame.tint, frameNext->tint, interpolation);
}
return frame;
}
void Item::frames_change(FrameChange& change, ChangeType type, int start, int numberFrames)
{
auto useStart = numberFrames > -1 ? start : 0;
auto end = numberFrames > -1 ? start + numberFrames : (int)frames.size();
end = glm::clamp(end, start, (int)frames.size());
for (int i = useStart; i < end; i++)
{
Frame& frame = frames[i];
if (change.isVisible) frame.isVisible = *change.isVisible;
if (change.isInterpolated) frame.isInterpolated = *change.isInterpolated;
switch (type)
{
case ADJUST:
if (change.rotation) frame.rotation = *change.rotation;
if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, *change.delay);
if (change.crop) frame.crop = *change.crop;
if (change.pivot) frame.pivot = *change.pivot;
if (change.position) frame.position = *change.position;
if (change.size) frame.size = *change.size;
if (change.scale) frame.scale = *change.scale;
if (change.colorOffset) frame.colorOffset = glm::clamp(*change.colorOffset, 0.0f, 1.0f);
if (change.tint) frame.tint = glm::clamp(*change.tint, 0.0f, 1.0f);
break;
case ADD:
if (change.rotation) frame.rotation += *change.rotation;
if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay + *change.delay);
if (change.crop) frame.crop += *change.crop;
if (change.pivot) frame.pivot += *change.pivot;
if (change.position) frame.position += *change.position;
if (change.size) frame.size += *change.size;
if (change.scale) frame.scale += *change.scale;
if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset + *change.colorOffset, 0.0f, 1.0f);
if (change.tint) frame.tint = glm::clamp(frame.tint + *change.tint, 0.0f, 1.0f);
break;
case SUBTRACT:
if (change.rotation) frame.rotation -= *change.rotation;
if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay - *change.delay);
if (change.crop) frame.crop -= *change.crop;
if (change.pivot) frame.pivot -= *change.pivot;
if (change.position) frame.position -= *change.position;
if (change.size) frame.size -= *change.size;
if (change.scale) frame.scale -= *change.scale;
if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset - *change.colorOffset, 0.0f, 1.0f);
if (change.tint) frame.tint = glm::clamp(frame.tint - *change.tint, 0.0f, 1.0f);
break;
}
}
}
bool Item::frames_deserialize(const std::string& string, Type type, int start, std::set<int>& indices,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
if (!document.FirstChildElement("Frame"))
{
if (errorString) *errorString = "No valid frame(s).";
return false;
}
int count{};
for (auto element = document.FirstChildElement("Frame"); element; element = element->NextSiblingElement("Frame"))
{
auto index = start + count;
frames.insert(frames.begin() + start + count, Frame(element, type));
indices.insert(index);
count++;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
void Item::frames_bake(int index, int interval, bool isRoundScale, bool isRoundRotation)
{
if (!vector::in_bounds(frames, index)) return;
Frame& frame = frames[index];
if (frame.delay == FRAME_DELAY_MIN) return;
Frame frameNext = vector::in_bounds(frames, index + 1) ? frames[index + 1] : frame;
int delay{};
int i = index;
while (delay < frame.delay)
{
Frame baked = frame;
float interpolation = (float)delay / frame.delay;
baked.delay = std::min(interval, frame.delay - delay);
baked.isInterpolated = (i == index) ? frame.isInterpolated : false;
baked.rotation = glm::mix(frame.rotation, frameNext.rotation, interpolation);
baked.position = glm::mix(frame.position, frameNext.position, interpolation);
baked.scale = glm::mix(frame.scale, frameNext.scale, interpolation);
baked.colorOffset = glm::mix(frame.colorOffset, frameNext.colorOffset, interpolation);
baked.tint = glm::mix(frame.tint, frameNext.tint, interpolation);
if (isRoundScale) baked.scale = vec2(ivec2(baked.scale));
if (isRoundRotation) baked.rotation = (int)baked.rotation;
if (i == index)
frames[i] = baked;
else
frames.insert(frames.begin() + i, baked);
i++;
delay += baked.delay;
}
}
void Item::frames_generate_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count, int delay)
{
for (int i = 0; i < count; i++)
{
Frame frame{};
frame.delay = delay;
frame.pivot = pivot;
frame.size = size;
frame.crop = startPosition + ivec2(size.x * (i % columns), size.y * (i / columns));
frames.emplace_back(frame);
}
}
}

28
src/anm2/item.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <set>
#include <vector>
#include "frame.h"
namespace anm2ed::anm2
{
class Item
{
public:
std::vector<Frame> frames{};
bool isVisible{true};
Item() = default;
Item(tinyxml2::XMLElement*, Type, int* = nullptr);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, int);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1);
std::string to_string(Type, int = -1);
int length(Type);
Frame frame_generate(float, Type);
void frames_change(FrameChange&, ChangeType, int, int = 0);
bool frames_deserialize(const std::string&, Type, int, std::set<int>&, std::string*);
void frames_bake(int, int, bool, bool);
void frames_generate_from_grid(glm::ivec2, glm::ivec2, glm::ivec2, int, int, int);
};
}

43
src/anm2/layer.cpp Normal file
View File

@@ -0,0 +1,43 @@
#include "layer.h"
#include "xml_.h"
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Layer::Layer(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("SpritesheetId", &spritesheetID);
}
XMLElement* Layer::to_element(XMLDocument& document, int id)
{
auto element = document.NewElement("Layer");
element->SetAttribute("Id", id);
element->SetAttribute("Name", name.c_str());
element->SetAttribute("SpritesheetId", spritesheetID);
return element;
}
void Layer::serialize(XMLDocument& document, XMLElement* parent, int id)
{
parent->InsertEndChild(to_element(document, id));
}
std::string Layer::to_string(int id)
{
XMLDocument document{};
document.InsertEndChild(to_element(document, id));
return xml::document_to_string(document);
}
bool Layer::is_spritesheet_valid()
{
return spritesheetID > -1;
}
}

24
src/anm2/layer.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <tinyxml2/tinyxml2.h>
namespace anm2ed::anm2
{
constexpr auto LAYER_FORMAT = "#{} {} (Spritesheet: #{})";
constexpr auto LAYER_NO_SPRITESHEET_FORMAT = "#{} {} (No Spritesheet)";
class Layer
{
public:
std::string name{"New Layer"};
int spritesheetID{};
Layer() = default;
Layer(tinyxml2::XMLElement*, int&);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
std::string to_string(int);
bool is_spritesheet_valid();
};
}

38
src/anm2/null.cpp Normal file
View File

@@ -0,0 +1,38 @@
#include "null.h"
#include "xml_.h"
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Null::Null(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
element->QueryBoolAttribute("ShowRect", &isShowRect);
}
XMLElement* Null::to_element(XMLDocument& document, int id)
{
auto element = document.NewElement("Null");
element->SetAttribute("Id", id);
element->SetAttribute("Name", name.c_str());
if (isShowRect) element->SetAttribute("ShowRect", isShowRect);
return element;
}
void Null::serialize(XMLDocument& document, XMLElement* parent, int id)
{
parent->InsertEndChild(to_element(document, id));
}
std::string Null::to_string(int id)
{
XMLDocument document{};
document.InsertEndChild(to_element(document, id));
return xml::document_to_string(document);
}
}

22
src/anm2/null.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include <string>
#include <tinyxml2/tinyxml2.h>
namespace anm2ed::anm2
{
constexpr auto NULL_FORMAT = "#{} {}";
class Null
{
public:
std::string name{"New Null"};
bool isShowRect{};
Null() = default;
Null(tinyxml2::XMLElement*, int&);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument& document, int id);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
std::string to_string(int);
};
}

73
src/anm2/sound.cpp Normal file
View File

@@ -0,0 +1,73 @@
#include "sound.h"
#include "filesystem_.h"
#include "xml_.h"
using namespace anm2ed::resource;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Sound::Sound(const Sound& other) : path(other.path)
{
audio = path.empty() ? Audio() : Audio(path.c_str());
}
Sound& Sound::operator=(const Sound& other)
{
if (this != &other)
{
path = other.path;
audio = path.empty() ? Audio() : Audio(path.c_str());
}
return *this;
}
Sound::Sound(const std::string& directory, const std::string& path)
{
filesystem::WorkingDirectory workingDirectory(directory);
this->path = !path.empty() ? std::filesystem::relative(path) : this->path;
this->path = filesystem::path_lower_case_backslash_handle(this->path);
audio = Audio(this->path.c_str());
}
Sound::Sound(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_path_attribute(element, "Path", &path);
path = filesystem::path_lower_case_backslash_handle(path);
audio = Audio(path.c_str());
}
XMLElement* Sound::to_element(XMLDocument& document, int id)
{
auto element = document.NewElement("Sound");
element->SetAttribute("Id", id);
element->SetAttribute("Path", path.c_str());
return element;
}
std::string Sound::to_string(int id)
{
XMLDocument document{};
document.InsertEndChild(to_element(document, id));
return xml::document_to_string(document);
}
void Sound::reload(const std::string& directory)
{
*this = Sound(directory, this->path);
}
bool Sound::is_valid()
{
return audio.is_valid();
}
void Sound::play()
{
audio.play();
}
}

32
src/anm2/sound.h Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include <filesystem>
#include <tinyxml2/tinyxml2.h>
#include "audio.h"
namespace anm2ed::anm2
{
constexpr auto SOUND_FORMAT = "{}";
class Sound
{
public:
std::filesystem::path path{};
resource::Audio audio{};
Sound() = default;
Sound(Sound&&) noexcept = default;
Sound& operator=(Sound&&) noexcept = default;
Sound(const Sound&);
Sound& operator=(const Sound&);
Sound(tinyxml2::XMLElement*, int&);
Sound(const std::string&, const std::string&);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
std::string to_string(int);
void reload(const std::string&);
bool is_valid();
void play();
};
}

69
src/anm2/spritesheet.cpp Normal file
View File

@@ -0,0 +1,69 @@
#include "spritesheet.h"
#include "filesystem_.h"
#include "xml_.h"
using namespace anm2ed::resource;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Spritesheet::Spritesheet(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_path_attribute(element, "Path", &path);
// Spritesheet paths from Isaac Rebirth are made with the assumption that paths are case-insensitive
// However when using the resource dumper, the spritesheet paths are all lowercase (on Linux anyway)
// This will handle this case and make the paths OS-agnostic
path = filesystem::path_lower_case_backslash_handle(path);
texture = Texture(path);
}
Spritesheet::Spritesheet(const std::string& directory, const std::string& path)
{
filesystem::WorkingDirectory workingDirectory(directory);
this->path = !path.empty() ? std::filesystem::relative(path) : this->path;
this->path = filesystem::path_lower_case_backslash_handle(this->path);
texture = Texture(this->path);
}
XMLElement* Spritesheet::to_element(XMLDocument& document, int id)
{
auto element = document.NewElement("Spritesheet");
element->SetAttribute("Id", id);
element->SetAttribute("Path", path.c_str());
return element;
}
void Spritesheet::serialize(XMLDocument& document, XMLElement* parent, int id)
{
parent->InsertEndChild(to_element(document, id));
}
std::string Spritesheet::to_string(int id)
{
XMLDocument document{};
document.InsertEndChild(to_element(document, id));
return xml::document_to_string(document);
}
bool Spritesheet::save(const std::string& directory, const std::string& path)
{
filesystem::WorkingDirectory workingDirectory(directory);
this->path = !path.empty() ? std::filesystem::relative(path).string() : this->path.string();
return texture.write_png(this->path);
}
void Spritesheet::reload(const std::string& directory)
{
*this = Spritesheet(directory, this->path);
}
bool Spritesheet::is_valid()
{
return texture.is_valid();
}
}

30
src/anm2/spritesheet.h Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include <filesystem>
#include <string>
#include <tinyxml2/tinyxml2.h>
#include "texture.h"
namespace anm2ed::anm2
{
constexpr auto SPRITESHEET_FORMAT_C = "#%d %s";
constexpr auto SPRITESHEET_FORMAT = "#{} {}";
class Spritesheet
{
public:
std::filesystem::path path{};
resource::Texture texture;
Spritesheet() = default;
Spritesheet(tinyxml2::XMLElement*, int&);
Spritesheet(const std::string&, const std::string& = {});
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
std::string to_string(int id);
bool save(const std::string&, const std::string& = {});
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
void reload(const std::string&);
bool is_valid();
};
}

View File

@@ -1,244 +1,351 @@
#include "canvas.h"
static void _canvas_framebuffer_set(Canvas* self, const ivec2& size)
#include <algorithm>
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/gtc/matrix_inverse.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "math_.h"
#include "texture.h"
using namespace glm;
using namespace anm2ed::resource;
using namespace anm2ed::util;
using namespace anm2ed::canvas;
namespace anm2ed
{
self->size = size;
self->previousSize = size;
constexpr float AXIS_VERTICES[] = {-1.0f, 0.0f, 1.0f, 0.0f};
constexpr float GRID_VERTICES[] = {-1.f, -1.f, 0.f, 0.f, 3.f, -1.f, 2.f, 0.f, -1.f, 3.f, 0.f, 2.f};
constexpr float RECT_VERTICES[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
constexpr GLuint TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0};
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
Canvas::Canvas() = default;
glBindTexture(GL_TEXTURE_2D, self->framebuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self->size.x, self->size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Canvas::Canvas(vec2 size)
{
this->size = size;
previousSize = size;
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->framebuffer, 0);
glBindRenderbuffer(GL_RENDERBUFFER, self->rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, self->size.x, self->size.y);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, self->rbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void canvas_init(Canvas* self, const ivec2& size)
{
// Axis
glGenVertexArrays(1, &self->axisVAO);
glGenBuffers(1, &self->axisVBO);
glGenVertexArrays(1, &axisVAO);
glGenBuffers(1, &axisVBO);
glBindVertexArray(self->axisVAO);
glBindVertexArray(axisVAO);
glBindBuffer(GL_ARRAY_BUFFER, self->axisVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_AXIS_VERTICES), CANVAS_AXIS_VERTICES, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, axisVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(AXIS_VERTICES), AXIS_VERTICES, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, sizeof(f32), (void*)0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
// Grid
glGenVertexArrays(1, &self->gridVAO);
glGenBuffers(1, &self->gridVBO);
glGenVertexArrays(1, &gridVAO);
glBindVertexArray(gridVAO);
glBindVertexArray(self->gridVAO);
glBindBuffer(GL_ARRAY_BUFFER, self->gridVBO);
glGenBuffers(1, &gridVBO);
glBindBuffer(GL_ARRAY_BUFFER, gridVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(GRID_VERTICES), GRID_VERTICES, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(f32), (void*)0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, (void*)0);
// Rect
glGenVertexArrays(1, &self->rectVAO);
glGenBuffers(1, &self->rectVBO);
glBindVertexArray(self->rectVAO);
glBindBuffer(GL_ARRAY_BUFFER, self->rectVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_RECT_VERTICES), CANVAS_RECT_VERTICES, GL_STATIC_DRAW);
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);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, (void*)(sizeof(float) * 2));
glBindVertexArray(0);
// Texture
glGenVertexArrays(1, &self->textureVAO);
glGenBuffers(1, &self->textureVBO);
glGenBuffers(1, &self->textureEBO);
// Rect
glGenVertexArrays(1, &rectVAO);
glGenBuffers(1, &rectVBO);
glBindVertexArray(self->textureVAO);
glBindVertexArray(rectVAO);
glBindBuffer(GL_ARRAY_BUFFER, self->textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(f32) * 4 * 4, nullptr, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self->textureEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GL_TEXTURE_INDICES), GL_TEXTURE_INDICES, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, rectVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(RECT_VERTICES), RECT_VERTICES, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(f32), (void*)0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
// Texture
glGenVertexArrays(1, &textureVAO);
glGenBuffers(1, &textureVBO);
glGenBuffers(1, &textureEBO);
glBindVertexArray(textureVAO);
glBindBuffer(GL_ARRAY_BUFFER, textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 4 * 4, nullptr, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, textureEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(TEXTURE_INDICES), TEXTURE_INDICES, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(f32), (void*)(2 * sizeof(f32)));
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
// Framebuffer
glGenTextures(1, &self->framebuffer);
glGenFramebuffers(1, &self->fbo);
glGenRenderbuffers(1, &self->rbo);
_canvas_framebuffer_set(self, size);
glGenTextures(1, &texture);
glGenFramebuffers(1, &fbo);
glGenRenderbuffers(1, &rbo);
self->isInit = true;
}
mat4 canvas_transform_get(Canvas* self, vec2 pan, f32 zoom, OriginType origin)
{
f32 zoomFactor = PERCENT_TO_UNIT(zoom);
mat4 projection = glm::ortho(0.0f, (f32)self->size.x, 0.0f, (f32)self->size.y, -1.0f, 1.0f);
mat4 view = mat4{1.0f};
vec2 size = vec2(self->size.x, self->size.y);
switch (origin)
{
case ORIGIN_TOP_LEFT:
view = glm::translate(view, vec3(pan, 0.0f));
break;
default:
view = glm::translate(view, vec3((size * 0.5f) + pan, 0.0f));
break;
framebuffer_set();
}
Canvas::~Canvas()
{
if (!is_valid()) return;
glDeleteFramebuffers(1, &fbo);
glDeleteRenderbuffers(1, &rbo);
glDeleteTextures(1, &texture);
glDeleteVertexArrays(1, &axisVAO);
glDeleteBuffers(1, &axisVBO);
glDeleteVertexArrays(1, &gridVAO);
glDeleteBuffers(1, &gridVBO);
glDeleteVertexArrays(1, &rectVAO);
glDeleteBuffers(1, &rectVBO);
}
bool Canvas::is_valid()
{
return fbo != 0;
}
void Canvas::framebuffer_set()
{
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, size.x, size.y);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void Canvas::framebuffer_resize_check()
{
if (previousSize != size)
{
framebuffer_set();
previousSize = size;
}
}
void Canvas::size_set(vec2 size)
{
this->size = size;
framebuffer_resize_check();
}
mat4 Canvas::transform_get(float zoom, vec2 pan)
{
auto zoomFactor = math::percent_to_unit(zoom);
auto projection = glm::ortho(0.0f, (float)size.x, 0.0f, (float)size.y, -1.0f, 1.0f);
auto view = mat4{1.0f};
view = glm::translate(view, vec3((size * 0.5f) + pan, 0.0f));
view = glm::scale(view, vec3(zoomFactor, zoomFactor, 1.0f));
return projection * view;
}
}
void canvas_clear(vec4& color)
{
glClearColor(color.r, color.g, color.b, color.a);
glClear(GL_COLOR_BUFFER_BIT);
}
void Canvas::axes_render(Shader& shader, float zoom, vec2 pan, vec4 color)
{
auto originNDC = transform_get(zoom, pan) * vec4(0.0f, 0.0f, 0.0f, 1.0f);
originNDC /= originNDC.w;
void canvas_viewport_set(Canvas* self)
{
glViewport(0, 0, (s32)self->size.x, (s32)self->size.y);
}
glUseProgram(shader.id);
void canvas_framebuffer_resize_check(Canvas* self)
{
if (self->previousSize != self->size)
_canvas_framebuffer_set(self, self->size);
}
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), 1, value_ptr(color));
glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_ORIGIN_NDC), originNDC.x, originNDC.y);
void canvas_grid_draw(Canvas* self, GLuint& shader, mat4& transform, ivec2& size, ivec2& offset, vec4& color)
{
mat4 inverseTransform = glm::inverse(transform);
glBindVertexArray(axisVAO);
glUseProgram(shader);
glUniform1i(glGetUniformLocation(shader.id, shader::UNIFORM_AXIS), 0);
glDrawArrays(GL_LINES, 0, 2);
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);
glUniform1i(glGetUniformLocation(shader.id, shader::UNIFORM_AXIS), 1);
glDrawArrays(GL_LINES, 0, 2);
glBindVertexArray(self->gridVAO);
glBindVertexArray(0);
glUseProgram(0);
}
void Canvas::grid_render(Shader& shader, float zoom, vec2 pan, ivec2 size, ivec2 offset, vec4 color)
{
auto transform = glm::inverse(transform_get(zoom, pan));
glUseProgram(shader.id);
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_SIZE), (float)size.x, (float)size.y);
glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_OFFSET), (float)offset.x, (float)offset.y);
glUniform4f(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), color.r, color.g, color.b, color.a);
glBindVertexArray(gridVAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glUseProgram(0);
}
}
void canvas_texture_draw(Canvas* self, GLuint& shader, GLuint& texture, mat4& transform, const f32* vertices, vec4 tint, vec3 colorOffset)
{
glUseProgram(shader);
void Canvas::texture_render(Shader& shader, GLuint& texture, mat4& transform, vec4 tint, vec3 colorOffset,
float* vertices)
{
glUseProgram(shader.id);
glBindVertexArray(self->textureVAO);
glUniform1i(glGetUniformLocation(shader.id, shader::UNIFORM_TEXTURE), 0);
glUniform3fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR_OFFSET), 1, value_ptr(colorOffset));
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TINT), 1, value_ptr(tint));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
glBindBuffer(GL_ARRAY_BUFFER, self->textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW);
glBindVertexArray(textureVAO);
glBindBuffer(GL_ARRAY_BUFFER, textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(shader, SHADER_UNIFORM_TEXTURE), 0);
glUniform3fv(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR_OFFSET), 1, value_ptr(colorOffset));
glUniform4fv(glGetUniformLocation(shader, SHADER_UNIFORM_TINT), 1, value_ptr(tint));
glUniformMatrix4fv(glGetUniformLocation(shader, SHADER_UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
}
}
void canvas_rect_draw(Canvas* self, const GLuint& shader, const mat4& transform, const vec4& color)
{
glUseProgram(shader);
void Canvas::rect_render(Shader& shader, const mat4& transform, const mat4& model, vec4 color, float dashLength,
float dashGap, float dashOffset)
{
glUseProgram(shader.id);
glBindVertexArray(self->rectVAO);
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_MODEL); location != -1)
glUniformMatrix4fv(location, 1, GL_FALSE, value_ptr(model));
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), 1, value_ptr(color));
glUniformMatrix4fv(glGetUniformLocation(shader, SHADER_UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
glUniform4fv(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), 1, value_ptr(color));
auto origin = model * vec4(0.0f, 0.0f, 0.0f, 1.0f);
auto edgeX = model * vec4(1.0f, 0.0f, 0.0f, 1.0f);
auto edgeY = model * vec4(0.0f, 1.0f, 0.0f, 1.0f);
auto axisX = vec2(edgeX - origin);
auto axisY = vec2(edgeY - origin);
if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_AXIS_X); location != -1)
glUniform2fv(location, 1, value_ptr(axisX));
if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_AXIS_Y); location != -1)
glUniform2fv(location, 1, value_ptr(axisY));
if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_DASH_LENGTH); location != -1)
glUniform1f(location, dashLength);
if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_DASH_GAP); location != -1)
glUniform1f(location, dashGap);
if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_DASH_OFFSET); location != -1)
glUniform1f(location, dashOffset);
glBindVertexArray(rectVAO);
glDrawArrays(GL_LINE_LOOP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
}
}
void Canvas::viewport_set()
{
glViewport(0, 0, size.x, size.y);
}
void canvas_axes_draw(Canvas* self, GLuint& shader, mat4& transform, vec4& color)
{
vec4 originNDC = transform * vec4(0.0f, 0.0f, 0.0f, 1.0f);
originNDC /= originNDC.w;
void Canvas::clear(vec4& color)
{
glClearColor(color.r, color.g, color.b, color.a);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
glUseProgram(shader);
glBindVertexArray(self->axisVAO);
glUniform4fv(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), 1, value_ptr(color));
glUniform2f(glGetUniformLocation(shader, SHADER_UNIFORM_ORIGIN_NDC), originNDC.x, originNDC.y);
glUniform1i(glGetUniformLocation(shader, SHADER_UNIFORM_AXIS), 0);
glDrawArrays(GL_LINES, 0, 2);
glUniform1i(glGetUniformLocation(shader, SHADER_UNIFORM_AXIS), 1);
glDrawArrays(GL_LINES, 0, 2);
glBindVertexArray(0);
glUseProgram(0);
}
void Canvas::bind()
{
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
}
void canvas_bind(Canvas* self)
{
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
}
void canvas_unbind(void)
{
void Canvas::unbind()
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
void canvas_free(Canvas* self)
{
if (!self->isInit) return;
std::vector<unsigned char> Canvas::pixels_get()
{
auto count = size.x * size.y * texture::CHANNELS;
std::vector<unsigned char> pixels(count);
glDeleteFramebuffers(1, &self->fbo);
glDeleteRenderbuffers(1, &self->rbo);
glDeleteTextures(1, &self->framebuffer);
glDeleteVertexArrays(1, &self->axisVAO);
glDeleteVertexArrays(1, &self->rectVAO);
glDeleteVertexArrays(1, &self->gridVAO);
glDeleteVertexArrays(1, &self->textureVAO);
glDeleteBuffers(1, &self->axisVBO);
glDeleteBuffers(1, &self->rectVBO);
glDeleteBuffers(1, &self->gridVBO);
glDeleteBuffers(1, &self->textureVBO);
glDeleteBuffers(1, &self->textureEBO);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glReadBuffer(GL_COLOR_ATTACHMENT0);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glReadPixels(0, 0, size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
return pixels;
}
void Canvas::zoom_set(float& zoom, vec2& pan, vec2 focus, float step)
{
auto zoomFactor = math::percent_to_unit(zoom);
float newZoom = glm::clamp(math::round_nearest_multiple(zoom + step, step), ZOOM_MIN, ZOOM_MAX);
if (newZoom != zoom)
{
float newZoomFactor = math::percent_to_unit(newZoom);
pan += focus * (zoomFactor - newZoomFactor);
zoom = newZoom;
}
}
vec4 Canvas::pixel_read(vec2 position, vec2 framebufferSize)
{
uint8_t rgba[4]{};
glBindTexture(GL_READ_FRAMEBUFFER, fbo);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(position.x, framebufferSize.y - 1 - position.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
return vec4(math::uint8_to_float(rgba[0]), math::uint8_to_float(rgba[1]), math::uint8_to_float(rgba[2]),
math::uint8_to_float(rgba[3]));
}
vec2 Canvas::position_translate(float& zoom, vec2& pan, vec2 position)
{
auto zoomFactor = math::percent_to_unit(zoom);
return (position - pan - (size * 0.5f)) / zoomFactor;
}
void Canvas::set_to_rect(float& zoom, vec2& pan, vec4 rect)
{
if (rect != vec4(-1.0f) && (rect.z > 0 && rect.w > 0))
{
f32 scaleX = size.x / rect.z;
f32 scaleY = size.y / rect.w;
f32 fitScale = std::min(scaleX, scaleY);
zoom = math::unit_to_percent(fitScale);
vec2 rectCenter = {rect.x + rect.z * 0.5f, rect.y + rect.w * 0.5f};
pan = -rectCenter * fitScale;
}
}
}

View File

@@ -1,45 +1,37 @@
#pragma once
#include "resources.h"
#include <glad/glad.h>
#include <glm/glm.hpp>
#define CANVAS_ZOOM_MIN 1.0f
#define CANVAS_ZOOM_MAX 2000.0f
#define CANVAS_ZOOM_DEFAULT 100.0f
#define CANVAS_ZOOM_STEP 100.0f
#define CANVAS_GRID_MIN 1
#define CANVAS_GRID_MAX 1000
#define CANVAS_GRID_DEFAULT 32
#include "shader.h"
const inline vec2 CANVAS_PIVOT_SIZE = {4, 4};
const inline vec2 CANVAS_SCALE_DEFAULT = {1.0f, 1.0f};
const inline f32 CANVAS_AXIS_VERTICES[] = {-1.0f, 1.0f};
const inline f32 CANVAS_GRID_VERTICES[] =
namespace anm2ed::canvas
{
-1.0f, -1.0f,
3.0f, -1.0f,
-1.0f, 3.0f
};
constexpr float TEXTURE_VERTICES[] = {0, 0, 0.0f, 0.0f, 1, 0, 1.0f, 0.0f, 1, 1, 1.0f, 1.0f, 0, 1, 0.0f, 1.0f};
const inline f32 CANVAS_RECT_VERTICES[] =
{
0, 0,
1, 0,
1, 1,
0, 1
};
constexpr auto PIVOT_SIZE = glm::vec2(8, 8);
constexpr auto ZOOM_MIN = 1.0f;
constexpr auto ZOOM_MAX = 2000.0f;
constexpr auto POSITION_FORMAT = "Position: ({:8}, {:8})";
const inline f32 CANVAS_TEXTURE_VERTICES[] =
{
0, 0, 0.0f, 0.0f,
1, 0, 1.0f, 0.0f,
1, 1, 1.0f, 1.0f,
0, 1, 0.0f, 1.0f
};
constexpr auto DASH_LENGTH = 4.0f;
constexpr auto DASH_GAP = 1.0f;
constexpr auto DASH_OFFSET = 1.0f;
struct Canvas
constexpr auto STEP = 1.0f;
constexpr auto STEP_FAST = 5.0f;
constexpr auto GRID_SIZE_MIN = 1;
constexpr auto GRID_SIZE_MAX = 10000;
constexpr auto GRID_OFFSET_MIN = 0;
constexpr auto GRID_OFFSET_MAX = 10000;
}
namespace anm2ed
{
class Canvas
{
public:
GLuint fbo{};
GLuint rbo{};
GLuint axisVAO{};
@@ -48,48 +40,37 @@ struct Canvas
GLuint rectVBO{};
GLuint gridVAO{};
GLuint gridVBO{};
GLuint framebuffer{};
GLuint textureVAO{};
GLuint textureVBO{};
GLuint textureEBO{};
ivec2 size{};
ivec2 previousSize{};
bool isInit = false;
};
GLuint texture{};
glm::vec2 previousSize{};
glm::vec2 size{};
#define UV_VERTICES(uvMin, uvMax) \
{ \
0, 0, uvMin.x, uvMin.y, \
1, 0, uvMax.x, uvMin.y, \
1, 1, uvMax.x, uvMax.y, \
0, 1, uvMin.x, uvMax.y \
Canvas();
Canvas(glm::vec2);
~Canvas();
bool is_valid();
void framebuffer_set();
void framebuffer_resize_check();
void size_set(glm::vec2);
glm::vec4 pixel_read(glm::vec2, glm::vec2);
glm::mat4 transform_get(float = 100.0f, glm::vec2 = {});
void axes_render(resource::Shader&, float, glm::vec2, glm::vec4 = glm::vec4(1.0f));
void grid_render(resource::Shader&, float, glm::vec2, glm::ivec2 = glm::ivec2(32, 32), glm::ivec2 = {},
glm::vec4 = glm::vec4(1.0f));
void texture_render(resource::Shader&, GLuint&, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {},
float* = (float*)canvas::TEXTURE_VERTICES);
void rect_render(resource::Shader&, const glm::mat4&, const glm::mat4&, glm::vec4 = glm::vec4(1.0f),
float dashLength = canvas::DASH_LENGTH, float dashGap = canvas::DASH_GAP,
float dashOffset = canvas::DASH_OFFSET);
void viewport_set();
void clear(glm::vec4&);
void bind();
void unbind();
void zoom_set(float&, glm::vec2&, glm::vec2, float);
glm::vec2 position_translate(float&, glm::vec2&, glm::vec2);
void set_to_rect(float& zoom, glm::vec2& pan, glm::vec4 rect);
std::vector<unsigned char> pixels_get();
};
}
#define ATLAS_UV_MIN(type) (ATLAS_POSITION(type) / TEXTURE_ATLAS_SIZE)
#define ATLAS_UV_MAX(type) ((ATLAS_POSITION(type) + ATLAS_SIZE(type)) / TEXTURE_ATLAS_SIZE)
#define ATLAS_UV_ARGS(type) ATLAS_UV_MIN(type), ATLAS_UV_MAX(type)
#define ATLAS_UV_VERTICES(type) UV_VERTICES(ATLAS_UV_MIN(type), ATLAS_UV_MAX(type))
mat4 canvas_transform_get(Canvas* self, vec2 pan, f32 zoom, OriginType origin);
void canvas_axes_draw(Canvas* self, GLuint& shader, mat4& transform, vec4& color);
void canvas_bind(Canvas* self);
void canvas_clear(vec4& color);
void canvas_draw(Canvas* self);
void canvas_free(Canvas* self);
void canvas_grid_draw(Canvas* self, GLuint& shader, mat4& transform, ivec2& size, ivec2& offset, vec4& color);
void canvas_init(Canvas* self, const ivec2& size);
void canvas_rect_draw(Canvas* self, const GLuint& shader, const mat4& transform, const vec4& color);
void canvas_framebuffer_resize_check(Canvas* self);
void canvas_unbind(void);
void canvas_viewport_set(Canvas* self);
void canvas_texture_draw
(
Canvas* self,
GLuint& shader,
GLuint& texture,
mat4& transform,
const f32* vertices = CANVAS_TEXTURE_VERTICES,
vec4 tint = COLOR_OPAQUE,
vec3 colorOffset = COLOR_OFFSET_NONE
);

View File

@@ -1,113 +1,25 @@
#include "clipboard.h"
void clipboard_copy(Clipboard* self)
#include <SDL3/SDL.h>
namespace anm2ed
{
std::string clipboardText{};
std::string Clipboard::get()
{
auto text = SDL_GetClipboardText();
auto string = std::string(text);
SDL_free(text);
auto clipboard_text_set = [&]()
{
if (!SDL_SetClipboardText(clipboardText.c_str()))
log_warning(std::format(CLIPBOARD_TEXT_SET_WARNING, SDL_GetError()));
};
return string;
}
switch (self->type)
bool Clipboard::is_empty()
{
case CLIPBOARD_FRAME:
{
Anm2Reference* reference = std::get_if<Anm2Reference>(&self->location);
if (!reference) break;
Anm2Frame* frame = anm2_frame_from_reference(self->anm2, reference);
if (!frame) break;
anm2_frame_serialize_to_string(frame, reference->itemType, &clipboardText);
clipboard_text_set();
break;
return get().empty();
}
case CLIPBOARD_ANIMATION:
void Clipboard::set(const std::string& string)
{
s32* id = std::get_if<s32>(&self->location);
if (!id) break;
Anm2Animation* animation = map_find(self->anm2->animations, *id);
if (!animation) break;
anm2_animation_serialize_to_string(animation, &clipboardText);
clipboard_text_set();
break;
}
break;
default:
break;
SDL_SetClipboardText(string.data());
}
}
void clipboard_cut(Clipboard* self)
{
clipboard_copy(self);
switch (self->type)
{
case CLIPBOARD_FRAME:
{
Anm2Reference* reference = std::get_if<Anm2Reference>(&self->location);
if (!reference) break;
anm2_frame_remove(self->anm2, reference);
break;
}
case CLIPBOARD_ANIMATION:
{
s32* id = std::get_if<s32>(&self->location);
if (!id) break;
anm2_animation_remove(self->anm2, *id);
break;
}
default:
break;
}
}
bool clipboard_paste(Clipboard* self)
{
auto clipboard_string = [&]()
{
char* clipboardText = SDL_GetClipboardText();
std::string clipboardString = std::string(clipboardText);
SDL_free(clipboardText);
return clipboardString;
};
switch (self->type)
{
case CLIPBOARD_FRAME:
{
Anm2Reference* reference = std::get_if<Anm2Reference>(&self->location);
if (!reference) break;
Anm2Frame frame;
if (anm2_frame_deserialize_from_xml(&frame, clipboard_string()))
anm2_frame_add(self->anm2, &frame, reference);
else return false;
break;
}
case CLIPBOARD_ANIMATION:
{
s32* id = std::get_if<s32>(&self->location);
if (!id) break;
Anm2Animation animation;
if (anm2_animation_deserialize_from_xml(&animation, clipboard_string()))
anm2_animation_add(self->anm2, &animation, *id);
else return false;
break;
}
default:
break;
}
return true;
}
void clipboard_init(Clipboard* self, Anm2* anm2)
{
self->anm2 = anm2;
}
bool clipboard_is_value(void)
{
return SDL_HasClipboardText();
}

View File

@@ -1,27 +1,14 @@
#pragma once
#include "anm2.h"
#include <string>
#define CLIPBOARD_TEXT_SET_WARNING "Unable to set clipboard text! ({})"
enum ClipboardType
namespace anm2ed
{
CLIPBOARD_NONE,
CLIPBOARD_FRAME,
CLIPBOARD_ANIMATION
};
using ClipboardLocation = std::variant<std::monostate, Anm2Reference, s32>;
struct Clipboard
{
Anm2* anm2 = nullptr;
ClipboardType type;
ClipboardLocation location;
};
bool clipboard_is_value(void);
void clipboard_copy(Clipboard* self);
void clipboard_cut(Clipboard* self);
bool clipboard_paste(Clipboard* self);
void clipboard_init(Clipboard* self, Anm2* anm2);
class Clipboard
{
public:
bool is_empty();
std::string get();
void set(const std::string&);
};
}

View File

@@ -1,91 +1,87 @@
#include "dialog.h"
#ifdef _WIN32
#include <windows.h>
#include <window.h>
#elif __unix__
#else
#include "toast.h"
#endif
static void _dialog_callback(void* userdata, const char* const* filelist, s32 filter)
{
Dialog* self;
#include <format>
self = (Dialog*)userdata;
namespace anm2ed::dialog
{
void callback(void* userData, const char* const* filelist, int filter)
{
auto self = (Dialog*)(userData);
if (filelist && filelist[0] && strlen(filelist[0]) > 0)
{
self->path = filelist[0];
self->isSelected = true;
self->selectedFilter = filter;
}
else
{
self->isSelected = false;
self->selectedFilter = INDEX_NONE;
self->selectedFilter = -1;
}
}
void dialog_init(Dialog* self, SDL_Window* window)
{
self->window = window;
}
using namespace anm2ed::dialog;
void dialog_anm2_open(Dialog* self)
namespace anm2ed
{
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, std::size(DIALOG_FILE_FILTER_ANM2), nullptr, false);
self->type = DIALOG_ANM2_OPEN;
}
void dialog_anm2_save(Dialog* self)
{
SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, std::size(DIALOG_FILE_FILTER_ANM2), nullptr);
self->type = DIALOG_ANM2_SAVE;
}
Dialog::Dialog(SDL_Window* window)
{
*this = Dialog();
this->window = window;
}
void dialog_spritesheet_add(Dialog* self)
{
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, std::size(DIALOG_FILE_FILTER_PNG), nullptr, false);
self->type = DIALOG_SPRITESHEET_ADD;
}
void Dialog::file_open(dialog::Type type)
{
SDL_ShowOpenFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]),
nullptr, false);
this->type = type;
}
void dialog_spritesheet_replace(Dialog* self, s32 id)
{
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, std::size(DIALOG_FILE_FILTER_PNG), nullptr, false);
self->replaceID = id;
self->type = DIALOG_SPRITESHEET_REPLACE;
}
void Dialog::file_save(dialog::Type type)
{
SDL_ShowSaveFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]),
nullptr);
this->type = type;
}
void dialog_render_path_set(Dialog* self, RenderType type)
{
SDL_DialogFileFilter filter = DIALOG_RENDER_FILE_FILTERS[type];
void Dialog::folder_open(dialog::Type type)
{
SDL_ShowOpenFolderDialog(callback, this, window, nullptr, false);
this->type = type;
}
if (type == RENDER_PNG)
SDL_ShowOpenFolderDialog(_dialog_callback, self, self->window, nullptr, false);
else
SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, &filter, 1, nullptr);
self->type = DIALOG_RENDER_PATH_SET;
}
void dialog_ffmpeg_path_set(Dialog* self)
{
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_FFMPEG, std::size(DIALOG_FILE_FILTER_FFMPEG), nullptr, false);
self->type = DIALOG_FFMPEG_PATH_SET;
}
void dialog_explorer_open(const std::string& path)
{
void Dialog::file_explorer_open(const std::string& path)
{
#ifdef _WIN32
ShellExecuteA(NULL, DIALOG_FILE_EXPLORER_COMMAND, path.c_str(), NULL, NULL, SW_SHOWNORMAL);
ShellExecuteA(NULL, "open", path.c_str(), NULL, NULL, SW_SHOWNORMAL);
#elif __unix__
system(std::format("xdg-open \"{}\" &", path).c_str());
#else
char command[DIALOG_FILE_EXPLORER_COMMAND_SIZE];
snprintf(command, sizeof(command), DIALOG_FILE_EXPLORER_COMMAND, path.c_str());
system(command);
toasts.info("Operation not supported.");
#endif
}
}
void
dialog_reset(Dialog* self)
{
self->replaceID = ID_NONE;
self->type = DIALOG_NONE;
self->path.clear();
self->isSelected = false;
}
void Dialog::reset()
{
*this = Dialog(this->window);
}
bool Dialog::is_selected(dialog::Type type)
{
return this->type == type && !path.empty();
}
void Dialog::set_string_to_selected_path(std::string& string, dialog::Type type)
{
if (type == NONE) return;
if (!is_selected(type)) return;
string = path;
reset();
}
};

View File

@@ -1,71 +1,91 @@
#pragma once
#include "render.h"
#include "window.h"
#include <string>
#define DIALOG_FILE_EXPLORER_COMMAND_SIZE 512
#include <SDL3/SDL.h>
#ifdef _WIN32
#define DIALOG_FILE_EXPLORER_COMMAND "open"
namespace anm2ed::dialog
{
#if defined(_WIN32)
#define EXECUTABLE_FILTER {"Executable", "exe"}
#else
#define DIALOG_FILE_EXPLORER_COMMAND "xdg-open \"%s\" &"
#define EXECUTABLE_FILTER {"Executable", "*"}
#endif
const SDL_DialogFileFilter DIALOG_FILE_FILTER_ANM2[] =
{
{"Anm2 file", "anm2;xml"}
};
#define FILTER_LIST \
X(NO_FILTER, {}) \
X(ANM2, {"Anm2 file", "anm2;xml"}) \
X(PNG, {"PNG image", "png"}) \
X(SOUND, {"WAV file;OGG file", "wav;ogg"}) \
X(GIF, {"GIF image", "gif"}) \
X(WEBM, {"WebM video", "webm"}) \
X(MP4, {"MP4 video", "MP4"}) \
X(EXECUTABLE, EXECUTABLE_FILTER)
const SDL_DialogFileFilter DIALOG_FILE_FILTER_PNG[] =
{
{"PNG image", "png"}
};
enum Filter
{
#define X(symbol, ...) symbol,
FILTER_LIST
#undef X
};
const SDL_DialogFileFilter DIALOG_RENDER_FILE_FILTERS[] =
{
{"PNG image", "png"},
{"GIF image", "gif"},
{"WebM video", "webm"},
{"MP4 video", "mp4"}
};
constexpr SDL_DialogFileFilter FILTERS[][1] = {
#define X(symbol, ...) {__VA_ARGS__},
FILTER_LIST
#undef X
};
const SDL_DialogFileFilter DIALOG_FILE_FILTER_FFMPEG[] =
{
#ifdef _WIN32
{"Executable", "exe"}
#else
{"Executable", ""}
#endif
};
#undef FILTER_LIST
enum DialogType
{
DIALOG_NONE,
DIALOG_ANM2_OPEN,
DIALOG_ANM2_SAVE,
DIALOG_SPRITESHEET_ADD,
DIALOG_SPRITESHEET_REPLACE,
DIALOG_RENDER_PATH_SET,
DIALOG_FFMPEG_PATH_SET
};
#define DIALOG_LIST \
X(NONE, NO_FILTER) \
X(ANM2_NEW, ANM2) \
X(ANM2_OPEN, ANM2) \
X(ANM2_SAVE, ANM2) \
X(SOUND_OPEN, SOUND) \
X(SPRITESHEET_OPEN, PNG) \
X(SPRITESHEET_REPLACE, PNG) \
X(FFMPEG_PATH_SET, EXECUTABLE) \
X(PNG_DIRECTORY_SET, NO_FILTER) \
X(GIF_PATH_SET, GIF) \
X(WEBM_PATH_SET, WEBM) \
X(MP4_PATH_SET, MP4)
struct Dialog
enum Type
{
#define X(symbol, filter) symbol,
DIALOG_LIST
#undef X
};
constexpr Filter TYPE_FILTERS[] = {
#define X(symbol, filter) filter,
DIALOG_LIST
#undef X
};
#undef DIALOG_LIST
}
namespace anm2ed
{
SDL_Window* window = nullptr;
s32 selectedFilter = ID_NONE;
class Dialog
{
public:
SDL_Window* window{};
std::string path{};
s32 replaceID = ID_NONE;
DialogType type = DIALOG_NONE;
bool isSelected = false;
};
dialog::Type type{dialog::NONE};
int selectedFilter{-1};
void dialog_init(Dialog* self, SDL_Window* window);
void dialog_anm2_open(Dialog* self);
void dialog_spritesheet_add(Dialog* self);
void dialog_spritesheet_replace(Dialog* self, s32 id);
void dialog_anm2_save(Dialog* self);
void dialog_render_path_set(Dialog* self, RenderType type);
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);
Dialog() = default;
Dialog(SDL_Window*);
void file_open(dialog::Type type);
void file_save(dialog::Type type);
void folder_open(dialog::Type type);
bool is_selected(dialog::Type type);
void reset();
void file_explorer_open(const std::string&);
void set_string_to_selected_path(std::string& set, dialog::Type type);
};
}

290
src/document.cpp Normal file
View File

@@ -0,0 +1,290 @@
#include "document.h"
#include <utility>
#include "filesystem_.h"
#include "log.h"
#include "toast.h"
using namespace anm2ed::anm2;
using namespace anm2ed::imgui;
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace glm;
namespace anm2ed
{
Document::Document(const std::string& path, bool isNew, std::string* errorString)
{
if (!filesystem::path_is_exist(path)) return;
if (isNew)
anm2 = anm2::Anm2();
else
{
anm2 = Anm2(path, errorString);
if (errorString && !errorString->empty()) return;
}
this->path = path;
clean();
change(Document::ALL);
}
Document::Document(Document&& other) noexcept
: path(std::move(other.path)), snapshots(std::move(other.snapshots)), current(snapshots.current),
anm2(current.anm2), reference(current.reference), playback(current.playback), animation(current.animation),
merge(current.merge), event(current.event), layer(current.layer), null(current.null), sound(current.sound),
spritesheet(current.spritesheet), message(current.message), previewZoom(other.previewZoom),
previewPan(other.previewPan), editorPan(other.editorPan), editorZoom(other.editorZoom),
overlayIndex(other.overlayIndex), hoveredFrame(other.hoveredFrame), hash(other.hash), saveHash(other.saveHash),
autosaveHash(other.autosaveHash), lastAutosaveTime(other.lastAutosaveTime), isOpen(other.isOpen),
isForceDirty(other.isForceDirty), isAnimationPreviewSet(other.isAnimationPreviewSet),
isSpritesheetEditorSet(other.isSpritesheetEditorSet)
{
}
Document& Document::operator=(Document&& other) noexcept
{
if (this != &other)
{
path = std::move(other.path);
snapshots = std::move(other.snapshots);
previewZoom = other.previewZoom;
previewPan = other.previewPan;
editorPan = other.editorPan;
editorZoom = other.editorZoom;
overlayIndex = other.overlayIndex;
hoveredFrame = other.hoveredFrame;
hash = other.hash;
saveHash = other.saveHash;
autosaveHash = other.autosaveHash;
lastAutosaveTime = other.lastAutosaveTime;
isOpen = other.isOpen;
isForceDirty = other.isForceDirty;
isAnimationPreviewSet = other.isAnimationPreviewSet;
isSpritesheetEditorSet = other.isSpritesheetEditorSet;
}
return *this;
}
bool Document::save(const std::string& path, std::string* errorString)
{
this->path = !path.empty() ? path : this->path.string();
if (anm2.serialize(this->path, errorString))
{
toasts.info(std::format("Saved document to: {}", this->path.string()));
clean();
return true;
}
else if (errorString)
toasts.warning(std::format("Could not save document to: {} ({})", this->path.string(), *errorString));
return false;
}
std::filesystem::path Document::autosave_path_get()
{
return directory_get() / std::string("." + filename_get().string() + ".autosave");
}
std::filesystem::path Document::path_from_autosave_get(std::filesystem::path& path)
{
auto fileName = path.filename().string();
if (!fileName.empty() && fileName.front() == '.') fileName.erase(fileName.begin());
auto restorePath = path.parent_path() / fileName;
restorePath.replace_extension("");
return path;
}
bool Document::autosave(std::string* errorString)
{
auto autosavePath = autosave_path_get();
if (anm2.serialize(autosavePath, errorString))
{
autosaveHash = hash;
lastAutosaveTime = 0.0f;
toasts.info("Autosaving...");
logger.info(std::format("Autosaved document to: {}", autosavePath.string()));
return true;
}
else if (errorString)
toasts.warning(std::format("Could not autosave document to: {} ({})", autosavePath.string(), *errorString));
return false;
}
void Document::hash_set()
{
hash = anm2.hash();
}
void Document::clean()
{
saveHash = anm2.hash();
hash = saveHash;
lastAutosaveTime = 0.0f;
isForceDirty = false;
}
void Document::change(ChangeType type)
{
hash_set();
auto layers_set = [&]() { layer.unused = anm2.layers_unused(); };
auto nulls_set = [&]() { null.unused = anm2.nulls_unused(); };
auto events_set = [&]()
{
event.unused = anm2.events_unused();
event.labels_set(anm2.event_labels_get());
};
auto animations_set = [&]() { animation.labels_set(anm2.animation_labels_get()); };
auto spritesheets_set = [&]()
{
spritesheet.unused = anm2.spritesheets_unused();
spritesheet.labels_set(anm2.spritesheet_labels_get());
};
auto sounds_set = [&]()
{
sound.unused = anm2.sounds_unused();
sound.labels_set(anm2.sound_labels_get());
for (auto& animation : anm2.animations.items)
for (auto& trigger : animation.triggers.frames)
if (!anm2.content.sounds.contains(trigger.soundID)) trigger.soundID = -1;
};
switch (type)
{
case LAYERS:
layers_set();
break;
case NULLS:
nulls_set();
break;
case EVENTS:
events_set();
break;
case SPRITESHEETS:
spritesheets_set();
break;
case SOUNDS:
sounds_set();
break;
case ANIMATIONS:
animations_set();
break;
case ALL:
layers_set();
nulls_set();
events_set();
spritesheets_set();
animations_set();
sounds_set();
break;
default:
break;
}
}
bool Document::is_dirty()
{
return hash != saveHash;
}
bool Document::is_autosave_dirty()
{
return hash != autosaveHash;
}
std::filesystem::path Document::directory_get()
{
return path.parent_path();
}
std::filesystem::path Document::filename_get()
{
return path.filename();
}
bool Document::is_valid()
{
return !path.empty();
}
anm2::Frame* Document::frame_get()
{
return anm2.frame_get(reference);
}
anm2::Item* Document::item_get()
{
return anm2.item_get(reference);
}
anm2::Animation* Document::animation_get()
{
return anm2.animation_get(reference);
}
anm2::Spritesheet* Document::spritesheet_get()
{
return anm2.spritesheet_get(spritesheet.reference);
}
void Document::spritesheet_add(const std::string& path)
{
auto add = [&]()
{
int id{};
if (anm2.spritesheet_add(directory_get(), path, id))
{
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
this->spritesheet.selection = {id};
toasts.info(std::format("Initialized spritesheet #{}: {}", id, spritesheet.path.string()));
}
else
toasts.error(std::format("Failed to initialize spritesheet: {}", path));
};
DOCUMENT_EDIT_PTR(this, "Add Spritesheet", Document::SPRITESHEETS, add());
}
void Document::snapshot(const std::string& message)
{
this->message = message;
snapshots.push(current);
}
void Document::undo()
{
snapshots.undo();
toasts.info(std::format("Undo: {}", message));
change(Document::ALL);
}
void Document::redo()
{
snapshots.redo();
toasts.info(std::format("Redo: {}", message));
change(Document::ALL);
}
bool Document::is_able_to_undo()
{
return !snapshots.undoStack.is_empty();
}
bool Document::is_able_to_redo()
{
return !snapshots.redoStack.is_empty();
}
}

111
src/document.h Normal file
View File

@@ -0,0 +1,111 @@
#pragma once
#include <filesystem>
#include "snapshots.h"
#include <glm/glm.hpp>
namespace anm2ed
{
class Document
{
public:
enum ChangeType
{
LAYERS,
NULLS,
SPRITESHEETS,
EVENTS,
ANIMATIONS,
ITEMS,
FRAMES,
SOUNDS,
ALL,
COUNT
};
std::filesystem::path path{};
Snapshots snapshots{};
Snapshot& current = snapshots.current;
anm2::Anm2& anm2 = current.anm2;
anm2::Reference& reference = current.reference;
Playback& playback = current.playback;
Storage& animation = current.animation;
Storage& merge = current.merge;
Storage& event = current.event;
Storage& layer = current.layer;
Storage& null = current.null;
Storage& sound = current.sound;
Storage& spritesheet = current.spritesheet;
std::string& message = current.message;
float previewZoom{200};
glm::vec2 previewPan{};
glm::vec2 editorPan{};
float editorZoom{200};
int overlayIndex{-1};
anm2::Reference hoveredFrame{};
uint64_t hash{};
uint64_t saveHash{};
uint64_t autosaveHash{};
double lastAutosaveTime{};
bool isOpen{true};
bool isForceDirty{false};
bool isAnimationPreviewSet{false};
bool isSpritesheetEditorSet{false};
Document(const std::string&, bool = false, std::string* = nullptr);
Document(const Document&) = delete;
Document& operator=(const Document&) = delete;
Document(Document&&) noexcept;
Document& operator=(Document&&) noexcept;
bool save(const std::string& = {}, std::string* = nullptr);
void hash_set();
void clean();
void on_change();
void change(ChangeType);
bool is_dirty();
bool is_autosave_dirty();
std::filesystem::path directory_get();
std::filesystem::path filename_get();
bool is_valid();
anm2::Frame* frame_get();
anm2::Item* item_get();
anm2::Spritesheet* spritesheet_get();
anm2::Animation* animation_get();
void spritesheet_add(const std::string&);
bool autosave(std::string* = nullptr);
std::filesystem::path autosave_path_get();
std::filesystem::path path_from_autosave_get(std::filesystem::path&);
void snapshot(const std::string& message);
void undo();
void redo();
bool is_able_to_undo();
bool is_able_to_redo();
};
#define DOCUMENT_EDIT(document, message, changeType, body) \
{ \
document.snapshot(message); \
body; \
document.change(changeType); \
}
#define DOCUMENT_EDIT_PTR(document, message, changeType, body) \
{ \
document->snapshot(message); \
body; \
document->change(changeType); \
}
}

View File

@@ -1,61 +0,0 @@
#include "editor.h"
void editor_init(Editor* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings)
{
self->anm2 = anm2;
self->reference = reference;
self->resources = resources;
self->settings = settings;
canvas_init(&self->canvas, vec2());
}
void editor_draw(Editor* self)
{
ivec2& gridSize = self->settings->editorGridSize;
ivec2& gridOffset = self->settings->editorGridOffset;
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_framebuffer_resize_check(&self->canvas);
canvas_bind(&self->canvas);
canvas_viewport_set(&self->canvas);
canvas_clear(self->settings->editorBackgroundColor);
if (Anm2Spritesheet* spritesheet = map_find(self->anm2->spritesheets, self->spritesheetID))
{
Texture& texture = spritesheet->texture;
mat4 spritesheetTransform = transform * quad_model_get(texture.size);
canvas_texture_draw(&self->canvas, shaderTexture, texture.id, spritesheetTransform);
if (self->settings->editorIsBorder)
canvas_rect_draw(&self->canvas, shaderLine, spritesheetTransform, EDITOR_BORDER_COLOR);
Anm2Frame* frame = (Anm2Frame*)anm2_frame_from_reference(self->anm2, self->reference);
if (frame)
{
mat4 cropTransform = transform * quad_model_get(frame->size, frame->crop);
canvas_rect_draw(&self->canvas, shaderLine, cropTransform, EDITOR_FRAME_COLOR);
mat4 pivotTransform = transform * quad_model_get(CANVAS_PIVOT_SIZE, frame->crop + frame->pivot, CANVAS_PIVOT_SIZE * 0.5f);
f32 vertices[] = ATLAS_UV_VERTICES(ATLAS_PIVOT);
canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, pivotTransform, vertices, EDITOR_PIVOT_COLOR);
}
}
if (self->settings->editorIsGrid)
canvas_grid_draw(&self->canvas, shaderGrid, transform, gridSize, gridOffset, gridColor);
canvas_unbind();
}
void editor_free(Editor* self)
{
canvas_free(&self->canvas);
}

View File

@@ -1,42 +0,0 @@
#pragma once
#include "anm2.h"
#include "canvas.h"
#include "resources.h"
#include "settings.h"
#define EDITOR_ZOOM_MIN 1.0f
#define EDITOR_ZOOM_MAX 1000.0f
#define EDITOR_ZOOM_STEP 25.0
#define EDITOR_GRID_MIN 1
#define EDITOR_GRID_MAX 1000
#define EDITOR_GRID_OFFSET_MIN 0
#define EDITOR_GRID_OFFSET_MAX 100
static const vec4 EDITOR_BORDER_COLOR = COLOR_OPAQUE;
static const vec4 EDITOR_FRAME_COLOR = COLOR_RED;
static const vec4 EDITOR_PIVOT_COLOR = COLOR_PINK;
struct Editor
{
Anm2* anm2 = nullptr;
Anm2Reference* reference = nullptr;
Resources* resources = nullptr;
Settings* settings = nullptr;
Canvas canvas;
GLuint fbo;
GLuint rbo;
GLuint gridVAO;
GLuint gridVBO;
GLuint texture;
GLuint textureEBO;
GLuint textureVAO;
GLuint textureVBO;
GLuint borderVAO;
GLuint borderVBO;
s32 spritesheetID = ID_NONE;
};
void editor_init(Editor* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings);
void editor_draw(Editor* self);
void editor_free(Editor* self);

View File

@@ -1,69 +0,0 @@
#include "ffmpeg.h"
bool
ffmpeg_render
(
const std::string& ffmpegPath,
const std::string& outputPath,
const std::vector<Texture>& frames,
ivec2 size,
s32 fps,
enum RenderType type
)
{
if (frames.empty() || size.x <= 0 || size.y <= 0 || fps <= 0 || ffmpegPath.empty() || outputPath.empty()) return false;
std::string command{};
switch (type)
{
case RENDER_GIF:
command = std::format(FFMPEG_GIF_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath);
break;
case RENDER_WEBM:
command = std::format(FFMPEG_WEBM_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath);
break;
case RENDER_MP4:
command = std::format(FFMPEG_MP4_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath);
break;
default:
break;
}
#if _WIN32
command = string_quote(command);
#endif
log_command(command);
FILE* fp = POPEN(command.c_str(), PWRITE_MODE);
if (!fp)
{
log_error(std::format(FFMPEG_POPEN_ERROR, strerror(errno)));
return false;
}
size_t frameBytes = size.x * size.y * TEXTURE_CHANNELS;
for (const auto& frame : frames)
{
std::vector<u8> rgba = texture_download(&frame);
if (rgba.size() != frameBytes)
{
PCLOSE(fp);
return false;
}
if (fwrite(rgba.data(), 1, frameBytes, fp) != frameBytes)
{
PCLOSE(fp);
return false;
}
}
const int code = PCLOSE(fp);
return (code == 0);
}

View File

@@ -1,38 +0,0 @@
#pragma once
#include "render.h"
#include "texture.h"
#define FFMPEG_POPEN_ERROR "popen() (for FFmpeg) failed!\n{}"
static constexpr const char* FFMPEG_GIF_FORMAT =
"\"{0}\" -y "
"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 "
"-lavfi \"split[s0][s1];"
"[s0]palettegen=stats_mode=full[p];"
"[s1][p]paletteuse=dither=floyd_steinberg\" "
"-loop 0 \"{4}\"";
static constexpr const char* FFMPEG_WEBM_FORMAT =
"\"{0}\" -y "
"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 "
"-c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 "
"-auto-alt-ref 0 -an \"{4}\"";
static constexpr const char* FFMPEG_MP4_FORMAT =
"\"{0}\" -y "
"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 "
"-vf \"format=yuv420p,scale=trunc(iw/2)*2:trunc(ih/2)*2\" "
"-c:v libx265 -crf 20 -preset slow "
"-tag:v hvc1 -movflags +faststart -an \"{4}\"";
bool
ffmpeg_render
(
const std::string& ffmpegPath,
const std::string& outputPath,
const std::vector<Texture>& frames,
ivec2 size,
s32 fps,
enum RenderType type
);

View File

@@ -1,57 +0,0 @@
#include "generate_preview.h"
void generate_preview_init(GeneratePreview* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings)
{
self->anm2 = anm2;
self->reference = reference;
self->resources = resources;
self->settings = settings;
canvas_init(&self->canvas, GENERATE_PREVIEW_SIZE);
}
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);
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);
Anm2Item* item = anm2_item_from_reference(self->anm2, self->reference);
if (item)
{
if (Anm2Spritesheet* spritesheet = map_find(self->anm2->spritesheets, self->anm2->layers[self->reference->itemID].spritesheetID))
{
Texture& texture = spritesheet->texture;
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);
vec2 textureSize = vec2(texture.size);
vec2 uvMin = (crop + vec2(0.5f)) / textureSize;
vec2 uvMax = (crop + size - vec2(0.5f)) / textureSize;
f32 vertices[] = UV_VERTICES(uvMin, uvMax);
mat4 generateTransform = transform * quad_model_get(size, {}, pivot);
canvas_texture_draw(&self->canvas, shaderTexture, texture.id, generateTransform, vertices, COLOR_OPAQUE, COLOR_OFFSET_NONE);
}
}
canvas_unbind();
}
void generate_preview_free(GeneratePreview* self)
{
canvas_free(&self->canvas);
}

View File

@@ -1,25 +0,0 @@
#pragma once
#include "anm2.h"
#include "resources.h"
#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
{
Anm2* anm2 = nullptr;
Anm2Reference* reference = nullptr;
Resources* resources = nullptr;
Settings* settings = nullptr;
Canvas canvas;
f32 time{};
};
void generate_preview_init(GeneratePreview* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings);
void generate_preview_draw(GeneratePreview* self);
void generate_preview_free(GeneratePreview* self);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

50
src/imgui/dockspace.cpp Normal file
View File

@@ -0,0 +1,50 @@
#include "dockspace.h"
namespace anm2ed::imgui
{
void Dockspace::tick(Manager& manager, Settings& settings)
{
if (auto document = manager.get(); document)
if (settings.windowIsAnimationPreview) animationPreview.tick(manager, *document, settings);
}
void Dockspace::update(Taskbar& taskbar, Documents& documents, Manager& manager, Settings& settings,
Resources& resources, Dialog& dialog, Clipboard& clipboard)
{
auto viewport = ImGui::GetMainViewport();
auto windowHeight = viewport->Size.y - taskbar.height - documents.height;
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height + documents.height));
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, windowHeight));
if (ImGui::Begin("##DockSpace", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus))
{
if (auto document = manager.get(); document)
{
if (ImGui::DockSpace(ImGui::GetID("##DockSpace"), ImVec2(), ImGuiDockNodeFlags_PassthruCentralNode))
{
if (settings.windowIsAnimationPreview) animationPreview.update(manager, settings, resources);
if (settings.windowIsAnimations) animations.update(manager, settings, resources, clipboard);
if (settings.windowIsEvents) events.update(manager, settings, resources, clipboard);
if (settings.windowIsFrameProperties) frameProperties.update(manager, settings);
if (settings.windowIsLayers) layers.update(manager, settings, resources, clipboard);
if (settings.windowIsNulls) nulls.update(manager, settings, resources, clipboard);
if (settings.windowIsOnionskin) onionskin.update(settings);
if (settings.windowIsSounds) sounds.update(manager, settings, resources, dialog, clipboard);
if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources);
if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog, clipboard);
if (settings.windowIsTimeline) timeline.update(manager, settings, resources, clipboard);
if (settings.windowIsTools) tools.update(manager, settings, resources);
}
}
else
welcome.update(manager, resources, dialog, taskbar, documents);
}
ImGui::End();
}
}

41
src/imgui/dockspace.h Normal file
View File

@@ -0,0 +1,41 @@
#pragma once
#include "documents.h"
#include "taskbar.h"
#include "window/animation_preview.h"
#include "window/animations.h"
#include "window/events.h"
#include "window/frame_properties.h"
#include "window/layers.h"
#include "window/nulls.h"
#include "window/onionskin.h"
#include "window/sounds.h"
#include "window/spritesheet_editor.h"
#include "window/spritesheets.h"
#include "window/timeline.h"
#include "window/tools.h"
#include "window/welcome.h"
namespace anm2ed::imgui
{
class Dockspace
{
AnimationPreview animationPreview;
Animations animations;
Events events;
FrameProperties frameProperties;
Layers layers;
Nulls nulls;
Onionskin onionskin;
SpritesheetEditor spritesheetEditor;
Spritesheets spritesheets;
Sounds sounds;
Timeline timeline;
Tools tools;
Welcome welcome;
public:
void tick(Manager&, Settings&);
void update(Taskbar&, Documents&, Manager&, Settings&, Resources&, Dialog&, Clipboard&);
};
}

161
src/imgui/documents.cpp Normal file
View File

@@ -0,0 +1,161 @@
#include "documents.h"
#include <vector>
#include "time_.h"
using namespace anm2ed::resource;
using namespace anm2ed::types;
using namespace anm2ed::util;
namespace anm2ed::imgui
{
void Documents::update(Taskbar& taskbar, Manager& manager, Settings& settings, Resources& resources, bool& isQuitting)
{
auto viewport = ImGui::GetMainViewport();
auto windowHeight = ImGui::GetFrameHeightWithSpacing();
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height));
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, windowHeight));
for (auto& document : manager.documents)
{
auto isDirty = document.is_dirty() && document.is_autosave_dirty();
document.lastAutosaveTime += ImGui::GetIO().DeltaTime;
if (isDirty && document.lastAutosaveTime > settings.fileAutosaveTime * time::SECOND_M) manager.autosave(document);
}
if (ImGui::Begin("##Documents", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse))
{
height = ImGui::GetWindowSize().y;
if (ImGui::BeginTabBar("Documents Bar", ImGuiTabBarFlags_Reorderable))
{
auto documentsCount = (int)manager.documents.size();
bool closeShortcut = imgui::shortcut(settings.shortcutClose, shortcut::GLOBAL) && !closePopup.is_open();
int closeShortcutIndex =
closeShortcut && manager.selected >= 0 && manager.selected < documentsCount ? manager.selected : -1;
std::vector<int> closeIndices{};
closeIndices.reserve(documentsCount);
for (int i = 0; i < documentsCount; ++i)
{
auto& document = manager.documents[i];
auto isDirty = document.is_dirty() || document.isForceDirty;
if (!closePopup.is_open())
{
if (isQuitting)
document.isOpen = false;
else if (i == closeShortcutIndex)
document.isOpen = false;
}
if (!closePopup.is_open() && !document.isOpen)
{
if (isDirty)
{
closePopup.open();
closeDocumentIndex = i;
document.isOpen = true;
}
else
{
closeIndices.push_back(i);
continue;
}
}
auto isRequested = i == manager.pendingSelected;
auto font = isDirty ? font::ITALICS : font::REGULAR;
auto string = isDirty ? std::format("[Not Saved] {}", document.filename_get().string())
: document.filename_get().string();
auto label = std::format("{}###Document{}", string, i);
auto flags = isDirty ? ImGuiTabItemFlags_UnsavedDocument : 0;
if (isRequested) flags |= ImGuiTabItemFlags_SetSelected;
ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
if (ImGui::BeginTabItem(label.c_str(), &document.isOpen, flags))
{
manager.set(i);
if (isRequested) manager.pendingSelected = -1;
ImGui::EndTabItem();
}
ImGui::PopFont();
}
for (auto it = closeIndices.rbegin(); it != closeIndices.rend(); ++it)
{
if (closePopup.is_open() && closeDocumentIndex > *it) --closeDocumentIndex;
manager.close(*it);
}
ImGui::EndTabBar();
}
closePopup.trigger();
if (ImGui::BeginPopupModal(closePopup.label, &closePopup.isOpen, ImGuiWindowFlags_NoResize))
{
if (closeDocumentIndex >= 0 && closeDocumentIndex < (int)manager.documents.size())
{
auto& closeDocument = manager.documents[closeDocumentIndex];
ImGui::TextUnformatted(std::format("The document \"{}\" has been modified.\nDo you want to save it?",
closeDocument.filename_get().string())
.c_str());
auto widgetSize = imgui::widget_size_with_row_get(3);
auto close = [&]()
{
closeDocumentIndex = -1;
closePopup.close();
};
if (ImGui::Button("Yes", widgetSize))
{
manager.save(closeDocumentIndex);
manager.close(closeDocumentIndex);
close();
}
ImGui::SameLine();
if (ImGui::Button("No", widgetSize))
{
manager.close(closeDocumentIndex);
close();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize))
{
isQuitting = false;
close();
}
}
else
{
closeDocumentIndex = -1;
closePopup.close();
}
ImGui::EndPopup();
}
}
ImGui::End();
}
}

20
src/imgui/documents.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "manager.h"
#include "resources.h"
#include "settings.h"
#include "taskbar.h"
namespace anm2ed::imgui
{
class Documents
{
int closeDocumentIndex{-1};
imgui::PopupHelper closePopup{imgui::PopupHelper("Close Document", imgui::POPUP_TO_CONTENT)};
public:
float height{};
void update(Taskbar&, Manager&, Settings&, Resources&, bool&);
};
}

318
src/imgui/imgui_.cpp Normal file
View File

@@ -0,0 +1,318 @@
#include "imgui_.h"
#include <imgui/imgui_internal.h>
#include <set>
#include <sstream>
#include <unordered_map>
using namespace anm2ed::types;
using namespace glm;
namespace anm2ed::imgui
{
int input_text_callback(ImGuiInputTextCallbackData* data)
{
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize)
{
auto* string = (std::string*)(data->UserData);
string->resize(data->BufTextLen);
data->Buf = string->data();
}
return 0;
}
bool input_text_string(const char* label, std::string* string, ImGuiInputTextFlags flags)
{
flags |= ImGuiInputTextFlags_CallbackResize;
return ImGui::InputText(label, string->data(), string->capacity() + 1, flags, input_text_callback, string);
}
bool combo_negative_one_indexed(const std::string& label, int* index, std::vector<const char*>& strings)
{
*index += 1;
bool isActivated = ImGui::Combo(label.c_str(), index, strings.data(), (int)strings.size());
*index -= 1;
return isActivated;
}
bool input_int_range(const char* label, int& value, int min, int max, int step, int stepFast,
ImGuiInputTextFlags flags)
{
auto isActivated = ImGui::InputInt(label, &value, step, stepFast, flags);
value = glm::clamp(value, min, max);
return isActivated;
}
bool input_int2_range(const char* label, ivec2& value, ivec2 min, ivec2 max, ImGuiInputTextFlags flags)
{
auto isActivated = ImGui::InputInt2(label, value_ptr(value), flags);
value = glm::clamp(value, min, max);
return isActivated;
}
bool input_float_range(const char* label, float& value, float min, float max, float step, float stepFast,
const char* format, ImGuiInputTextFlags flags)
{
auto isActivated = ImGui::InputFloat(label, &value, step, stepFast, format, flags);
value = glm::clamp(value, min, max);
return isActivated;
}
bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected,
ImGuiSelectableFlags flags, bool* isRenamed)
{
static std::string editID{};
static bool isJustEdit{};
const bool isEditing = editID == id;
bool isActivated{};
if (isEditing)
{
if (isJustEdit)
{
ImGui::SetKeyboardFocusHere();
isJustEdit = false;
}
ImGui::SetNextItemWidth(-FLT_MIN);
if (input_text_string("##Edit", &text, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll))
{
editID.clear();
isActivated = true;
if (isRenamed) *isRenamed = true;
}
if (ImGui::IsItemDeactivatedAfterEdit() || ImGui::IsKeyPressed(ImGuiKey_Escape)) editID.clear();
}
else
{
if (ImGui::Selectable(label.c_str(), isSelected, flags)) isActivated = true;
if ((ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_F2) && isSelected) ||
(ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)))
{
editID = id;
isJustEdit = true;
}
}
return isActivated;
}
void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut)
{
ImGui::SetItemTooltip("%s\n(Shortcut: %s)", tooltip, shortcut.c_str());
}
void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected)
{
auto* set = (std::set<int>*)self->UserData;
if (isSelected)
set->insert(id);
else
set->erase(id);
};
std::string chord_to_string(ImGuiKeyChord chord)
{
std::string result;
if (chord & ImGuiMod_Ctrl) result += "Ctrl+";
if (chord & ImGuiMod_Shift) result += "Shift+";
if (chord & ImGuiMod_Alt) result += "Alt+";
if (chord & ImGuiMod_Super) result += "Super+";
if (auto key = (ImGuiKey)(chord & ~ImGuiMod_Mask_); key != ImGuiKey_None)
{
if (const char* name = ImGui::GetKeyName(key); name && *name)
result += name;
else
result += "Unknown";
}
if (!result.empty() && result.back() == '+') result.pop_back();
return result;
}
ImGuiKeyChord string_to_chord(const std::string& string)
{
ImGuiKeyChord chord = 0;
ImGuiKey baseKey = ImGuiKey_None;
std::stringstream ss(string);
std::string token;
while (std::getline(ss, token, '+'))
{
token.erase(0, token.find_first_not_of(" \t\r\n"));
token.erase(token.find_last_not_of(" \t\r\n") + 1);
if (token.empty()) continue;
if (auto it = MOD_MAP.find(token); it != MOD_MAP.end())
chord |= it->second;
else if (baseKey == ImGuiKey_None)
if (auto it2 = KEY_MAP.find(token); it2 != KEY_MAP.end()) baseKey = it2->second;
}
if (baseKey != ImGuiKey_None) chord |= baseKey;
return chord;
}
float row_widget_width_get(int count, float width)
{
return (width - (ImGui::GetStyle().ItemSpacing.x * (float)(count - 1))) / (float)count;
}
ImVec2 widget_size_with_row_get(int count, float width) { return ImVec2(row_widget_width_get(count, width), 0); }
float footer_height_get(int itemCount)
{
return ImGui::GetTextLineHeightWithSpacing() * itemCount + ImGui::GetStyle().WindowPadding.y +
ImGui::GetStyle().ItemSpacing.y * (itemCount);
}
ImVec2 footer_size_get(int itemCount)
{
return ImVec2(ImGui::GetContentRegionAvail().x, footer_height_get(itemCount));
}
ImVec2 size_without_footer_get(int rowCount)
{
return ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - footer_height_get(rowCount));
}
ImVec2 child_size_get(int rowCount)
{
return ImVec2(ImGui::GetContentRegionAvail().x,
(ImGui::GetFrameHeightWithSpacing() * rowCount) + (ImGui::GetStyle().WindowPadding.y * 2.0f));
}
ImVec2 icon_size_get()
{
return ImVec2(ImGui::GetTextLineHeightWithSpacing(), ImGui::GetTextLineHeightWithSpacing());
}
bool chord_held(ImGuiKeyChord chord)
{
auto& io = ImGui::GetIO();
for (constexpr ImGuiKey mods[] = {ImGuiMod_Ctrl, ImGuiMod_Shift, ImGuiMod_Alt, ImGuiMod_Super}; ImGuiKey mod : mods)
{
bool required = (chord & mod) != 0;
if (bool held = io.KeyMods & mod; required && !held) return false;
}
auto main_key = (ImGuiKey)(chord & ~ImGuiMod_Mask_);
if (main_key == ImGuiKey_None) return false;
return ImGui::IsKeyDown(main_key);
}
bool chord_repeating(ImGuiKeyChord chord, float delay, float rate)
{
struct State
{
float timeHeld = 0.f;
float nextRepeat = 0.f;
};
static std::unordered_map<ImGuiKeyChord, State> stateMap;
auto& io = ImGui::GetIO();
auto& state = stateMap[chord];
if (chord_held(chord))
{
state.timeHeld += io.DeltaTime;
if (state.timeHeld <= io.DeltaTime)
{
state.nextRepeat = delay;
return true;
}
if (state.timeHeld >= state.nextRepeat)
{
state.nextRepeat += rate;
return true;
}
}
else
{
state.timeHeld = 0.f;
state.nextRepeat = 0.f;
}
return false;
}
bool shortcut(std::string string, shortcut::Type type)
{
if (ImGui::GetTopMostPopupModal() != nullptr) return false;
int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal
: ImGuiInputFlags_RouteFocused;
if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET)
{
ImGui::SetNextItemShortcut(string_to_chord(string), flags);
return false;
}
return ImGui::Shortcut(string_to_chord(string), flags);
}
MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; }
void MultiSelectStorage::start(size_t size, ImGuiMultiSelectFlags flags)
{
internal.UserData = this;
auto io = ImGui::BeginMultiSelect(flags, this->size(), size);
internal.ApplyRequests(io);
}
void MultiSelectStorage::finish()
{
auto io = ImGui::EndMultiSelect();
internal.ApplyRequests(io);
}
PopupHelper::PopupHelper(const char* label, PopupType type, PopupPosition position)
{
this->label = label;
this->type = type;
this->position = position;
}
void PopupHelper::open()
{
isOpen = true;
isTriggered = true;
isJustOpened = true;
}
bool PopupHelper::is_open() { return isOpen; }
void PopupHelper::trigger()
{
if (isTriggered) ImGui::OpenPopup(label);
isTriggered = false;
auto viewport = ImGui::GetMainViewport();
if (position == POPUP_CENTER)
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f)));
else
ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None);
if (POPUP_IS_HEIGHT_SET[type])
ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * POPUP_MULTIPLIERS[type]));
else
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0));
}
void PopupHelper::end() { isJustOpened = false; }
void PopupHelper::close() { isOpen = false; }
}

216
src/imgui/imgui_.h Normal file
View File

@@ -0,0 +1,216 @@
#pragma once
#include <imgui/imgui.h>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
#include "types.h"
namespace anm2ed::imgui
{
constexpr auto DRAG_SPEED = 1.0f;
constexpr auto STEP = 1.0f;
constexpr auto STEP_FAST = 5.0f;
#define POPUP_LIST \
X(POPUP_SMALL, 0.25f, true) \
X(POPUP_NORMAL, 0.5f, true) \
X(POPUP_TO_CONTENT, 0.0f, true) \
X(POPUP_SMALL_NO_HEIGHT, 0.25f, false) \
X(POPUP_NORMAL_NO_HEIGHT, 0.5f, false)
enum PopupType
{
#define X(name, multiplier, isHeightSet) name,
POPUP_LIST
#undef X
};
enum PopupPosition
{
POPUP_CENTER,
POPUP_BY_ITEM
};
constexpr float POPUP_MULTIPLIERS[] = {
#define X(name, multiplier, isHeightSet) multiplier,
POPUP_LIST
#undef X
};
constexpr bool POPUP_IS_HEIGHT_SET[] = {
#define X(name, multiplier, isHeightSet) isHeightSet,
POPUP_LIST
#undef X
};
const std::unordered_map<std::string, ImGuiKey> KEY_MAP = {
{"A", ImGuiKey_A},
{"B", ImGuiKey_B},
{"C", ImGuiKey_C},
{"D", ImGuiKey_D},
{"E", ImGuiKey_E},
{"F", ImGuiKey_F},
{"G", ImGuiKey_G},
{"H", ImGuiKey_H},
{"I", ImGuiKey_I},
{"J", ImGuiKey_J},
{"K", ImGuiKey_K},
{"L", ImGuiKey_L},
{"M", ImGuiKey_M},
{"N", ImGuiKey_N},
{"O", ImGuiKey_O},
{"P", ImGuiKey_P},
{"Q", ImGuiKey_Q},
{"R", ImGuiKey_R},
{"S", ImGuiKey_S},
{"T", ImGuiKey_T},
{"U", ImGuiKey_U},
{"V", ImGuiKey_V},
{"W", ImGuiKey_W},
{"X", ImGuiKey_X},
{"Y", ImGuiKey_Y},
{"Z", ImGuiKey_Z},
{"0", ImGuiKey_0},
{"1", ImGuiKey_1},
{"2", ImGuiKey_2},
{"3", ImGuiKey_3},
{"4", ImGuiKey_4},
{"5", ImGuiKey_5},
{"6", ImGuiKey_6},
{"7", ImGuiKey_7},
{"8", ImGuiKey_8},
{"9", ImGuiKey_9},
{"Num0", ImGuiKey_Keypad0},
{"Num1", ImGuiKey_Keypad1},
{"Num2", ImGuiKey_Keypad2},
{"Num3", ImGuiKey_Keypad3},
{"Num4", ImGuiKey_Keypad4},
{"Num5", ImGuiKey_Keypad5},
{"Num6", ImGuiKey_Keypad6},
{"Num7", ImGuiKey_Keypad7},
{"Num8", ImGuiKey_Keypad8},
{"Num9", ImGuiKey_Keypad9},
{"NumAdd", ImGuiKey_KeypadAdd},
{"NumSubtract", ImGuiKey_KeypadSubtract},
{"NumMultiply", ImGuiKey_KeypadMultiply},
{"NumDivide", ImGuiKey_KeypadDivide},
{"NumEnter", ImGuiKey_KeypadEnter},
{"NumDecimal", ImGuiKey_KeypadDecimal},
{"F1", ImGuiKey_F1},
{"F2", ImGuiKey_F2},
{"F3", ImGuiKey_F3},
{"F4", ImGuiKey_F4},
{"F5", ImGuiKey_F5},
{"F6", ImGuiKey_F6},
{"F7", ImGuiKey_F7},
{"F8", ImGuiKey_F8},
{"F9", ImGuiKey_F9},
{"F10", ImGuiKey_F10},
{"F11", ImGuiKey_F11},
{"F12", ImGuiKey_F12},
{"Up", ImGuiKey_UpArrow},
{"Down", ImGuiKey_DownArrow},
{"Left", ImGuiKey_LeftArrow},
{"Right", ImGuiKey_RightArrow},
{"Space", ImGuiKey_Space},
{"Enter", ImGuiKey_Enter},
{"Escape", ImGuiKey_Escape},
{"Tab", ImGuiKey_Tab},
{"Backspace", ImGuiKey_Backspace},
{"Delete", ImGuiKey_Delete},
{"Insert", ImGuiKey_Insert},
{"Home", ImGuiKey_Home},
{"End", ImGuiKey_End},
{"PageUp", ImGuiKey_PageUp},
{"PageDown", ImGuiKey_PageDown},
{"Minus", ImGuiKey_Minus},
{"Equal", ImGuiKey_Equal},
{"LeftBracket", ImGuiKey_LeftBracket},
{"RightBracket", ImGuiKey_RightBracket},
{"Semicolon", ImGuiKey_Semicolon},
{"Apostrophe", ImGuiKey_Apostrophe},
{"Comma", ImGuiKey_Comma},
{"Period", ImGuiKey_Period},
{"Slash", ImGuiKey_Slash},
{"Backslash", ImGuiKey_Backslash},
{"GraveAccent", ImGuiKey_GraveAccent},
};
const std::unordered_map<std::string, ImGuiKey> MOD_MAP = {
{"Ctrl", ImGuiMod_Ctrl},
{"Shift", ImGuiMod_Shift},
{"Alt", ImGuiMod_Alt},
{"Super", ImGuiMod_Super},
};
std::string chord_to_string(ImGuiKeyChord);
ImGuiKeyChord string_to_chord(const std::string&);
float row_widget_width_get(int, float = ImGui::GetContentRegionAvail().x);
ImVec2 widget_size_with_row_get(int, float = ImGui::GetContentRegionAvail().x);
float footer_height_get(int = 1);
ImVec2 footer_size_get(int = 1);
ImVec2 size_without_footer_get(int = 1);
ImVec2 child_size_get(int = 1);
int input_text_callback(ImGuiInputTextCallbackData*);
bool input_text_string(const char*, std::string*, ImGuiInputTextFlags = 0);
bool input_int_range(const char*, int&, int, int, int = STEP, int = STEP_FAST, ImGuiInputTextFlags = 0);
bool input_int2_range(const char*, glm::ivec2&, glm::ivec2, glm::ivec2, ImGuiInputTextFlags = 0);
bool input_float_range(const char*, float&, float, float, float = STEP, float = STEP_FAST, const char* = "%.3f",
ImGuiInputTextFlags = 0);
bool combo_negative_one_indexed(const std::string&, int*, std::vector<const char*>&);
bool selectable_input_text(const std::string&, const std::string&, std::string&, bool = false,
ImGuiSelectableFlags = 0, bool* = nullptr);
void set_item_tooltip_shortcut(const char*, const std::string& = {});
void external_storage_set(ImGuiSelectionExternalStorage*, int, bool);
ImVec2 icon_size_get();
bool chord_held(ImGuiKeyChord);
bool chord_repeating(ImGuiKeyChord, float = ImGui::GetIO().KeyRepeatDelay, float = ImGui::GetIO().KeyRepeatRate);
bool shortcut(std::string, types::shortcut::Type = types::shortcut::FOCUSED_SET);
class MultiSelectStorage : public std::set<int>
{
public:
ImGuiSelectionExternalStorage internal{};
using std::set<int>::set;
using std::set<int>::operator=;
using std::set<int>::begin;
using std::set<int>::rbegin;
using std::set<int>::end;
using std::set<int>::size;
using std::set<int>::insert;
using std::set<int>::erase;
MultiSelectStorage();
void start(size_t, ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_BoxSelect2d |
ImGuiMultiSelectFlags_ClearOnEscape |
ImGuiMultiSelectFlags_ScopeWindow);
void finish();
};
class PopupHelper
{
public:
const char* label{};
PopupType type{};
PopupPosition position{};
bool isOpen{};
bool isTriggered{};
bool isJustOpened{};
PopupHelper(const char*, PopupType = POPUP_NORMAL, PopupPosition = POPUP_CENTER);
bool is_open();
void open();
void trigger();
void end();
void close();
};
}

870
src/imgui/taskbar.cpp Normal file
View File

@@ -0,0 +1,870 @@
#include "taskbar.h"
#include <array>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <ranges>
#include <imgui/imgui.h>
#include "math_.h"
#include "render.h"
#include "shader.h"
#include "toast.h"
#include "types.h"
#include "icon.h"
using namespace anm2ed::resource;
using namespace anm2ed::types;
using namespace anm2ed::canvas;
using namespace anm2ed::util;
using namespace glm;
namespace anm2ed::imgui
{
#ifdef __unix__
namespace
{
constexpr std::array<int, 7> ICON_SIZES{16, 24, 32, 48, 64, 128, 256};
bool ensure_parent_directory_exists(const std::filesystem::path& path)
{
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec)
{
toasts.warning(std::format("Could not create directory for {} ({})", path.string(), ec.message()));
return false;
}
return true;
}
bool write_binary_blob(const std::filesystem::path& path, const std::uint8_t* data, size_t size)
{
if (!ensure_parent_directory_exists(path)) return false;
std::ofstream file(path, std::ios::binary | std::ios::trunc);
if (!file.is_open())
{
toasts.warning(std::format("Could not open {} for writing", path.string()));
return false;
}
file.write(reinterpret_cast<const char*>(data), static_cast<std::streamsize>(size));
return true;
}
bool run_command_checked(const std::string& command, const std::string& description)
{
auto result = std::system(command.c_str());
if (result != 0)
{
toasts.warning(std::format("{} failed (exit code {})", description, result));
return false;
}
return true;
}
bool install_icon_set(const std::string& context, const std::string& iconName, const std::filesystem::path& path)
{
bool success = true;
for (auto size : ICON_SIZES)
{
auto command = std::format("xdg-icon-resource install --noupdate --novendor --context {} --size {} \"{}\" {}",
context, size, path.string(), iconName);
success &= run_command_checked(command, std::format("Install {} icon ({}px)", iconName, size));
}
return success;
}
bool uninstall_icon_set(const std::string& context, const std::string& iconName)
{
bool success = true;
for (auto size : ICON_SIZES)
{
auto command =
std::format("xdg-icon-resource uninstall --noupdate --context {} --size {} {}", context, size, iconName);
success &= run_command_checked(command, std::format("Remove {} icon ({}px)", iconName, size));
}
return success;
}
bool remove_file_if_exists(const std::filesystem::path& path)
{
std::error_code ec;
if (!std::filesystem::exists(path, ec)) return true;
std::filesystem::remove(path, ec);
if (ec)
{
toasts.warning(std::format("Could not remove {} ({})", path.string(), ec.message()));
return false;
}
return true;
}
}
constexpr auto MIME_TYPE = R"(<?xml version="1.0" encoding="utf-8"?>
<mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="application/x-anm2+xml">
<!--Created automatically by update-mime-database. DO NOT EDIT!-->
<comment>Anm2 Animation</comment>
<glob pattern="*.anm2"/>
</mime-type>
)";
constexpr auto DESKTOP_ENTRY_FORMAT = R"([Desktop Entry]
Type=Application
Name=Anm2Ed
Icon=anm2ed
Comment=Animation editor for .anm2 files
Exec={}
Terminal=false
Categories=Graphics;Development;
MimeType=application/x-anm2+xml;
)";
#endif
Taskbar::Taskbar() : generate(vec2()) {}
void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting)
{
auto document = manager.get();
auto reference = document ? &document->reference : nullptr;
auto animation = document ? document->animation_get() : nullptr;
auto item = document ? document->item_get() : nullptr;
if (ImGui::BeginMainMenuBar())
{
height = ImGui::GetWindowSize().y;
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("New", settings.shortcutNew.c_str())) dialog.file_save(dialog::ANM2_NEW);
if (ImGui::MenuItem("Open", settings.shortcutOpen.c_str())) dialog.file_open(dialog::ANM2_NEW);
if (ImGui::BeginMenu("Open Recent", !manager.recentFiles.empty()))
{
for (auto [i, file] : std::views::enumerate(manager.recentFiles))
{
auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string());
ImGui::PushID(i);
if (ImGui::MenuItem(label.c_str())) manager.open(file);
ImGui::PopID();
}
if (!manager.recentFiles.empty())
if (ImGui::MenuItem("Clear List")) manager.recent_files_clear();
ImGui::EndMenu();
}
if (ImGui::MenuItem("Save", settings.shortcutSave.c_str(), false, document)) manager.save();
if (ImGui::MenuItem("Save As", settings.shortcutSaveAs.c_str(), false, document))
dialog.file_save(dialog::ANM2_SAVE);
if (ImGui::MenuItem("Explore XML Location", nullptr, false, document))
dialog.file_explorer_open(document->directory_get());
ImGui::Separator();
if (ImGui::MenuItem("Exit", settings.shortcutExit.c_str())) isQuitting = true;
ImGui::EndMenu();
}
if (dialog.is_selected(dialog::ANM2_NEW))
{
manager.new_(dialog.path);
dialog.reset();
}
if (dialog.is_selected(dialog::ANM2_OPEN))
{
manager.open(dialog.path);
dialog.reset();
}
if (dialog.is_selected(dialog::ANM2_SAVE))
{
manager.save(dialog.path);
dialog.reset();
}
if (ImGui::BeginMenu("Wizard"))
{
ImGui::BeginDisabled(!item || document->reference.itemType != anm2::LAYER);
if (ImGui::MenuItem("Generate Animation From Grid")) generatePopup.open();
if (ImGui::MenuItem("Change All Frame Properties")) changePopup.open();
ImGui::EndDisabled();
ImGui::Separator();
if (ImGui::MenuItem("Render Animation", nullptr, false, animation)) renderPopup.open();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Playback"))
{
ImGui::MenuItem("Always Loop", nullptr, &settings.playbackIsLoop);
ImGui::SetItemTooltip("%s", "Animations will always loop during playback, even if looping isn't set.");
ImGui::MenuItem("Clamp Playhead", nullptr, &settings.playbackIsClampPlayhead);
ImGui::SetItemTooltip("%s", "The playhead will always clamp to the animation's length.");
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Window"))
{
for (auto [i, member] : std::views::enumerate(WINDOW_MEMBERS))
ImGui::MenuItem(WINDOW_STRINGS[i], nullptr, &(settings.*member));
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Settings"))
{
if (ImGui::MenuItem("Configure"))
{
editSettings = settings;
configurePopup.open();
}
ImGui::Separator();
if (ImGui::MenuItem("Associate .anm2 Files with Editor", nullptr, false,
!isAnm2Association || !isAbleToAssociateAnm2))
{
#ifdef _WIN32
#elif __unix__
auto cache_icons = []()
{
auto programIconPath = std::filesystem::path(filesystem::path_icon_get());
auto fileIconPath = std::filesystem::path(filesystem::path_icon_file_get());
auto iconBytes = std::size(resource::icon::PROGRAM);
bool isSuccess = write_binary_blob(programIconPath, resource::icon::PROGRAM, iconBytes) &&
write_binary_blob(fileIconPath, resource::icon::PROGRAM, iconBytes);
if (isSuccess)
{
isSuccess = install_icon_set("apps", "anm2ed", programIconPath) &&
install_icon_set("mimetypes", "application-x-anm2+xml", fileIconPath) &&
run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache");
}
remove_file_if_exists(programIconPath);
remove_file_if_exists(fileIconPath);
if (isSuccess) toasts.info("Cached program and file icons.");
return isSuccess;
};
auto register_mime = []()
{
auto path = std::filesystem::path(filesystem::path_mime_get());
if (!ensure_parent_directory_exists(path)) return false;
std::ofstream file(path, std::ofstream::out | std::ofstream::trunc);
if (!file.is_open())
{
toasts.warning(std::format("Could not write .anm2 MIME type: {}", path.string()));
return false;
}
file << MIME_TYPE;
file.close();
toasts.info(std::format("Wrote .anm2 MIME type to: {}", path.string()));
auto mimeRoot = path.parent_path().parent_path();
auto command = std::format("update-mime-database \"{}\"", mimeRoot.string());
return run_command_checked(command, "Update MIME database");
};
auto register_desktop_entry = []()
{
auto path = std::filesystem::path(filesystem::path_application_get());
if (!ensure_parent_directory_exists(path)) return false;
std::ofstream file(path, std::ofstream::out | std::ofstream::trunc);
if (!file.is_open())
{
toasts.warning(std::format("Could not write desktop entry: {}", path.string()));
return false;
}
auto desktopEntry = std::format(DESKTOP_ENTRY_FORMAT, filesystem::path_executable_get());
file << desktopEntry;
file.close();
toasts.info(std::format("Wrote desktop entry to: {}", path.string()));
auto desktopDir = path.parent_path();
auto desktopUpdate =
std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string());
auto desktopFileName = path.filename().string();
auto setDefault = std::format("xdg-mime default {} application/x-anm2+xml",
desktopFileName.empty() ? path.string() : desktopFileName);
auto databaseUpdated = run_command_checked(desktopUpdate, "Update desktop database");
auto defaultRegistered = run_command_checked(setDefault, "Set default handler for .anm2");
return databaseUpdated && defaultRegistered;
};
auto iconsCached = cache_icons();
auto mimeRegistered = register_mime();
auto desktopRegistered = register_desktop_entry();
isAnm2Association = iconsCached && mimeRegistered && desktopRegistered;
if (isAnm2Association)
toasts.info("Associated .anm2 files with the editor.");
else
toasts.warning("Association incomplete. Please review the warnings above.");
#endif
}
ImGui::SetItemTooltip(
"Associate .anm2 files with the application (i.e., clicking on them in a file explorer will "
"open the application).");
if (ImGui::MenuItem("Remove .anm2 File Association", nullptr, false,
isAnm2Association || !isAbleToAssociateAnm2))
{
#ifdef _WIN32
#elif __unix__
{
auto iconsRemoved =
uninstall_icon_set("apps", "anm2ed") && uninstall_icon_set("mimetypes", "application-x-anm2+xml") &&
run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache");
if (iconsRemoved)
toasts.info("Removed cached icons.");
else
toasts.warning("Could not remove all cached icons.");
}
{
auto path = std::filesystem::path(filesystem::path_mime_get());
auto removed = remove_file_if_exists(path);
if (removed) toasts.info(std::format("Removed .anm2 MIME type: {}", path.string()));
auto mimeRoot = path.parent_path().parent_path();
run_command_checked(std::format("update-mime-database \"{}\"", mimeRoot.string()), "Update MIME database");
}
{
auto path = std::filesystem::path(filesystem::path_application_get());
if (remove_file_if_exists(path)) toasts.info(std::format("Removed desktop entry: {}", path.string()));
auto desktopDir = path.parent_path();
run_command_checked(
std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string()),
"Update desktop database");
}
#endif
isAnm2Association = false;
}
ImGui::SetItemTooltip("Unassociate .anm2 files with the application.");
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help"))
{
if (ImGui::MenuItem("About")) aboutPopup.open();
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
generatePopup.trigger();
if (ImGui::BeginPopupModal(generatePopup.label, &generatePopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto& startPosition = settings.generateStartPosition;
auto& size = settings.generateSize;
auto& pivot = settings.generatePivot;
auto& rows = settings.generateRows;
auto& columns = settings.generateColumns;
auto& count = settings.generateCount;
auto& delay = settings.generateDelay;
auto& zoom = settings.generateZoom;
auto& zoomStep = settings.viewZoomStep;
auto childSize = ImVec2(row_widget_width_get(2), size_without_footer_get().y);
if (ImGui::BeginChild("##Options Child", childSize, ImGuiChildFlags_Borders))
{
ImGui::InputInt2("Start Position", value_ptr(startPosition));
ImGui::InputInt2("Frame Size", value_ptr(size));
ImGui::InputInt2("Pivot", value_ptr(pivot));
ImGui::InputInt("Rows", &rows, STEP, STEP_FAST);
ImGui::InputInt("Columns", &columns, STEP, STEP_FAST);
input_int_range("Count", count, anm2::FRAME_NUM_MIN, rows * columns);
ImGui::InputInt("Delay", &delay, STEP, STEP_FAST);
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Preview Child", childSize, ImGuiChildFlags_Borders))
{
auto& backgroundColor = settings.previewBackgroundColor;
auto& time = generateTime;
auto& shaderTexture = resources.shaders[resource::shader::TEXTURE];
auto previewSize = ImVec2(ImGui::GetContentRegionAvail().x, size_without_footer_get(2).y);
generate.size_set(to_vec2(previewSize));
generate.bind();
generate.viewport_set();
generate.clear(backgroundColor);
if (document && document->reference.itemType == anm2::LAYER)
{
auto& texture = document->anm2.content
.spritesheets[document->anm2.content.layers[document->reference.itemID].spritesheetID]
.texture;
auto index = std::clamp((int)(time * (count - 1)), 0, (count - 1));
auto row = index / columns;
auto column = index % columns;
auto crop = startPosition + ivec2(size.x * column, size.y * row);
auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(texture.size);
auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(texture.size);
mat4 transform = generate.transform_get(zoom) * math::quad_model_get(size, {}, pivot);
generate.texture_render(shaderTexture, texture.id, transform, vec4(1.0f), {},
math::uv_vertices_get(uvMin, uvMax).data());
}
generate.unbind();
ImGui::Image(generate.texture, previewSize);
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
ImGui::SliderFloat("##Time", &time, 0.0f, 1.0f, "");
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
ImGui::InputFloat("##Zoom", &zoom, zoomStep, zoomStep, "%.0f%%");
zoom = glm::clamp(zoom, ZOOM_MIN, ZOOM_MAX);
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(2);
if (ImGui::Button("Generate", widgetSize))
{
auto generate_from_grid = [&]()
{
item->frames_generate_from_grid(startPosition, size, pivot, columns, count, delay);
animation->frameNum = animation->length();
};
DOCUMENT_EDIT_PTR(document, "Generate Animation from Grid", Document::FRAMES, generate_from_grid());
generatePopup.close();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) generatePopup.close();
ImGui::EndPopup();
}
changePopup.trigger();
if (ImGui::BeginPopupModal(changePopup.label, &changePopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto& isCrop = settings.changeIsCrop;
auto& isSize = settings.changeIsSize;
auto& isPosition = settings.changeIsPosition;
auto& isPivot = settings.changeIsPivot;
auto& isScale = settings.changeIsScale;
auto& isRotation = settings.changeIsRotation;
auto& isDelay = settings.changeIsDelay;
auto& isTint = settings.changeIsTint;
auto& isColorOffset = settings.changeIsColorOffset;
auto& isVisibleSet = settings.changeIsVisibleSet;
auto& isInterpolatedSet = settings.changeIsInterpolatedSet;
auto& crop = settings.changeCrop;
auto& size = settings.changeSize;
auto& position = settings.changePosition;
auto& pivot = settings.changePivot;
auto& scale = settings.changeScale;
auto& rotation = settings.changeRotation;
auto& delay = settings.changeDelay;
auto& tint = settings.changeTint;
auto& colorOffset = settings.changeColorOffset;
auto& isVisible = settings.changeIsVisible;
auto& isInterpolated = settings.changeIsInterpolated;
auto& isFromSelectedFrame = settings.changeIsFromSelectedFrame;
auto& numberFrames = settings.changeNumberFrames;
auto propertiesSize = child_size_get(10);
if (ImGui::BeginChild("##Properties", propertiesSize, ImGuiChildFlags_Borders))
{
#define PROPERTIES_WIDGET(body) \
ImGui::Checkbox(checkboxLabel, &isEnabled); \
ImGui::SameLine(); \
ImGui::BeginDisabled(!isEnabled); \
body; \
ImGui::EndDisabled();
auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value)
{ PROPERTIES_WIDGET(ImGui::Checkbox(valueLabel, &value)); };
auto color3_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec3& value)
{ PROPERTIES_WIDGET(ImGui::ColorEdit3(valueLabel, value_ptr(value))); };
auto color4_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec4& value)
{ PROPERTIES_WIDGET(ImGui::ColorEdit4(valueLabel, value_ptr(value))); };
auto float2_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec2& value)
{ PROPERTIES_WIDGET(ImGui::InputFloat2(valueLabel, value_ptr(value), math::vec2_format_get(value))); };
auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value)
{ PROPERTIES_WIDGET(ImGui::InputFloat(valueLabel, &value, STEP, STEP_FAST, math::float_format_get(value))); };
auto int_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value)
{ PROPERTIES_WIDGET(ImGui::InputInt(valueLabel, &value, STEP, STEP_FAST)); };
#undef PROPERTIES_WIDGET
float2_value("##Is Crop", "Crop", isCrop, crop);
float2_value("##Is Size", "Size", isSize, size);
float2_value("##Is Position", "Position", isPosition, position);
float2_value("##Is Pivot", "Pivot", isPivot, pivot);
float2_value("##Is Scale", "Scale", isScale, scale);
float_value("##Is Rotation", "Rotation", isRotation, rotation);
int_value("##Is Delay", "Delay", isDelay, delay);
color4_value("##Is Tint", "Tint", isTint, tint);
color3_value("##Is Color Offset", "Color Offset", isColorOffset, colorOffset);
bool_value("##Is Visible", "Visible", isVisibleSet, isVisible);
ImGui::SameLine();
bool_value("##Is Interpolated", "Interpolated", isInterpolatedSet, isInterpolated);
}
ImGui::EndChild();
auto settingsSize = child_size_get(2);
if (ImGui::BeginChild("##Settings", settingsSize, ImGuiChildFlags_Borders))
{
ImGui::Checkbox("From Selected Frame", &isFromSelectedFrame);
ImGui::SetItemTooltip("The frames after the currently referenced frame will be changed with these values.\nIf"
" off, will use all frames.");
ImGui::BeginDisabled(!isFromSelectedFrame);
input_int_range("Number of Frames", numberFrames, anm2::FRAME_NUM_MIN,
item->frames.size() - reference->frameIndex);
ImGui::SetItemTooltip("Set the number of frames that will be changed.");
ImGui::EndDisabled();
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(4);
auto frame_change = [&](anm2::ChangeType type)
{
anm2::FrameChange frameChange;
if (isCrop) frameChange.crop = std::make_optional(crop);
if (isSize) frameChange.size = std::make_optional(size);
if (isPosition) frameChange.position = std::make_optional(position);
if (isPivot) frameChange.pivot = std::make_optional(pivot);
if (isScale) frameChange.scale = std::make_optional(scale);
if (isRotation) frameChange.rotation = std::make_optional(rotation);
if (isDelay) frameChange.delay = std::make_optional(delay);
if (isTint) frameChange.tint = std::make_optional(tint);
if (isColorOffset) frameChange.colorOffset = std::make_optional(colorOffset);
if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible);
if (isInterpolatedSet) frameChange.isInterpolated = std::make_optional(isInterpolated);
DOCUMENT_EDIT_PTR(document, "Change Frame Properties", Document::FRAMES,
item->frames_change(frameChange, type,
isFromSelectedFrame && document->frame_get() ? reference->frameIndex : 0,
isFromSelectedFrame ? numberFrames : -1));
changePopup.close();
};
if (ImGui::Button("Add", widgetSize)) frame_change(anm2::ADD);
ImGui::SameLine();
if (ImGui::Button("Subtract", widgetSize)) frame_change(anm2::SUBTRACT);
ImGui::SameLine();
if (ImGui::Button("Adjust", widgetSize)) frame_change(anm2::ADJUST);
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) changePopup.close();
ImGui::EndPopup();
}
configurePopup.trigger();
if (ImGui::BeginPopupModal(configurePopup.label, &configurePopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto childSize = size_without_footer_get(2);
if (ImGui::BeginTabBar("##Configure Tabs"))
{
if (ImGui::BeginTabItem("General"))
{
if (ImGui::BeginChild("##Tab Child", childSize, true))
{
ImGui::SeparatorText("File");
ImGui::Checkbox("Autosaving", &editSettings.fileIsAutosave);
ImGui::SetItemTooltip("Enables autosaving of documents.");
ImGui::BeginDisabled(!editSettings.fileIsAutosave);
input_int_range("Autosave Time (minutes)", editSettings.fileAutosaveTime, 0, 10);
ImGui::SetItemTooltip("If changed, will autosave documents using this interval.");
ImGui::EndDisabled();
ImGui::SeparatorText("Keyboard");
input_float_range("Repeat Delay (seconds)", editSettings.keyboardRepeatDelay, 0.05f, 1.0f, 0.05f, 0.05f,
"%.2f");
ImGui::SetItemTooltip("Set how often, after repeating begins, key inputs will be fired.");
input_float_range("Repeat Rate (seconds)", editSettings.keyboardRepeatRate, 0.005f, 1.0f, 0.005f, 0.005f,
"%.3f");
ImGui::SetItemTooltip("Set how often, after repeating begins, key inputs will be fired.");
ImGui::SeparatorText("UI");
input_float_range("UI Scale", editSettings.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f");
ImGui::SetItemTooltip("Change the scale of the UI.");
ImGui::Checkbox("Vsync", &editSettings.isVsync);
ImGui::SetItemTooltip("Toggle vertical sync; synchronizes program update rate with monitor refresh rate.");
ImGui::SeparatorText("View");
input_float_range("Zoom Step", editSettings.viewZoomStep, 10.0f, 250.0f, 10.0f, 10.0f, "%.0f");
ImGui::SetItemTooltip("When zooming in/out with mouse or shortcut, this value will be used.");
}
ImGui::EndChild();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Shortcuts"))
{
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::BeginChild("##Tab Child", childSize, true))
{
if (ImGui::BeginTable("Shortcuts", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY))
{
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("Shortcut");
ImGui::TableSetupColumn("Value");
ImGui::TableHeadersRow();
for (int i = 0; i < SHORTCUT_COUNT; ++i)
{
bool isSelected = selectedShortcut == i;
ShortcutMember member = SHORTCUT_MEMBERS[i];
std::string* settingString = &(editSettings.*member);
std::string chordString = isSelected ? "" : *settingString;
ImGui::PushID(i);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextUnformatted(SHORTCUT_STRINGS[i]);
ImGui::TableSetColumnIndex(1);
if (ImGui::Selectable(chordString.c_str(), isSelected)) selectedShortcut = i;
ImGui::PopID();
if (isSelected)
{
ImGuiKeyChord chord{ImGuiKey_None};
if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) chord |= ImGuiMod_Ctrl;
if (ImGui::IsKeyDown(ImGuiMod_Shift)) chord |= ImGuiMod_Shift;
if (ImGui::IsKeyDown(ImGuiMod_Alt)) chord |= ImGuiMod_Alt;
if (ImGui::IsKeyDown(ImGuiMod_Super)) chord |= ImGuiMod_Super;
for (auto& key : KEY_MAP | std::views::values)
{
if (ImGui::IsKeyPressed(key))
{
chord |= key;
*settingString = chord_to_string(chord);
selectedShortcut = -1;
break;
}
}
}
}
ImGui::EndTable();
}
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
auto widgetSize = widget_size_with_row_get(3);
if (ImGui::Button("Save", widgetSize))
{
settings = editSettings;
configurePopup.close();
}
ImGui::SetItemTooltip("Use the configured settings.");
ImGui::SameLine();
if (ImGui::Button("Use Default Settings", widgetSize)) editSettings = Settings();
ImGui::SetItemTooltip("Reset the settings to their defaults.");
ImGui::SameLine();
if (ImGui::Button("Close", widgetSize)) configurePopup.close();
ImGui::SetItemTooltip("Close without updating settings.");
ImGui::EndPopup();
}
renderPopup.trigger();
if (ImGui::BeginPopupModal(renderPopup.label, &renderPopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto animation = document ? document->animation_get() : nullptr;
if (!animation) renderPopup.close();
auto& playback = document->playback;
auto& ffmpegPath = settings.renderFFmpegPath;
auto& path = settings.renderPath;
auto& format = settings.renderFormat;
auto& scale = settings.renderScale;
auto& isRaw = settings.renderIsRawAnimation;
auto& type = settings.renderType;
auto& start = manager.recordingStart;
auto& end = manager.recordingEnd;
auto& isRange = manager.isRecordingRange;
auto widgetSize = widget_size_with_row_get(2);
auto dialogType = type == render::PNGS ? dialog::PNG_DIRECTORY_SET
: type == render::GIF ? dialog::GIF_PATH_SET
: type == render::WEBM ? dialog::WEBM_PATH_SET
: dialog::NONE;
auto replace_extension = [&]()
{ path = std::filesystem::path(path).replace_extension(render::EXTENSIONS[type]); };
auto range_to_length = [&]()
{
start = 0;
end = animation->frameNum;
};
if (renderPopup.isJustOpened)
{
replace_extension();
if (!isRange) range_to_length();
}
if (ImGui::ImageButton("##FFmpeg Path Set", resources.icons[icon::FOLDER].id, icon_size_get()))
dialog.file_open(dialog::FFMPEG_PATH_SET);
ImGui::SameLine();
input_text_string("FFmpeg Path", &ffmpegPath);
ImGui::SetItemTooltip("Set the path where the FFmpeg installation is located.\nFFmpeg is required to render "
"animations.\nhttps://ffmpeg.org");
dialog.set_string_to_selected_path(ffmpegPath, dialog::FFMPEG_PATH_SET);
if (ImGui::ImageButton("##Path Set", resources.icons[icon::FOLDER].id, icon_size_get()))
{
if (dialogType == dialog::PNG_DIRECTORY_SET)
dialog.folder_open(dialogType);
else
dialog.file_save(dialogType);
}
ImGui::SameLine();
input_text_string(type == render::PNGS ? "Directory" : "Path", &path);
ImGui::SetItemTooltip("Set the output path or directory for the animation.");
dialog.set_string_to_selected_path(path, dialogType);
if (ImGui::Combo("Type", &type, render::STRINGS, render::COUNT)) replace_extension();
ImGui::SetItemTooltip("Set the type of the output.");
ImGui::BeginDisabled(type != render::PNGS);
input_text_string("Format", &format);
ImGui::SetItemTooltip(
"For outputted images, each image will use this format.\n{} represents the index of each image.");
ImGui::EndDisabled();
ImGui::BeginDisabled(!isRange);
input_int_range("Start", start, 0, animation->frameNum - 1);
ImGui::SetItemTooltip("Set the starting time of the animation.");
input_int_range("End", end, start + 1, animation->frameNum);
ImGui::SetItemTooltip("Set the ending time of the animation.");
ImGui::EndDisabled();
ImGui::BeginDisabled(!isRaw);
input_float_range("Scale", scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx");
ImGui::SetItemTooltip("Set the output scale of the animation.");
ImGui::EndDisabled();
if (ImGui::Checkbox("Custom Range", &isRange))
if (!isRange) range_to_length();
ImGui::SetItemTooltip("Toggle using a custom range for the animation.");
ImGui::SameLine();
ImGui::Checkbox("Raw", &isRaw);
ImGui::SetItemTooltip("Record only the raw animation; i.e., only its layers, to its bounds.");
ImGui::SameLine();
ImGui::Checkbox("Sound", &settings.timelineIsSound);
ImGui::SetItemTooltip("Toggle sounds playing with triggers.\nBind sounds to events in the Events window.\nThe "
"output animation will use the played sounds.");
if (ImGui::Button("Render", widgetSize))
{
manager.isRecordingStart = true;
playback.time = start;
playback.isPlaying = true;
renderPopup.close();
manager.progressPopup.open();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) renderPopup.close();
ImGui::EndPopup();
}
renderPopup.end();
aboutPopup.trigger();
if (ImGui::BeginPopupModal(aboutPopup.label, &aboutPopup.isOpen, ImGuiWindowFlags_NoResize))
{
if (ImGui::Button("Close")) aboutPopup.close();
ImGui::EndPopup();
}
if (shortcut(settings.shortcutNew, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_NEW);
if (shortcut(settings.shortcutOpen, shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_OPEN);
if (shortcut(settings.shortcutSave, shortcut::GLOBAL)) document->save();
if (shortcut(settings.shortcutSaveAs, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_SAVE);
if (shortcut(settings.shortcutExit, shortcut::GLOBAL)) isQuitting = true;
}
}

41
src/imgui/taskbar.h Normal file
View File

@@ -0,0 +1,41 @@
#pragma once
#include "canvas.h"
#include "dialog.h"
#include "filesystem_.h"
#include "imgui_.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::imgui
{
class Taskbar
{
Canvas generate;
float generateTime{};
PopupHelper generatePopup{PopupHelper("Generate Animation from Grid")};
PopupHelper changePopup{PopupHelper("Change All Frame Properties", imgui::POPUP_SMALL_NO_HEIGHT)};
PopupHelper renderPopup{PopupHelper("Render Animation", imgui::POPUP_SMALL_NO_HEIGHT)};
PopupHelper configurePopup{PopupHelper("Configure")};
PopupHelper aboutPopup{PopupHelper("About")};
Settings editSettings{};
int selectedShortcut{-1};
#if defined(_WIN32) || defined(__unix__)
bool isAbleToAssociateAnm2 = true;
#else
bool isAbleToAssociateAnm2 = false;
#endif
bool isAnm2Association = std::filesystem::exists(util::filesystem::path_application_get());
bool isQuittingMode{};
public:
float height{};
Taskbar();
void update(Manager&, Settings&, Resources&, Dialog&, bool&);
};
};

89
src/imgui/toast.cpp Normal file
View File

@@ -0,0 +1,89 @@
#include "toast.h"
#include "log.h"
#include <imgui/imgui.h>
#include "types.h"
using namespace anm2ed::types;
namespace anm2ed::imgui
{
constexpr auto LIFETIME = 4.0f;
constexpr auto FADE_THRESHOLD = 1.0f;
Toast::Toast(const std::string& message)
{
this->message = message;
lifetime = LIFETIME;
}
void Toasts::update()
{
ImGuiIO& io = ImGui::GetIO();
auto borderColor = ImGui::GetStyleColorVec4(ImGuiCol_Border);
auto textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text);
auto position = to_vec2(io.DisplaySize) - to_vec2(ImGui::GetStyle().ItemSpacing);
for (int i = (int)toasts.size() - 1; i >= 0; --i)
{
Toast& toast = toasts[i];
if (toast.lifetime <= 0.0f)
{
toasts.erase(toasts.begin() + i);
i--;
continue;
}
toast.lifetime -= ImGui::GetIO().DeltaTime;
auto alpha = toast.lifetime <= FADE_THRESHOLD ? toast.lifetime / FADE_THRESHOLD : 1.0f;
borderColor.w = alpha;
textColor.w = alpha;
ImGui::SetNextWindowPos(to_imvec2(position), ImGuiCond_None, {1.0f, 1.0f});
ImGui::SetNextWindowSize(ImVec2(0, ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y));
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
ImGui::SetNextWindowBgAlpha(alpha);
if (ImGui::Begin(std::format("##Toast #{}", i).c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav))
{
ImGui::TextUnformatted(toast.message.c_str());
position.y -= ImGui::GetWindowSize().y + ImGui::GetStyle().ItemSpacing.y;
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) toast.lifetime = 0.0f;
}
ImGui::End();
ImGui::PopStyleColor(2);
}
}
void Toasts::info(const std::string& message)
{
toasts.emplace_back(Toast(message));
logger.info(message);
}
void Toasts::error(const std::string& message)
{
toasts.emplace_back(Toast(message));
logger.error(message);
}
void Toasts::warning(const std::string& message)
{
toasts.emplace_back(Toast(message));
logger.warning(message);
}
}
anm2ed::imgui::Toasts toasts;

30
src/imgui/toast.h Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include <string>
#include <vector>
namespace anm2ed::imgui
{
class Toast
{
public:
std::string message{};
float lifetime{};
Toast(const std::string&);
};
class Toasts
{
public:
std::vector<Toast> toasts{};
void update();
void info(const std::string&);
void error(const std::string&);
void warning(const std::string&);
};
}
extern anm2ed::imgui::Toasts toasts;

View File

@@ -0,0 +1,605 @@
#include "animation_preview.h"
#include <ranges>
#include <glm/gtc/type_ptr.hpp>
#include "log.h"
#include "math_.h"
#include "toast.h"
#include "tool.h"
#include "types.h"
using namespace anm2ed::canvas;
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace anm2ed::resource;
using namespace glm;
namespace anm2ed::imgui
{
constexpr auto NULL_COLOR = vec4(0.0f, 0.0f, 1.0f, 0.90f);
constexpr auto TARGET_SIZE = vec2(32, 32);
constexpr auto POINT_SIZE = vec2(4, 4);
constexpr auto NULL_RECT_SIZE = vec2(100);
constexpr auto TRIGGER_TEXT_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.5f);
AnimationPreview::AnimationPreview() : Canvas(vec2())
{
}
void AnimationPreview::tick(Manager& manager, Document& document, Settings& settings)
{
auto& anm2 = document.anm2;
auto& playback = document.playback;
auto& zoom = document.previewZoom;
auto& pan = document.previewPan;
auto& isRootTransform = settings.previewIsRootTransform;
auto& scale = settings.renderScale;
if (playback.isPlaying)
{
auto& isSound = settings.timelineIsSound;
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
if (!anm2.content.sounds.empty() && isSound)
{
if (auto animation = document.animation_get();
animation && animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording))
{
if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER);
trigger.is_visible(anm2::TRIGGER))
if (anm2.content.sounds.contains(trigger.soundID)) anm2.content.sounds[trigger.soundID].audio.play(mixer);
}
}
document.reference.frameTime = playback.time;
}
if (manager.isRecording)
{
auto pixels = pixels_get();
renderFrames.push_back(Texture(pixels.data(), size));
if (playback.time > manager.recordingEnd || playback.isFinished)
{
auto& ffmpegPath = settings.renderFFmpegPath;
auto& path = settings.renderPath;
auto& type = settings.renderType;
if (type == render::PNGS)
{
auto& format = settings.renderFormat;
bool isSuccess{true};
for (auto [i, frame] : std::views::enumerate(renderFrames))
{
std::filesystem::path outputPath =
std::filesystem::path(path) / std::vformat(format, std::make_format_args(i));
if (!frame.write_png(outputPath))
{
isSuccess = false;
break;
}
logger.info(std::format("Saved frame to: {}", outputPath.string()));
}
if (isSuccess)
toasts.info(std::format("Exported rendered frames to: {}", path));
else
toasts.warning(std::format("Could not export frames to: {}", path));
}
else
{
if (animation_render(ffmpegPath, path, renderFrames, audioStream, (render::Type)type, size, anm2.info.fps))
toasts.info(std::format("Exported rendered animation to: {}", path));
else
toasts.warning(std::format("Could not output rendered animation: {}", path));
}
renderFrames.clear();
pan = savedPan;
zoom = savedZoom;
settings = savedSettings;
isSizeTrySet = true;
if (settings.timelineIsSound) audioStream.capture_end(mixer);
playback.isPlaying = false;
playback.isFinished = false;
manager.isRecording = false;
manager.progressPopup.close();
}
}
if (manager.isRecordingStart)
{
savedSettings = settings;
if (settings.timelineIsSound) audioStream.capture_begin(mixer);
if (settings.renderIsRawAnimation)
{
settings.previewBackgroundColor = vec4();
settings.previewIsGrid = false;
settings.previewIsAxes = false;
settings.timelineIsOnlyShowLayers = true;
savedZoom = zoom;
savedPan = pan;
if (auto animation = document.animation_get())
{
if (auto rect = animation->rect(isRootTransform); rect != vec4(-1.0f))
{
size_set(vec2(rect.w, rect.z) * scale);
set_to_rect(zoom, pan, rect);
}
}
isSizeTrySet = false;
bind();
clear(settings.previewBackgroundColor);
unbind();
}
manager.isRecordingStart = false;
manager.isRecording = true;
}
}
void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& playback = document.playback;
auto& reference = document.reference;
auto animation = document.animation_get();
auto& pan = document.previewPan;
auto& zoom = document.previewZoom;
auto& backgroundColor = settings.previewBackgroundColor;
auto& axesColor = settings.previewAxesColor;
auto& gridColor = settings.previewGridColor;
auto& gridSize = settings.previewGridSize;
auto& gridOffset = settings.previewGridOffset;
auto& zoomStep = settings.viewZoomStep;
auto& isGrid = settings.previewIsGrid;
auto& overlayTransparency = settings.previewOverlayTransparency;
auto& overlayIndex = document.overlayIndex;
auto& isRootTransform = settings.previewIsRootTransform;
auto& isPivots = settings.previewIsPivots;
auto& isAxes = settings.previewIsAxes;
auto& isAltIcons = settings.previewIsAltIcons;
auto& isBorder = settings.previewIsBorder;
auto& tool = settings.tool;
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
auto& shaderLine = resources.shaders[shader::LINE];
auto& shaderAxes = resources.shaders[shader::AXIS];
auto& shaderGrid = resources.shaders[shader::GRID];
auto& shaderTexture = resources.shaders[shader::TEXTURE];
auto center_view = [&]() { pan = vec2(); };
if (ImGui::Begin("Animation Preview", &settings.windowIsAnimationPreview))
{
auto childSize = ImVec2(row_widget_width_get(4),
(ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2));
if (ImGui::BeginChild("##Grid Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::Checkbox("Grid", &isGrid);
ImGui::SetItemTooltip("Toggle the visibility of the grid.");
ImGui::SameLine();
ImGui::ColorEdit4("Color", value_ptr(gridColor), ImGuiColorEditFlags_NoInputs);
ImGui::SetItemTooltip("Change the grid's color.");
input_int2_range("Size", gridSize, ivec2(GRID_SIZE_MIN), ivec2(GRID_SIZE_MAX));
ImGui::SetItemTooltip("Change the size of all cells in the grid.");
input_int2_range("Offset", gridOffset, ivec2(GRID_OFFSET_MIN), ivec2(GRID_OFFSET_MAX));
ImGui::SetItemTooltip("Change the offset of the grid.");
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##View Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::InputFloat("Zoom", &zoom, zoomStep, zoomStep, "%.0f%%");
ImGui::SetItemTooltip("Change the zoom of the preview.");
auto widgetSize = widget_size_with_row_get(2);
shortcut(settings.shortcutCenterView);
if (ImGui::Button("Center View", widgetSize)) pan = vec2();
set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView);
ImGui::SameLine();
shortcut(settings.shortcutFit);
if (ImGui::Button("Fit", widgetSize))
if (animation) set_to_rect(zoom, pan, animation->rect(isRootTransform));
set_item_tooltip_shortcut("Set the view to match the extent of the animation.", settings.shortcutFit);
ImGui::TextUnformatted(std::format(POSITION_FORMAT, (int)mousePos.x, (int)mousePos.y).c_str());
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::ColorEdit4("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs);
ImGui::SetItemTooltip("Change the background color.");
ImGui::SameLine();
ImGui::Checkbox("Axes", &isAxes);
ImGui::SetItemTooltip("Toggle the axes' visbility.");
ImGui::SameLine();
ImGui::ColorEdit4("Color", value_ptr(axesColor), ImGuiColorEditFlags_NoInputs);
ImGui::SetItemTooltip("Set the color of the axes.");
combo_negative_one_indexed("Overlay", &overlayIndex, document.animation.labels);
ImGui::SetItemTooltip("Set an animation to be drawn over the current animation.");
ImGui::InputFloat("Alpha", &overlayTransparency, 0, 0, "%.0f");
ImGui::SetItemTooltip("Set the alpha of the overlayed animation.");
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Helpers Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
auto helpersChildSize = ImVec2(row_widget_width_get(2), ImGui::GetContentRegionAvail().y);
if (ImGui::BeginChild("##Helpers Child 1", helpersChildSize))
{
ImGui::Checkbox("Root Transform", &isRootTransform);
ImGui::SetItemTooltip("Root frames will transform the rest of the animation.");
ImGui::Checkbox("Pivots", &isPivots);
ImGui::SetItemTooltip("Toggle the visibility of the animation's pivots.");
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Helpers Child 2", helpersChildSize))
{
ImGui::Checkbox("Alt Icons", &isAltIcons);
ImGui::SetItemTooltip("Toggle a different appearance of the target icons.");
ImGui::Checkbox("Border", &isBorder);
ImGui::SetItemTooltip("Toggle the visibility of borders around layers.");
}
ImGui::EndChild();
}
ImGui::EndChild();
auto cursorScreenPos = ImGui::GetCursorScreenPos();
if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail()));
viewport_set();
bind();
clear(backgroundColor);
if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor);
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
auto render = [&](anm2::Animation* animation, float time, vec3 colorOffset = {}, float alphaOffset = {},
bool isOnionskin = false)
{
auto transform = transform_get(zoom, pan);
auto root = animation->rootAnimation.frame_generate(time, anm2::ROOT);
if (isRootTransform)
transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation);
if (!isOnlyShowLayers && root.isVisible && animation->rootAnimation.isVisible)
{
auto rootTransform = transform * math::quad_model_get(TARGET_SIZE, root.position, TARGET_SIZE * 0.5f,
math::percent_to_unit(root.scale), root.rotation);
vec4 color = isOnionskin ? vec4(colorOffset, alphaOffset) : color::GREEN;
auto icon = isAltIcons ? icon::TARGET_ALT : icon::TARGET;
texture_render(shaderTexture, resources.icons[icon].id, rootTransform, color);
}
for (auto& id : animation->layerOrder)
{
auto& layerAnimation = animation->layerAnimations[id];
if (!layerAnimation.isVisible) continue;
auto& layer = anm2.content.layers.at(id);
if (auto frame = layerAnimation.frame_generate(time, anm2::LAYER); frame.is_visible())
{
auto spritesheet = anm2.spritesheet_get(layer.spritesheetID);
if (!spritesheet || !spritesheet->is_valid()) continue;
auto& texture = spritesheet->texture;
auto layerModel = math::quad_model_get(frame.size, frame.position, frame.pivot,
math::percent_to_unit(frame.scale), frame.rotation);
auto layerTransform = transform * layerModel;
auto uvMin = frame.crop / vec2(texture.size);
auto uvMax = (frame.crop + frame.size) / vec2(texture.size);
auto vertices = math::uv_vertices_get(uvMin, uvMax);
vec3 frameColorOffset = frame.colorOffset + colorOffset;
vec4 frameTint = frame.tint;
frameTint.a = std::max(0.0f, frameTint.a - alphaOffset);
texture_render(shaderTexture, texture.id, layerTransform, frameTint, frameColorOffset, vertices.data());
auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) : color::RED;
if (isBorder) rect_render(shaderLine, layerTransform, layerModel, color);
if (isPivots)
{
auto pivotModel = math::quad_model_get(PIVOT_SIZE, frame.position, PIVOT_SIZE * 0.5f,
math::percent_to_unit(frame.scale), frame.rotation);
auto pivotTransform = transform * pivotModel;
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color);
}
}
}
for (auto& [id, nullAnimation] : animation->nullAnimations)
{
if (!nullAnimation.isVisible || isOnlyShowLayers) continue;
auto& isShowRect = anm2.content.nulls[id].isShowRect;
if (auto frame = nullAnimation.frame_generate(time, anm2::NULL_); frame.isVisible)
{
auto icon = isShowRect ? icon::POINT : isAltIcons ? icon::TARGET_ALT : icon::TARGET;
auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE;
auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset)
: id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED
: NULL_COLOR;
auto nullModel = math::quad_model_get(size, frame.position, size * 0.5f, math::percent_to_unit(frame.scale),
frame.rotation);
auto nullTransform = transform * nullModel;
texture_render(shaderTexture, resources.icons[icon].id, nullTransform, color);
if (isShowRect)
{
auto rectModel = math::quad_model_get(NULL_RECT_SIZE, frame.position, NULL_RECT_SIZE * 0.5f,
math::percent_to_unit(frame.scale), frame.rotation);
auto rectTransform = transform * rectModel;
rect_render(shaderLine, rectTransform, rectModel, color);
}
}
}
};
auto onionskin_render = [&](float time, int count, int direction, vec3 color)
{
for (int i = 1; i <= count; i++)
{
float useTime = time + (float)(direction * i);
if (useTime < 0.0f || useTime > animation->frameNum) continue;
float alphaOffset = (1.0f / (count + 1)) * i;
render(animation, useTime, color, alphaOffset, true);
}
};
auto onionskins_render = [&](float time)
{
onionskin_render(time, settings.onionskinBeforeCount, -1, settings.onionskinBeforeColor);
onionskin_render(time, settings.onionskinAfterCount, 1, settings.onionskinAfterColor);
};
auto frameTime = reference.frameTime > -1 && !playback.isPlaying ? reference.frameTime : playback.time;
if (animation)
{
auto& drawOrder = settings.onionskinDrawOrder;
auto& isEnabled = settings.onionskinIsEnabled;
if (drawOrder == draw_order::BELOW && isEnabled) onionskins_render(frameTime);
render(animation, frameTime);
if (auto overlayAnimation = anm2.animation_get({overlayIndex}))
render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency));
if (drawOrder == draw_order::ABOVE && isEnabled) onionskins_render(frameTime);
}
unbind();
ImGui::Image(texture, to_imvec2(size));
isPreviewHovered = ImGui::IsItemHovered();
if (animation && animation->triggers.isVisible && !isOnlyShowLayers && !manager.isRecording)
{
if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER);
trigger.isVisible && trigger.eventID > -1)
{
auto clipMin = ImGui::GetItemRectMin();
auto clipMax = ImGui::GetItemRectMax();
auto drawList = ImGui::GetWindowDrawList();
auto textPos = to_imvec2(to_vec2(cursorScreenPos) + to_vec2(ImGui::GetStyle().WindowPadding));
drawList->PushClipRect(clipMin, clipMax);
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE);
drawList->AddText(textPos, ImGui::GetColorU32(TRIGGER_TEXT_COLOR),
anm2.content.events.at(trigger.eventID).name.c_str());
ImGui::PopFont();
drawList->PopClipRect();
}
}
if (isPreviewHovered)
{
auto isMouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
auto isMouseReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
auto isMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
auto mouseDelta = to_ivec2(ImGui::GetIO().MouseDelta);
auto mouseWheel = ImGui::GetIO().MouseWheel;
auto isLeftJustPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false);
auto isRightJustPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow, false);
auto isUpJustPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow, false);
auto isDownJustPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow, false);
auto isLeftPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
auto isRightPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
auto isUpPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow);
auto isDownPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow);
auto isLeftDown = ImGui::IsKeyDown(ImGuiKey_LeftArrow);
auto isRightDown = ImGui::IsKeyDown(ImGuiKey_RightArrow);
auto isUpDown = ImGui::IsKeyDown(ImGuiKey_UpArrow);
auto isDownDown = ImGui::IsKeyDown(ImGuiKey_DownArrow);
auto isLeftReleased = ImGui::IsKeyReleased(ImGuiKey_LeftArrow);
auto isRightReleased = ImGui::IsKeyReleased(ImGuiKey_RightArrow);
auto isUpReleased = ImGui::IsKeyReleased(ImGuiKey_UpArrow);
auto isDownReleased = ImGui::IsKeyReleased(ImGuiKey_DownArrow);
auto isKeyJustPressed = isLeftJustPressed || isRightJustPressed || isUpJustPressed || isDownJustPressed;
auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown;
auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased;
auto isZoomIn = chord_repeating(string_to_chord(settings.shortcutZoomIn));
auto isZoomOut = chord_repeating(string_to_chord(settings.shortcutZoomOut));
auto isBegin = isMouseClicked || isKeyJustPressed;
auto isDuring = isMouseDown || isKeyDown;
auto isEnd = isMouseReleased || isKeyReleased;
auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift);
auto frame = document.frame_get();
auto useTool = tool;
auto step = isMod ? canvas::STEP_FAST : canvas::STEP;
mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos));
if (isMouseMiddleDown) useTool = tool::PAN;
if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE;
if (tool == tool::SCALE && isMouseRightDown) useTool = tool::MOVE;
auto& areaType = tool::INFO[useTool].areaType;
auto cursor = areaType == tool::ANIMATION_PREVIEW || areaType == tool::ALL ? tool::INFO[useTool].cursor
: ImGuiMouseCursor_NotAllowed;
ImGui::SetMouseCursor(cursor);
ImGui::SetKeyboardFocusHere(-1);
switch (useTool)
{
case tool::PAN:
if (isMouseDown || isMouseMiddleDown) pan += mouseDelta;
break;
case tool::MOVE:
if (!frame) break;
if (isBegin) document.snapshot("Frame Position");
if (isMouseDown) frame->position = mousePos;
if (isLeftPressed) frame->position.x -= step;
if (isRightPressed) frame->position.x += step;
if (isUpPressed) frame->position.y -= step;
if (isDownPressed) frame->position.y += step;
if (isEnd) document.change(Document::FRAMES);
if (isDuring)
{
if (ImGui::BeginTooltip())
{
auto positionFormat = math::vec2_format_get(frame->position);
auto positionString = std::format("Position: ({}, {})", positionFormat, positionFormat);
ImGui::Text(positionString.c_str(), frame->position.x, frame->position.y);
ImGui::EndTooltip();
}
}
break;
case tool::SCALE:
if (!frame) break;
if (isBegin) document.snapshot("Frame Scale");
if (isMouseDown) frame->scale += mouseDelta;
if (isLeftPressed) frame->scale.x -= step;
if (isRightPressed) frame->scale.x += step;
if (isUpPressed) frame->scale.y -= step;
if (isDownPressed) frame->scale.y += step;
if (isDuring)
{
if (ImGui::BeginTooltip())
{
auto scaleFormat = math::vec2_format_get(frame->scale);
auto scaleString = std::format("Scale: ({}, {})", scaleFormat, scaleFormat);
ImGui::Text(scaleString.c_str(), frame->scale.x, frame->scale.y);
ImGui::EndTooltip();
}
}
if (isEnd) document.change(Document::FRAMES);
break;
case tool::ROTATE:
if (!frame) break;
if (isBegin) document.snapshot("Frame Rotation");
if (isMouseDown) frame->rotation += mouseDelta.y;
if (isLeftPressed || isDownPressed) frame->rotation -= step;
if (isUpPressed || isRightPressed) frame->rotation += step;
if (isDuring)
{
if (ImGui::BeginTooltip())
{
auto rotationFormat = math::float_format_get(frame->rotation);
auto rotationString = std::format("Rotation: {}", rotationFormat);
ImGui::Text(rotationString.c_str(), frame->rotation);
ImGui::EndTooltip();
}
}
if (isEnd) document.change(Document::FRAMES);
break;
default:
break;
}
if (mouseWheel != 0 || isZoomIn || isZoomOut)
zoom_set(zoom, pan, mouseWheel != 0 ? vec2(mousePos) : vec2(),
(mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep);
}
}
ImGui::End();
manager.progressPopup.trigger();
if (ImGui::BeginPopupModal(manager.progressPopup.label, &manager.progressPopup.isOpen, ImGuiWindowFlags_NoResize))
{
if (!animation) return;
auto& start = manager.recordingStart;
auto& end = manager.recordingEnd;
auto progress = (playback.time - start) / (end - start);
ImGui::ProgressBar(progress);
if (ImGui::Button("Cancel", ImVec2(ImGui::GetContentRegionAvail().x, 0)))
{
playback.isPlaying = false;
manager.isRecording = false;
manager.progressPopup.close();
}
ImGui::EndPopup();
}
if (!document.isAnimationPreviewSet)
{
center_view();
zoom = settings.previewStartZoom;
document.isAnimationPreviewSet = true;
}
settings.previewStartZoom = zoom;
}
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "audio_stream.h"
#include "canvas.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::imgui
{
class AnimationPreview : public Canvas
{
MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
AudioStream audioStream = AudioStream(mixer);
bool isPreviewHovered{};
bool isSizeTrySet{true};
Settings savedSettings{};
float savedZoom{};
glm::vec2 savedPan{};
glm::ivec2 mousePos{};
std::vector<resource::Texture> renderFrames{};
public:
AnimationPreview();
void tick(Manager&, Document&, Settings&);
void update(Manager&, Settings&, Resources&);
};
}

View File

@@ -0,0 +1,395 @@
#include "animations.h"
#include <ranges>
#include "toast.h"
#include "vector_.h"
using namespace anm2ed::util;
using namespace anm2ed::resource;
using namespace anm2ed::types;
namespace anm2ed::imgui
{
void Animations::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& reference = document.reference;
auto& hovered = document.animation.hovered;
auto& selection = document.animation.selection;
auto& mergeSelection = document.merge.selection;
auto& mergeReference = document.merge.reference;
auto& overlayIndex = document.overlayIndex;
hovered = -1;
if (ImGui::Begin("Animations", &settings.windowIsAnimations))
{
auto childSize = size_without_footer_get();
if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders))
{
selection.start(anm2.animations.items.size());
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
{
ImGui::PushID(i);
auto isDefault = anm2.animations.defaultAnimation == animation.name;
auto isReferenced = reference.animationIndex == i;
auto font = isDefault && isReferenced ? font::BOLD_ITALICS
: isDefault ? font::BOLD
: isReferenced ? font::ITALICS
: font::REGULAR;
ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
ImGui::SetNextItemSelectionUserData((int)i);
if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i),
animation.name, selection.contains((int)i)))
reference = {(int)i};
if (ImGui::IsItemHovered()) hovered = (int)i;
ImGui::PopFont();
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(animation.name.c_str());
ImGui::PopFont();
if (isDefault)
{
ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
ImGui::TextUnformatted("(Default Animation)");
ImGui::PopFont();
}
ImGui::Text("Length: %d", animation.frameNum);
ImGui::Text("Loop: %s", animation.isLoop ? "true" : "false");
ImGui::EndTooltip();
}
if (ImGui::BeginDragDropSource())
{
static std::vector<int> dragDropSelection{};
dragDropSelection.assign(selection.begin(), selection.end());
ImGui::SetDragDropPayload("Animation Drag Drop", dragDropSelection.data(),
dragDropSelection.size() * sizeof(int));
for (auto& i : dragDropSelection)
ImGui::Text("%s", anm2.animations.items[(int)i].name.c_str());
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget())
{
if (auto payload = ImGui::AcceptDragDropPayload("Animation Drag Drop"))
{
auto payloadIndices = (int*)(payload->Data);
auto payloadCount = payload->DataSize / sizeof(int);
std::vector<int> indices(payloadIndices, payloadIndices + payloadCount);
std::sort(indices.begin(), indices.end());
DOCUMENT_EDIT(document, "Move Animation(s)", Document::ANIMATIONS,
selection = vector::move_indices(anm2.animations.items, indices, i));
}
ImGui::EndDragDropTarget();
}
ImGui::PopID();
}
selection.finish();
auto copy = [&]()
{
if (!selection.empty())
{
std::string clipboardText{};
for (auto& i : selection)
clipboardText += anm2.animations.items[i].to_string();
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.animations.items[hovered].to_string());
};
auto cut = [&]()
{
copy();
auto remove = [&]()
{
if (!selection.empty())
{
for (auto& i : selection | std::views::reverse)
anm2.animations.items.erase(anm2.animations.items.begin() + i);
selection.clear();
}
else if (hovered > -1)
{
anm2.animations.items.erase(anm2.animations.items.begin() + hovered);
hovered = -1;
}
};
DOCUMENT_EDIT(document, "Cut Animation(s)", Document::ANIMATIONS, remove());
};
auto paste = [&]()
{
auto clipboardText = clipboard.get();
auto deserialize = [&]()
{
auto start = selection.empty() ? anm2.animations.items.size() : *selection.rbegin() + 1;
std::set<int> indices{};
std::string errorString{};
if (anm2.animations_deserialize(clipboardText, start, indices, &errorString))
selection = indices;
else
toasts.error(std::format("Failed to deserialize animation(s): {}", errorString));
};
DOCUMENT_EDIT(document, "Paste Animation(s)", Document::ANIMATIONS, deserialize());
};
if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut();
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste();
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, !selection.empty() || hovered > -1)) cut();
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, !selection.empty() || hovered > -1)) copy();
if (ImGui::MenuItem("Paste", settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) paste();
ImGui::EndPopup();
}
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(5);
shortcut(settings.shortcutAdd);
if (ImGui::Button("Add", widgetSize))
{
auto add = [&]()
{
anm2::Animation animation;
if (anm2::Animation* referenceAnimation = document.animation_get())
{
for (auto [id, layerAnimation] : referenceAnimation->layerAnimations)
animation.layerAnimations[id] = anm2::Item();
animation.layerOrder = referenceAnimation->layerOrder;
for (auto [id, nullAnimation] : referenceAnimation->nullAnimations)
animation.nullAnimations[id] = anm2::Item();
}
animation.rootAnimation.frames.emplace_back(anm2::Frame());
auto index = 0;
if (!anm2.animations.items.empty())
index = selection.empty() ? (int)anm2.animations.items.size() - 1 : *selection.rbegin() + 1;
anm2.animations.items.insert(anm2.animations.items.begin() + index, animation);
selection = {index};
reference = {index};
};
DOCUMENT_EDIT(document, "Add Animation", Document::ANIMATIONS, add());
}
set_item_tooltip_shortcut("Add a new animation.", settings.shortcutAdd);
ImGui::SameLine();
ImGui::BeginDisabled(selection.empty());
{
shortcut(settings.shortcutDuplicate);
if (ImGui::Button("Duplicate", widgetSize))
{
auto duplicate = [&]()
{
auto duplicated = selection;
auto end = std::ranges::max(duplicated);
for (auto& id : duplicated)
{
anm2.animations.items.insert(anm2.animations.items.begin() + end, anm2.animations.items[id]);
selection.insert(++end);
selection.erase(id);
}
};
DOCUMENT_EDIT(document, "Duplicate Animation(s)", Document::ANIMATIONS, duplicate());
}
set_item_tooltip_shortcut("Duplicate the selected animation(s).", settings.shortcutDuplicate);
ImGui::SameLine();
if (shortcut(settings.shortcutMerge, shortcut::FOCUSED) && !selection.empty())
{
auto merge_quick = [&]()
{
int merged{};
if (selection.contains(overlayIndex)) overlayIndex = -1;
if (selection.size() > 1)
merged = anm2.animations_merge(*selection.begin(), selection);
else if (selection.size() == 1 && *selection.begin() != (int)anm2.animations.items.size() - 1)
{
auto start = *selection.begin();
auto next = *selection.begin() + 1;
std::set<int> animationSet{};
animationSet.insert(start);
animationSet.insert(next);
merged = anm2.animations_merge(start, animationSet);
}
else
return;
selection = {merged};
reference = {merged};
};
DOCUMENT_EDIT(document, "Merge Animations", Document::ANIMATIONS, merge_quick())
}
ImGui::BeginDisabled(selection.size() != 1);
{
if (ImGui::Button("Merge", widgetSize))
{
mergePopup.open();
mergeSelection.clear();
mergeReference = *selection.begin();
}
}
ImGui::EndDisabled();
set_item_tooltip_shortcut("Open the merge popup.\nUsing the shortcut will merge the animations with\nthe last "
"configured merge settings.",
settings.shortcutMerge);
ImGui::SameLine();
shortcut(settings.shortcutRemove);
if (ImGui::Button("Remove", widgetSize))
{
auto remove = [&]()
{
for (auto& i : selection | std::views::reverse)
{
if (i == overlayIndex) overlayIndex = -1;
anm2.animations.items.erase(anm2.animations.items.begin() + i);
}
selection.clear();
};
DOCUMENT_EDIT(document, "Remove Animation(s)", Document::ANIMATIONS, remove());
}
set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate);
ImGui::SameLine();
shortcut(settings.shortcutDefault);
ImGui::BeginDisabled(selection.size() != 1);
if (ImGui::Button("Default", widgetSize))
{
DOCUMENT_EDIT(document, "Default Animation", Document::ANIMATIONS,
anm2.animations.defaultAnimation = anm2.animations.items[*selection.begin()].name);
}
ImGui::EndDisabled();
set_item_tooltip_shortcut("Set the selected animation as the default.", settings.shortcutDefault);
}
ImGui::EndDisabled();
mergePopup.trigger();
if (ImGui::BeginPopupModal(mergePopup.label, &mergePopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto merge_close = [&]()
{
mergeSelection.clear();
mergePopup.close();
};
auto& type = settings.mergeType;
auto& isDeleteAnimationsAfter = settings.mergeIsDeleteAnimationsAfter;
auto footerSize = footer_size_get();
auto optionsSize = child_size_get(2);
auto deleteAfterSize = child_size_get();
auto animationsSize =
ImVec2(0, ImGui::GetContentRegionAvail().y -
(optionsSize.y + deleteAfterSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 3));
if (ImGui::BeginChild("Animations", animationsSize, ImGuiChildFlags_Borders))
{
mergeSelection.start(anm2.animations.items.size());
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
{
if (i == mergeReference) continue;
ImGui::PushID(i);
ImGui::SetNextItemSelectionUserData(i);
ImGui::Selectable(animation.name.c_str(), mergeSelection.contains(i));
ImGui::PopID();
}
mergeSelection.finish();
}
ImGui::EndChild();
if (ImGui::BeginChild("Merge Options", optionsSize, ImGuiChildFlags_Borders))
{
auto size = ImVec2(optionsSize.x * 0.5f, optionsSize.y - ImGui::GetStyle().WindowPadding.y * 2);
if (ImGui::BeginChild("Merge Options 1", size))
{
ImGui::RadioButton("Append Frames", &type, merge::APPEND);
ImGui::RadioButton("Prepend Frames", &type, merge::PREPEND);
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("Merge Options 2", size))
{
ImGui::RadioButton("Replace Frames", &type, merge::REPLACE);
ImGui::RadioButton("Ignore Frames", &type, merge::IGNORE);
}
ImGui::EndChild();
}
ImGui::EndChild();
if (ImGui::BeginChild("Merge Delete After", deleteAfterSize, ImGuiChildFlags_Borders))
ImGui::Checkbox("Delete Animations After", &isDeleteAnimationsAfter);
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(2);
if (ImGui::Button("Merge", widgetSize))
{
auto merge = [&]()
{
if (mergeSelection.contains(overlayIndex)) overlayIndex = -1;
auto merged =
anm2.animations_merge(mergeReference, mergeSelection, (merge::Type)type, isDeleteAnimationsAfter);
selection = {merged};
reference = {merged};
};
DOCUMENT_EDIT(document, "Merge Animations", Document::ANIMATIONS, merge());
merge_close();
}
ImGui::SameLine();
if (ImGui::Button("Close", widgetSize)) merge_close();
ImGui::EndPopup();
}
}
ImGui::End();
}
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include "clipboard.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::imgui
{
class Animations
{
PopupHelper mergePopup{PopupHelper("Merge Animations")};
public:
void update(Manager&, Settings&, Resources&, Clipboard&);
};
}

134
src/imgui/window/events.cpp Normal file
View File

@@ -0,0 +1,134 @@
#include "events.h"
#include <ranges>
#include "map_.h"
#include "toast.h"
using namespace anm2ed::util;
using namespace anm2ed::resource;
using namespace anm2ed::types;
namespace anm2ed::imgui
{
void Events::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& unused = document.event.unused;
auto& hovered = document.event.hovered;
auto& reference = document.event.reference;
auto& selection = document.event.selection;
hovered = -1;
if (ImGui::Begin("Events", &settings.windowIsEvents))
{
auto childSize = size_without_footer_get();
if (ImGui::BeginChild("##Events Child", childSize, true))
{
selection.start(anm2.content.events.size());
for (auto& [id, event] : anm2.content.events)
{
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
if (selectable_input_text(event.name, std::format("###Document #{} Event #{}", manager.selected, id),
event.name, selection.contains(id)))
if (ImGui::IsItemHovered()) hovered = id;
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(event.name.c_str());
ImGui::PopFont();
ImGui::EndTooltip();
}
ImGui::PopID();
}
selection.finish();
auto copy = [&]()
{
if (!selection.empty())
{
std::string clipboardText{};
for (auto& id : selection)
clipboardText += anm2.content.events[id].to_string(id);
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.content.events[hovered].to_string(hovered));
};
auto paste = [&](merge::Type type)
{
std::string errorString{};
document.snapshot("Paste Event(s)");
if (anm2.events_deserialize(clipboard.get(), type, &errorString))
document.change(Document::EVENTS);
else
toasts.error(std::format("Failed to deserialize event(s): {}", errorString));
};
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, false);
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, !selection.empty() || hovered > -1)) copy();
if (ImGui::BeginMenu("Paste", !clipboard.is_empty()))
{
if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND);
if (ImGui::MenuItem("Replace")) paste(merge::REPLACE);
ImGui::EndMenu();
}
ImGui::EndPopup();
}
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(2);
shortcut(settings.shortcutAdd);
if (ImGui::Button("Add", widgetSize))
{
auto add = [&]()
{
auto id = map::next_id_get(anm2.content.events);
anm2.content.events[id] = anm2::Event();
selection = {id};
reference = {id};
};
DOCUMENT_EDIT(document, "Add Event", Document::EVENTS, add());
}
set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd);
ImGui::SameLine();
shortcut(settings.shortcutRemove);
ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize))
{
auto remove_unused = [&]()
{
for (auto& id : unused)
anm2.content.events.erase(id);
unused.clear();
};
DOCUMENT_EDIT(document, "Remove Unused Events", Document::EVENTS, remove_unused());
}
ImGui::EndDisabled();
set_item_tooltip_shortcut("Remove unused events (i.e., ones not used by any trigger in any animation.)",
settings.shortcutRemove);
}
ImGui::End();
}
}

15
src/imgui/window/events.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "clipboard.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::imgui
{
class Events
{
public:
void update(Manager&, Settings&, Resources&, Clipboard&);
};
}

View File

@@ -0,0 +1,124 @@
#include "frame_properties.h"
#include <glm/gtc/type_ptr.hpp>
#include "math_.h"
#include "types.h"
using namespace anm2ed::util::math;
using namespace anm2ed::types;
using namespace glm;
namespace anm2ed::imgui
{
void FrameProperties::update(Manager& manager, Settings& settings)
{
if (ImGui::Begin("Frame Properties", &settings.windowIsFrameProperties))
{
auto& document = *manager.get();
auto& type = document.reference.itemType;
auto frame = document.frame_get();
auto useFrame = frame ? *frame : anm2::Frame();
ImGui::BeginDisabled(!frame);
{
if (type == anm2::TRIGGER)
{
if (combo_negative_one_indexed("Event", frame ? &useFrame.eventID : &dummy_value<int>(),
document.event.labels))
DOCUMENT_EDIT(document, "Trigger Event", Document::FRAMES, frame->eventID = useFrame.eventID);
ImGui::SetItemTooltip("Change the event this trigger uses.");
if (combo_negative_one_indexed("Sound", frame ? &useFrame.soundID : &dummy_value<int>(),
document.sound.labels))
DOCUMENT_EDIT(document, "Trigger Sound", Document::FRAMES, frame->soundID = useFrame.soundID);
ImGui::SetItemTooltip("Change the sound this trigger uses.");
if (ImGui::InputInt("At Frame", frame ? &useFrame.atFrame : &dummy_value<int>(), imgui::STEP,
imgui::STEP_FAST, !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0))
DOCUMENT_EDIT(document, "Trigger At Frame", Document::FRAMES, frame->atFrame = useFrame.atFrame);
ImGui::SetItemTooltip("Change the frame the trigger will be activated at.");
}
else
{
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_);
{
if (ImGui::InputFloat2("Crop", frame ? value_ptr(useFrame.crop) : &dummy_value<float>(),
frame ? vec2_format_get(useFrame.crop) : ""))
DOCUMENT_EDIT(document, "Frame Crop", Document::FRAMES, frame->crop = useFrame.crop);
ImGui::SetItemTooltip("Change the crop position the frame uses.");
if (ImGui::InputFloat2("Size", frame ? value_ptr(useFrame.size) : &dummy_value<float>(),
frame ? vec2_format_get(useFrame.size) : ""))
DOCUMENT_EDIT(document, "Frame Size", Document::FRAMES, frame->size = useFrame.size);
ImGui::SetItemTooltip("Change the size of the crop the frame uses.");
}
ImGui::EndDisabled();
if (ImGui::InputFloat2("Position", frame ? value_ptr(useFrame.position) : &dummy_value<float>(),
frame ? vec2_format_get(useFrame.position) : ""))
DOCUMENT_EDIT(document, "Frame Position", Document::FRAMES, frame->position = useFrame.position);
ImGui::SetItemTooltip("Change the position of the frame.");
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_);
{
if (ImGui::InputFloat2("Pivot", frame ? value_ptr(useFrame.pivot) : &dummy_value<float>(),
frame ? vec2_format_get(useFrame.pivot) : ""))
DOCUMENT_EDIT(document, "Frame Pivot", Document::FRAMES, frame->pivot = useFrame.pivot);
ImGui::SetItemTooltip("Change the pivot of the frame; i.e., where it is centered.");
}
ImGui::EndDisabled();
if (ImGui::InputFloat2("Scale", frame ? value_ptr(useFrame.scale) : &dummy_value<float>(),
frame ? vec2_format_get(useFrame.scale) : ""))
DOCUMENT_EDIT(document, "Frame Scale", Document::FRAMES, frame->scale = useFrame.scale);
ImGui::SetItemTooltip("Change the scale of the frame, in percent.");
if (ImGui::InputFloat("Rotation", frame ? &useFrame.rotation : &dummy_value<float>(), imgui::STEP,
imgui::STEP_FAST, frame ? float_format_get(useFrame.rotation) : ""))
DOCUMENT_EDIT(document, "Frame Rotation", Document::FRAMES, frame->rotation = useFrame.rotation);
ImGui::SetItemTooltip("Change the rotation of the frame.");
if (ImGui::InputInt("Duration", frame ? &useFrame.delay : &dummy_value<int>(), imgui::STEP, imgui::STEP_FAST,
!frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0))
DOCUMENT_EDIT(document, "Frame Duration", Document::FRAMES, frame->delay = useFrame.delay);
ImGui::SetItemTooltip("Change how long the frame lasts.");
if (ImGui::ColorEdit4("Tint", frame ? value_ptr(useFrame.tint) : &dummy_value<float>()))
DOCUMENT_EDIT(document, "Frame Tint", Document::FRAMES, frame->tint = useFrame.tint);
ImGui::SetItemTooltip("Change the tint of the frame.");
if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.colorOffset) : &dummy_value<float>()))
DOCUMENT_EDIT(document, "Frame Color Offset", Document::FRAMES, frame->colorOffset = useFrame.colorOffset);
ImGui::SetItemTooltip("Change the color added onto the frame.");
if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value<bool>()))
DOCUMENT_EDIT(document, "Frame Visibility", Document::FRAMES, frame->isVisible = useFrame.isVisible);
ImGui::SetItemTooltip("Toggle the frame's visibility.");
ImGui::SameLine();
if (ImGui::Checkbox("Interpolated", frame ? &useFrame.isInterpolated : &dummy_value<bool>()))
DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES,
frame->isInterpolated = useFrame.isInterpolated);
ImGui::SetItemTooltip(
"Toggle the frame interpolating; i.e., blending its values into the next frame based on the time.");
auto widgetSize = imgui::widget_size_with_row_get(2);
if (ImGui::Button("Flip X", widgetSize))
DOCUMENT_EDIT(document, "Frame Flip X", Document::FRAMES, frame->scale.x = -frame->scale.x);
ImGui::SetItemTooltip("%s", "Flip the horizontal scale of the frame, to cheat mirroring the frame "
"horizontally.\n(Note: the format does not support mirroring.)");
ImGui::SameLine();
if (ImGui::Button("Flip Y", widgetSize))
DOCUMENT_EDIT(document, "Frame Flip Y", Document::FRAMES, frame->scale.y = -frame->scale.y);
ImGui::SetItemTooltip("%s", "Flip the vertical scale of the frame, to cheat mirroring the frame "
"vertically.\n(Note: the format does not support mirroring.)");
}
}
ImGui::EndDisabled();
}
ImGui::End();
}
}

View File

@@ -0,0 +1,13 @@
#pragma once
#include "manager.h"
#include "settings.h"
namespace anm2ed::imgui
{
class FrameProperties
{
public:
void update(Manager&, Settings&);
};
}

186
src/imgui/window/layers.cpp Normal file
View File

@@ -0,0 +1,186 @@
#include "layers.h"
#include <ranges>
#include "map_.h"
#include "toast.h"
using namespace anm2ed::util;
using namespace anm2ed::resource;
using namespace anm2ed::types;
namespace anm2ed::imgui
{
void Layers::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& reference = document.layer.reference;
auto& unused = document.layer.unused;
auto& hovered = document.layer.hovered;
auto& selection = document.layer.selection;
auto& propertiesPopup = manager.layerPropertiesPopup;
hovered = -1;
if (ImGui::Begin("Layers", &settings.windowIsLayers))
{
auto childSize = size_without_footer_get();
if (ImGui::BeginChild("##Layers Child", childSize, true))
{
selection.start(anm2.content.layers.size());
for (auto& [id, layer] : anm2.content.layers)
{
auto isSelected = selection.contains(id);
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
ImGui::Selectable(std::format(anm2::LAYER_FORMAT, id, layer.name, layer.spritesheetID).c_str(), isSelected);
if (ImGui::IsItemHovered())
{
hovered = id;
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) manager.layer_properties_open(id);
}
else
hovered = -1;
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(layer.name.c_str());
ImGui::PopFont();
ImGui::Text("ID: %d", id);
ImGui::Text("Spritesheet ID: %d", layer.spritesheetID);
ImGui::EndTooltip();
}
ImGui::PopID();
}
selection.finish();
auto copy = [&]()
{
if (!selection.empty())
{
std::string clipboardText{};
for (auto& id : selection)
clipboardText += anm2.content.layers[id].to_string(id);
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.content.layers[hovered].to_string(hovered));
};
auto paste = [&](merge::Type type)
{
std::string errorString{};
document.snapshot("Paste Layer(s)");
if (anm2.layers_deserialize(clipboard.get(), type, &errorString))
document.change(Document::NULLS);
else
toasts.error(std::format("Failed to deserialize layer(s): {}", errorString));
};
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, false);
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, !selection.empty() || hovered > -1)) copy();
if (ImGui::BeginMenu("Paste", !clipboard.is_empty()))
{
if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND);
if (ImGui::MenuItem("Replace")) paste(merge::REPLACE);
ImGui::EndMenu();
}
ImGui::EndPopup();
}
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(2);
shortcut(settings.shortcutAdd);
if (ImGui::Button("Add", widgetSize)) manager.layer_properties_open();
set_item_tooltip_shortcut("Add a layer.", settings.shortcutAdd);
ImGui::SameLine();
shortcut(settings.shortcutRemove);
ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize))
{
auto remove_unused = [&]()
{
for (auto& id : unused)
anm2.content.layers.erase(id);
unused.clear();
};
DOCUMENT_EDIT(document, "Remove Unused Layers", Document::LAYERS, remove_unused());
}
ImGui::EndDisabled();
set_item_tooltip_shortcut("Remove unused layers (i.e., ones not used in any animation.)",
settings.shortcutRemove);
}
ImGui::End();
manager.layer_properties_trigger();
if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto childSize = child_size_get(2);
auto& layer = manager.editLayer;
if (ImGui::BeginChild("Child", childSize, ImGuiChildFlags_Borders))
{
if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere();
input_text_string("Name", &layer.name);
ImGui::SetItemTooltip("Set the item's name.");
combo_negative_one_indexed("Spritesheet", &layer.spritesheetID, document.spritesheet.labels);
ImGui::SetItemTooltip("Set the layer item's spritesheet.");
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(2);
if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize))
{
auto add = [&]()
{
auto id = map::next_id_get(anm2.content.layers);
anm2.content.layers[id] = layer;
selection = {id};
};
auto set = [&]()
{
anm2.content.layers[reference] = layer;
selection = {reference};
};
if (reference == -1)
{
DOCUMENT_EDIT(document, "Add Layer", Document::LAYERS, add());
}
else
DOCUMENT_EDIT(document, "Set Layer Properties", Document::LAYERS, set());
manager.layer_properties_close();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) manager.layer_properties_close();
manager.layer_properties_end();
ImGui::EndPopup();
}
}
}

15
src/imgui/window/layers.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "clipboard.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::imgui
{
class Layers
{
public:
void update(Manager&, Settings&, Resources&, Clipboard&);
};
}

187
src/imgui/window/nulls.cpp Normal file
View File

@@ -0,0 +1,187 @@
#include "nulls.h"
#include <ranges>
#include "map_.h"
#include "toast.h"
using namespace anm2ed::resource;
using namespace anm2ed::util;
using namespace anm2ed::types;
namespace anm2ed::imgui
{
void Nulls::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& reference = document.null.reference;
auto& unused = document.null.unused;
auto& hovered = document.null.hovered;
auto& selection = document.null.selection;
auto& propertiesPopup = manager.nullPropertiesPopup;
hovered = -1;
if (ImGui::Begin("Nulls", &settings.windowIsNulls))
{
auto childSize = size_without_footer_get();
if (ImGui::BeginChild("##Nulls Child", childSize, true))
{
selection.start(anm2.content.nulls.size());
for (auto& [id, null] : anm2.content.nulls)
{
auto isSelected = selection.contains(id);
auto isReferenced = reference == id;
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
ImGui::Selectable(std::format(anm2::NULL_FORMAT, id, null.name).c_str(), isSelected);
if (ImGui::IsItemHovered())
{
hovered = id;
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) manager.null_properties_open(id);
}
if (isReferenced) ImGui::PopFont();
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(null.name.c_str());
ImGui::PopFont();
ImGui::Text("ID: %d", id);
ImGui::EndTooltip();
}
ImGui::PopID();
}
selection.finish();
auto copy = [&]()
{
if (!selection.empty())
{
std::string clipboardText{};
for (auto& id : selection)
clipboardText += anm2.content.nulls[id].to_string(id);
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.content.nulls[hovered].to_string(hovered));
};
auto paste = [&](merge::Type type)
{
std::string errorString{};
document.snapshot("Paste Null(s)");
if (anm2.nulls_deserialize(clipboard.get(), type, &errorString))
document.change(Document::NULLS);
else
toasts.error(std::format("Failed to deserialize null(s): {}", errorString));
};
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, false);
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, selection.empty() || hovered > -1)) copy();
if (ImGui::BeginMenu("Paste", !clipboard.is_empty()))
{
if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND);
if (ImGui::MenuItem("Replace")) paste(merge::REPLACE);
ImGui::EndMenu();
}
ImGui::EndPopup();
}
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(2);
shortcut(settings.shortcutAdd);
if (ImGui::Button("Add", widgetSize)) manager.null_properties_open();
set_item_tooltip_shortcut("Add a null.", settings.shortcutAdd);
ImGui::SameLine();
shortcut(settings.shortcutRemove);
ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize))
{
auto remove_unused = [&]()
{
for (auto& id : unused)
anm2.content.nulls.erase(id);
unused.clear();
};
DOCUMENT_EDIT(document, "Remove Unused Events", Document::EVENTS, remove_unused());
}
ImGui::EndDisabled();
set_item_tooltip_shortcut("Remove unused nulls (i.e., ones not used in any animation.)", settings.shortcutRemove);
}
ImGui::End();
manager.null_properties_trigger();
if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto childSize = child_size_get(2);
auto& null = manager.editNull;
if (ImGui::BeginChild("Child", childSize, ImGuiChildFlags_Borders))
{
if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere();
input_text_string("Name", &null.name);
ImGui::SetItemTooltip("Set the null's name.");
ImGui::Checkbox("Rect", &null.isShowRect);
ImGui::SetItemTooltip("The null will have a rectangle show around it.");
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(2);
if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize))
{
auto add = [&]()
{
auto id = map::next_id_get(anm2.content.nulls);
anm2.content.nulls[id] = null;
selection = {id};
};
auto set = [&]()
{
anm2.content.nulls[reference] = null;
selection = {reference};
};
if (reference == -1)
{
DOCUMENT_EDIT(document, "Add Null", Document::NULLS, add());
}
else
DOCUMENT_EDIT(document, "Set Null Properties", Document::NULLS, set());
manager.null_properties_close();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) manager.null_properties_close();
ImGui::EndPopup();
}
manager.null_properties_end();
}
}

15
src/imgui/window/nulls.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "clipboard.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::imgui
{
class Nulls
{
public:
void update(Manager&, Settings&, Resources&, Clipboard&);
};
}

View File

@@ -0,0 +1,55 @@
#include "onionskin.h"
#include <glm/gtc/type_ptr.hpp>
#include "imgui_.h"
using namespace anm2ed::types;
using namespace glm;
namespace anm2ed::imgui
{
constexpr auto FRAMES_MAX = 100;
void Onionskin::update(Settings& settings)
{
auto& isEnabled = settings.onionskinIsEnabled;
auto& beforeCount = settings.onionskinBeforeCount;
auto& beforeColor = settings.onionskinBeforeColor;
auto& afterCount = settings.onionskinAfterCount;
auto& afterColor = settings.onionskinAfterColor;
auto& drawOrder = settings.onionskinDrawOrder;
if (ImGui::Begin("Onionskin", &settings.windowIsOnionskin))
{
auto configure_widgets = [&](const char* separator, int& frames, vec3& color)
{
ImGui::PushID(separator);
ImGui::SeparatorText(separator);
input_int_range("Frames", frames, 0, FRAMES_MAX);
ImGui::SetItemTooltip("Change the amount of frames this onionskin will use.");
ImGui::ColorEdit3("Color", value_ptr(color));
ImGui::SetItemTooltip("Change the color of the frames this onionskin will use.");
ImGui::PopID();
};
ImGui::Checkbox("Enabled", &isEnabled);
set_item_tooltip_shortcut("Toggle onionskinning.", settings.shortcutOnionskin);
configure_widgets("Before", beforeCount, beforeColor);
configure_widgets("After", afterCount, afterColor);
ImGui::Text("Draw Order");
ImGui::SameLine();
ImGui::RadioButton("Below", &drawOrder, draw_order::BELOW);
ImGui::SetItemTooltip("The onionskin frames will draw below the original frames.");
ImGui::SameLine();
ImGui::RadioButton("Above", &drawOrder, draw_order::ABOVE);
ImGui::SetItemTooltip("The onionskin frames will draw above the original frames.");
}
ImGui::End();
if (shortcut(settings.shortcutOnionskin, shortcut::GLOBAL)) isEnabled = !isEnabled;
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "settings.h"
namespace anm2ed::imgui
{
class Onionskin
{
public:
void update(Settings&);
};
}

148
src/imgui/window/sounds.cpp Normal file
View File

@@ -0,0 +1,148 @@
#include "sounds.h"
#include <ranges>
#include "toast.h"
using namespace anm2ed::dialog;
using namespace anm2ed::types;
using namespace anm2ed::resource;
namespace anm2ed::imgui
{
void Sounds::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& reference = document.sound.reference;
auto& unused = document.sound.unused;
auto& hovered = document.null.hovered;
auto& selection = document.sound.selection;
hovered = -1;
if (ImGui::Begin("Sounds", &settings.windowIsSounds))
{
auto childSize = imgui::size_without_footer_get();
if (ImGui::BeginChild("##Sounds Child", childSize, true))
{
selection.start(anm2.content.sounds.size());
for (auto& [id, sound] : anm2.content.sounds)
{
auto isSelected = selection.contains(id);
auto isReferenced = reference == id;
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
if (ImGui::Selectable(sound.path.c_str(), isSelected)) sound.play();
if (ImGui::IsItemHovered()) hovered = id;
if (isReferenced) ImGui::PopFont();
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(sound.path.c_str());
ImGui::PopFont();
ImGui::Text("ID: %d", id);
ImGui::Text("Click to play.");
ImGui::EndTooltip();
}
ImGui::PopID();
}
selection.finish();
auto copy = [&]()
{
if (!selection.empty())
{
std::string clipboardText{};
for (auto& id : selection)
clipboardText += anm2.content.sounds[id].to_string(id);
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.content.sounds[hovered].to_string(hovered));
};
auto paste = [&](merge::Type type)
{
std::string errorString{};
document.snapshot("Paste Sound(s)");
if (anm2.sounds_deserialize(clipboard.get(), document.directory_get(), type, &errorString))
document.change(Document::SOUNDS);
else
toasts.error(std::format("Failed to deserialize sound(s): {}", errorString));
};
if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, false);
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), !selection.empty() && hovered > -1)) copy();
if (ImGui::BeginMenu("Paste", !clipboard.is_empty()))
{
if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND);
if (ImGui::MenuItem("Replace")) paste(merge::REPLACE);
ImGui::EndMenu();
}
ImGui::EndPopup();
}
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
imgui::shortcut(settings.shortcutAdd);
if (ImGui::Button("Add", widgetSize)) dialog.file_open(dialog::SOUND_OPEN);
imgui::set_item_tooltip_shortcut("Add a sound.", settings.shortcutAdd);
ImGui::SameLine();
imgui::shortcut(settings.shortcutRemove);
ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize))
{
auto remove_unused = [&]()
{
for (auto& id : unused)
anm2.content.sounds.erase(id);
unused.clear();
};
DOCUMENT_EDIT(document, "Remove Unused Sounds", Document::SOUNDS, remove_unused());
};
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Remove unused sounds (i.e., ones not used in any trigger.)",
settings.shortcutRemove);
}
ImGui::End();
if (dialog.is_selected(dialog::SOUND_OPEN))
{
auto add = [&]()
{
int id{};
if (anm2.sound_add(document.directory_get(), dialog.path, id))
{
selection = {id};
toasts.info(std::format("Initialized sound #{}: {}", id, dialog.path));
}
else
toasts.error(std::format("Failed to initialize sound: {}", dialog.path));
};
DOCUMENT_EDIT(document, "Add Sound", Document::SOUNDS, add());
dialog.reset();
}
}
}

16
src/imgui/window/sounds.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "clipboard.h"
#include "dialog.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::imgui
{
class Sounds
{
public:
void update(Manager&, Settings&, Resources&, Dialog&, Clipboard&);
};
}

Some files were not shown because too many files have changed in this diff Show More