Compare commits
117 Commits
cf9f04ecdc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 24f98fe0a0 | |||
| ca5eb54177 | |||
| f5c5161f57 | |||
| 1672570fa1 | |||
| 79f9d9a92a | |||
| 8c198d97e5 | |||
| 193c434137 | |||
| c4b4e78fe2 | |||
| cef4af5c1e | |||
| d81e68a925 | |||
| 10e2e70244 | |||
| 086697b0dc | |||
| 70aa3ffe06 | |||
| f6a5c01d74 | |||
| 63c170adbb | |||
| 294ccd4884 | |||
| 04cb191c42 | |||
| 300e322b9d | |||
| d0221928aa | |||
| 412d860a7d | |||
| d1e0b14561 | |||
| 009b285baa | |||
| ab65eb7c58 | |||
| ca719d7dfb | |||
| 2b356cbabf | |||
| 6c6660d7c4 | |||
| 631c9c02fa | |||
| f44ce80bc0 | |||
| fbcf79da80 | |||
| e655cdf6d3 | |||
| 2259a411a6 | |||
| d810ce70ea | |||
| be772481f6 | |||
| 34948292ae | |||
| 01d0f6e49a | |||
| 61e38a1482 | |||
| b207a78a1f | |||
| 25e902f4ea | |||
| d414375d82 | |||
| f462692bb1 | |||
| 131934df1a | |||
| a968371ac6 | |||
| 408d50b6ce | |||
| 9846a514ad | |||
| b9f195ab89 | |||
| cbbf81b739 | |||
| 4b6fa7153d | |||
| 5470368b6a | |||
| 911085ef47 | |||
| 82d856fd94 | |||
| c6b6b1e682 | |||
| 54593e9b6e | |||
| defddd9de8 | |||
| a048a37b42 | |||
| a7c0698a93 | |||
| 9159ffdb02 | |||
| 352594e1e3 | |||
| 9af0135a3a | |||
| 7abb0fcda4 | |||
| c57c32aca8 | |||
| 51bf4c2012 | |||
| bb6b68311b | |||
| e4cb0056a0 | |||
| 431ffebd8d | |||
| 1b21080fe2 | |||
| c0ace90b5a | |||
| 57b2e3aa4c | |||
| 7de716a6f2 | |||
| 38b92a588f | |||
| ba08ed7a60 | |||
| a1f6bda59d | |||
| d7c481a20b | |||
| 7e1b8c8b04 | |||
| 2202cdff3a | |||
| 0ce567c282 | |||
| 7ab7ed3ee2 | |||
| a2a33e471d | |||
| 24465f29c5 | |||
| 2ce46b4de4 | |||
| ec45bb4e62 | |||
| 3f666ab18a | |||
| 0f5dae7ef8 | |||
| 21fa89c35d | |||
| 517a8262a6 | |||
| 993f92e412 | |||
| 96dd0fbc50 | |||
| 9eb0bfffce | |||
| a01b5ec4eb | |||
| bff73c8936 | |||
| 7733cb2883 | |||
| 217179e9af | |||
| ca9e8d92d9 | |||
| 9f36324a58 | |||
| a72d663d80 | |||
| d9a05947c0 | |||
| 2a671e2623 | |||
| 07096c487b | |||
| d07b4dc2eb | |||
| 4e5ee603f2 | |||
| 48d873230b | |||
| e2799b1e58 | |||
| 1e35910b0a | |||
| 62cd94ca78 | |||
| 99b7d9f49d | |||
| 729d5fb216 | |||
| dd3aeae6d2 | |||
| fe9366f9ef | |||
| 87c2db2a77 | |||
| 5b0f9a39c4 | |||
| 7f07eaa128 | |||
| 8039df667c | |||
| 62c5dcf6ea | |||
| dbb6d13f34 | |||
| 9a6810d1bb | |||
| 9fb6366d7c | |||
| f49eaa6a37 | |||
| 3e22f8eb9d |
11
.clang-format
Normal file
11
.clang-format
Normal 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
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,8 +4,7 @@ release/
|
|||||||
packed/
|
packed/
|
||||||
vcpkg_installed/
|
vcpkg_installed/
|
||||||
out/
|
out/
|
||||||
include/imgui/
|
|
||||||
include/glm/
|
|
||||||
include/tinyxml2
|
|
||||||
workshop/resources
|
workshop/resources
|
||||||
|
cmake-build-debug/
|
||||||
.vs/
|
.vs/
|
||||||
|
.idea/
|
||||||
|
|||||||
23
.gitmodules
vendored
23
.gitmodules
vendored
@@ -1,10 +1,19 @@
|
|||||||
[submodule "include/imgui"]
|
[submodule "external/glm"]
|
||||||
path = include/imgui
|
path = external/glm
|
||||||
|
url = https://github.com/g-truc/glm
|
||||||
|
[submodule "external/imgui"]
|
||||||
|
path = external/imgui
|
||||||
url = https://github.com/ocornut/imgui
|
url = https://github.com/ocornut/imgui
|
||||||
branch = docking
|
branch = docking
|
||||||
[submodule "include/glm"]
|
[submodule "external/tinyxml2"]
|
||||||
path = include/glm
|
path = external/tinyxml2
|
||||||
url = https://github.com/g-truc/glm
|
|
||||||
[submodule "include/tinyxml2"]
|
|
||||||
path = include/tinyxml2
|
|
||||||
url = https://github.com/leethomason/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
|
||||||
|
|||||||
51
.vscode/launch.json
vendored
Normal file
51
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Release",
|
||||||
|
"type": "cppdbg",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/build-release/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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
26
.vscode/tasks.json
vendored
Normal file
26
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "cmake --build build --target anm2ed",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": [
|
||||||
|
"$gcc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "build-release",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release && cmake --build build-release --target anm2ed",
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": [
|
||||||
|
"$gcc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
244
CMakeLists.txt
244
CMakeLists.txt
@@ -1,35 +1,103 @@
|
|||||||
cmake_minimum_required(VERSION 3.15)
|
cmake_minimum_required(VERSION 3.27)
|
||||||
|
project(anm2ed LANGUAGES C CXX)
|
||||||
|
|
||||||
if(WIN32 AND DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
|
if (WIN32 AND DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
|
||||||
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
|
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
|
||||||
CACHE STRING "Vcpkg toolchain file")
|
CACHE STRING "Vcpkg toolchain file")
|
||||||
endif()
|
endif ()
|
||||||
|
|
||||||
project(anm2ed CXX)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
if (CMAKE_EXPORT_COMPILE_COMMANDS AND NOT WIN32)
|
||||||
|
execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
|
||||||
|
${CMAKE_BINARY_DIR}/compile_commands.json
|
||||||
|
${CMAKE_SOURCE_DIR}/compile_commands.json
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
|
||||||
find_package(SDL3 REQUIRED)
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
find_package(OpenGL REQUIRED)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
set(GLAD_SRC
|
if (MSVC)
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp
|
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||||
)
|
endif ()
|
||||||
|
|
||||||
|
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||||
|
|
||||||
|
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||||
|
if (NOT WIN32)
|
||||||
|
set(SDL_ALSA ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_ALSA_SHARED ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_JACK ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_JACK_SHARED ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_PIPEWIRE ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_PIPEWIRE_SHARED ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_SNDIO ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_SNDIO_SHARED ON CACHE BOOL "" FORCE)
|
||||||
|
endif ()
|
||||||
|
set(SDL_HAPTIC OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_JOYSTICK 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)
|
||||||
|
set(SDL_VULKAN OFF CACHE BOOL "" FORCE)
|
||||||
|
add_subdirectory(external/SDL EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
|
set(SDLMIXER_DEPS_SHARED OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_AIFF OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_AU OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_FLAC_LIBFLAC OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_FLAC_DRFLAC OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_GME OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_MOD_XMP OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_MP3_DRMP3 OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_MP3_MPG123 OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_MIDI_FLUIDSYNTH OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_MIDI_TIMIDITY OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_OPUS ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_VOC OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_VORBIS_STB ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_VORBIS_VORBISFILE OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_VORBIS_TREMOR OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDLMIXER_WAVE ON 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
|
set(IMGUI_SRC
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/imgui.cpp
|
external/imgui/imgui.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/imgui_draw.cpp
|
external/imgui/imgui_draw.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/imgui_widgets.cpp
|
external/imgui/imgui_widgets.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/imgui_tables.cpp
|
external/imgui/imgui_tables.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/backends/imgui_impl_sdl3.cpp
|
external/imgui/backends/imgui_impl_sdl3.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui/backends/imgui_impl_opengl3.cpp
|
external/imgui/backends/imgui_impl_opengl3.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(TINYXML2_SRC
|
set(TINYXML2_SRC external/tinyxml2/tinyxml2.cpp)
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/tinyxml2/tinyxml2.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
file(GLOB PROJECT_SRC
|
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp
|
src/anm2/*.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/*.h
|
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}
|
add_executable(${PROJECT_NAME}
|
||||||
@@ -38,38 +106,116 @@ add_executable(${PROJECT_NAME}
|
|||||||
${TINYXML2_SRC}
|
${TINYXML2_SRC}
|
||||||
${PROJECT_SRC}
|
${PROJECT_SRC}
|
||||||
)
|
)
|
||||||
|
if (MSVC)
|
||||||
if(WIN32)
|
target_compile_options(${PROJECT_NAME} PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:/std:c++latest>")
|
||||||
enable_language(RC)
|
|
||||||
target_sources(${PROJECT_NAME} PRIVATE assets/Icon.rc)
|
|
||||||
set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE TRUE)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
|
if (WIN32)
|
||||||
|
enable_language(RC)
|
||||||
|
target_sources(${PROJECT_NAME} PRIVATE anm2ed.rc)
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE /EHsc)
|
||||||
|
target_link_options(${PROJECT_NAME} PRIVATE /STACK:0xffffff)
|
||||||
|
target_link_options(${PROJECT_NAME} PRIVATE
|
||||||
|
"$<$<AND:$<CONFIG:Release>,$<CXX_COMPILER_ID:MSVC>>:/SUBSYSTEM:WINDOWS>"
|
||||||
|
"$<$<AND:$<CONFIG:Release>,$<NOT:$<CXX_COMPILER_ID:MSVC>>>:-mwindows>"
|
||||||
|
)
|
||||||
|
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_include_directories(${PROJECT_NAME} PRIVATE
|
target_link_libraries(${PROJECT_NAME} PRIVATE m)
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
endif ()
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/glad
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/imgui
|
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/tinyxml2
|
IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
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_link_libraries(${PROJECT_NAME} PRIVATE OpenGL::GL SDL3::SDL3)
|
target_include_directories(${PROJECT_NAME} PRIVATE
|
||||||
|
external
|
||||||
|
external/imgui
|
||||||
|
external/glm
|
||||||
|
external/tinyxml2
|
||||||
|
external/lunasvg
|
||||||
|
external/SDL
|
||||||
|
external/SDL_mixer
|
||||||
|
external/SDL_mixer/include
|
||||||
|
include
|
||||||
|
include/glad
|
||||||
|
src
|
||||||
|
src/imgui
|
||||||
|
src/resource
|
||||||
|
src/util
|
||||||
|
)
|
||||||
|
|
||||||
message("System: ${CMAKE_SYSTEM_NAME}")
|
if (WIN32)
|
||||||
message("Project: ${PROJECT_NAME}")
|
set(OPENGL_LIB opengl32)
|
||||||
message("Build: ${CMAKE_BUILD_TYPE}")
|
set(PLATFORM_LIBS
|
||||||
|
ws2_32
|
||||||
|
bcrypt
|
||||||
|
imm32
|
||||||
|
version
|
||||||
|
winmm
|
||||||
|
shell32
|
||||||
|
ole32
|
||||||
|
advapi32
|
||||||
|
gdi32
|
||||||
|
user32
|
||||||
|
setupapi
|
||||||
|
legacy_stdio_definitions
|
||||||
|
)
|
||||||
|
if (TARGET SDL3::SDL3main)
|
||||||
|
set(SDL_MAIN_TARGET SDL3::SDL3main)
|
||||||
|
else ()
|
||||||
|
set(SDL_MAIN_TARGET)
|
||||||
|
endif ()
|
||||||
|
else ()
|
||||||
|
set(OPENGL_LIB GL)
|
||||||
|
set(PLATFORM_LIBS)
|
||||||
|
set(SDL_MAIN_TARGET)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||||
|
${OPENGL_LIB}
|
||||||
|
SDL3-static
|
||||||
|
${SDL_MAIN_TARGET}
|
||||||
|
SDL3_mixer::SDL3_mixer
|
||||||
|
lunasvg
|
||||||
|
${PLATFORM_LIBS}
|
||||||
|
)
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "x64-Debug",
|
"name": "x64-Debug",
|
||||||
"generator": "Ninja",
|
"generator": "Ninja",
|
||||||
"configurationType": "Debug",
|
"configurationType": "Debug",
|
||||||
"inheritEnvironments": [ "msvc_x64_x64" ],
|
"inheritEnvironments": [
|
||||||
"buildRoot": "${projectDir}\\build\\${name}",
|
"msvc_x64_x64"
|
||||||
"installRoot": "${projectDir}\\install\\${name}",
|
],
|
||||||
"cmakeCommandArgs": "",
|
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||||
"buildCommandArgs": "",
|
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||||
"ctestCommandArgs": ""
|
"cmakeCommandArgs": "-DCMAKE_BUILD_TYPE=\"Debug\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "x64-Release",
|
"name": "x64-Release",
|
||||||
"generator": "Ninja",
|
"generator": "Ninja",
|
||||||
"configurationType": "Release",
|
"configurationType": "Release",
|
||||||
"inheritEnvironments": [ "msvc_x64_x64" ],
|
"inheritEnvironments": [
|
||||||
"buildRoot": "${projectDir}\\build\\${name}",
|
"msvc_x64_x64"
|
||||||
"installRoot": "${projectDir}\\install\\${name}",
|
],
|
||||||
"cmakeCommandArgs": "",
|
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||||
"buildCommandArgs": "",
|
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||||
"ctestCommandArgs": ""
|
"cmakeCommandArgs": "-DCMAKE_BUILD_TYPE=\"Release\""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
39
README.md
39
README.md
@@ -1,48 +1,37 @@
|
|||||||
# Anm2Ed
|
# Anm2Ed
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
A reimplementation of *The Binding of Isaac: Rebirth*'s proprietary animation editor. Manipulates the XML-based ".anm2" format, used for in-game tweened animations.
|
A reimplementation of *The Binding of Isaac: Rebirth*'s proprietary animation editor. Manipulates the XML-based ".anm2" format, used for in-game tweened animations.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Extended version of the original proprietary Nicalis animation editor
|
- Extended version of the original proprietary Nicalis animation editor
|
||||||
- Smooth [Dear ImGui](https://github.com/ocornut/imgui) interface; docking, dragging and dropping, etc. You might be familiar with it from You might be familiar with it from (Repentogon)
|
- Smooth [Dear ImGui](https://github.com/ocornut/imgui) interface; docking, dragging and dropping, etc. You might be familiar with it from [Repentogon](https://repentogon.com/).
|
||||||
|
- Greatly polished in many areas, to assist with all your animating needs
|
||||||
- New features
|
- New features
|
||||||
|
- Additional hotkeys/shortcuts (rebindable!)
|
||||||
|
- Additional wizard options
|
||||||
|
- Broadened sound support
|
||||||
- Can output .webm, .mp4 or *.png sequence (wih FFmpeg)
|
- Can output .webm, .mp4 or *.png sequence (wih FFmpeg)
|
||||||
- Cutting, copying and pasting
|
- Cutting, copying and pasting
|
||||||
- Additional wizard options
|
|
||||||
- Robust snapshot (undo/redo) system
|
|
||||||
- Additional hotkeys/shortcuts (rebindable!)
|
|
||||||
- Onionskinning
|
- Onionskinning
|
||||||
|
- Robust snapshot (undo/redo) system
|
||||||
|
- Selecting multiple frames at a time
|
||||||
- Settings that will preserve on exit (stored in %APPDATA% on Windows or ~/.local/share on Linux)
|
- Settings that will preserve on exit (stored in %APPDATA% on Windows or ~/.local/share on Linux)
|
||||||
|
|
||||||
### Note: Difference from Nicalis editor
|
|
||||||
Layers/nulls are handled differently (in their own window) and are not on the timeline! Add a layer/null in the Layers/Nulls window, and then add a LayerAnimation/NullAnimation in the timeline!
|
|
||||||
|
|
||||||
### Note: Rendering Animations
|
### Note: Rendering Animations
|
||||||
You will need FFmpeg installed! Get it from [here](https://ffmpeg.org/download.html), and point to the downloaded ffmpeg executable within the program!
|
You will need FFmpeg installed to output GIF/video! Get it from [here](https://ffmpeg.org/download.html), and point to the downloaded ffmpeg executable within the program!
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
Download these from your package manager:
|
|
||||||
- SDL3
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
Visual Studio is recommended; make sure your installation has "Desktop development with C++" and ".NET desktop development" workloads.
|
|
||||||
|
|
||||||
Install and configure [vcpkg](https://vcpkg.io/en/).
|
|
||||||
|
|
||||||
Build should be straightforward from there.
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
After cloning and enter the repository's directory, make sure to initialize the submodules:
|
After cloning and enter the repository's directory, make sure to initialize the submodules:
|
||||||
|
|
||||||
```git submodule update --init```
|
```git submodule update --init --recursive```
|
||||||
|
|
||||||
Then:
|
### Windows
|
||||||
|
Visual Studio is recommended for build.
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
```
|
```
|
||||||
mkdir build
|
mkdir build
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
IDI_ICON1 ICON DISCARDABLE "Icon.ico"
|
|
||||||
BIN
assets/atlas.png
BIN
assets/atlas.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -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
1
compile_commands.json
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/home/anon/sda/Personal/Repos/anm2ed/build/compile_commands.json
|
||||||
1
external/SDL
vendored
Submodule
1
external/SDL
vendored
Submodule
Submodule external/SDL added at 25ab8c99df
1
external/SDL_mixer
vendored
Submodule
1
external/SDL_mixer
vendored
Submodule
Submodule external/SDL_mixer added at 4182794ea4
1
external/glm
vendored
Submodule
1
external/glm
vendored
Submodule
Submodule external/glm added at a583c59e16
1
external/imgui
vendored
Submodule
1
external/imgui
vendored
Submodule
Submodule external/imgui added at d4c156a0f0
1
external/lunasvg
vendored
Submodule
1
external/lunasvg
vendored
Submodule
Submodule external/lunasvg added at d925755c3d
1
external/tinyxml2
vendored
Submodule
1
external/tinyxml2
vendored
Submodule
Submodule external/tinyxml2 added at 36ff404c34
Submodule include/glm deleted from 2d4c4b4dd3
Submodule include/imgui deleted from d896eab166
3121
include/nanosvg.h
Normal file
3121
include/nanosvg.h
Normal file
File diff suppressed because it is too large
Load Diff
1461
include/nanosvgrast.h
Normal file
1461
include/nanosvgrast.h
Normal file
File diff suppressed because it is too large
Load Diff
Submodule include/tinyxml2 deleted from e6caeae857
395
src/COMMON.h
395
src/COMMON.h
@@ -1,395 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#define FLOAT_FORMAT_MAX_DECIMALS 9
|
|
||||||
#define FLOAT_FORMAT_EPSILON 1e-9f
|
|
||||||
static constexpr f32 FLOAT_FORMAT_POW10[] = {
|
|
||||||
1.f,
|
|
||||||
10.f,
|
|
||||||
100.f,
|
|
||||||
1000.f,
|
|
||||||
10000.f,
|
|
||||||
100000.f,
|
|
||||||
1000000.f,
|
|
||||||
10000000.f,
|
|
||||||
100000000.f,
|
|
||||||
1000000000.f
|
|
||||||
};
|
|
||||||
|
|
||||||
static inline s32 f32_decimals_needed(f32 value)
|
|
||||||
{
|
|
||||||
f32 integerPart = 0.f;
|
|
||||||
f32 fractionalPart = modff(value, &integerPart);
|
|
||||||
fractionalPart = fabsf(fractionalPart);
|
|
||||||
|
|
||||||
if (fractionalPart < FLOAT_FORMAT_EPSILON)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
for (s32 decimalCount = 1; decimalCount <= FLOAT_FORMAT_MAX_DECIMALS; ++decimalCount)
|
|
||||||
{
|
|
||||||
f32 scaledFraction = fractionalPart * FLOAT_FORMAT_POW10[decimalCount];
|
|
||||||
if (fabsf(scaledFraction - roundf(scaledFraction)) <
|
|
||||||
FLOAT_FORMAT_EPSILON * FLOAT_FORMAT_POW10[decimalCount])
|
|
||||||
{
|
|
||||||
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, _] : map)
|
|
||||||
if (key != id)
|
|
||||||
break;
|
|
||||||
else
|
|
||||||
++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
|
|
||||||
};
|
|
||||||
347
src/PACKED.h
347
src/PACKED.h
@@ -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}
|
|
||||||
};
|
|
||||||
1233
src/anm2.cpp
1233
src/anm2.cpp
File diff suppressed because it is too large
Load Diff
309
src/anm2.h
309
src/anm2.h
@@ -1,309 +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_FRAMES,
|
|
||||||
ANM2_MERGE_REPLACE_FRAMES,
|
|
||||||
ANM2_MERGE_PREPEND_FRAMES,
|
|
||||||
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_created_on_set(Anm2* self);
|
|
||||||
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);
|
|
||||||
182
src/anm2/animation.cpp
Normal file
182
src/anm2/animation.cpp
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#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.emplace(id, Item(child, LAYER, &id));
|
||||||
|
layerOrder.emplace_back(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
|
||||||
|
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
|
||||||
|
child = child->NextSiblingElement("NullAnimation"))
|
||||||
|
nullAnimations.emplace(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Animation::fit_length() { frameNum = 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};
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/anm2/animation.h
Normal file
37
src/anm2/animation.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "item.h"
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
constexpr auto FRAME_NUM_MIN = 1;
|
||||||
|
constexpr auto FRAME_NUM_MAX = FRAME_DURATION_MAX;
|
||||||
|
|
||||||
|
class Animation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::string name{};
|
||||||
|
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();
|
||||||
|
void fit_length();
|
||||||
|
glm::vec4 rect(bool);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
43
src/anm2/animations.cpp
Normal file
43
src/anm2/animations.cpp
Normal 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
20
src/anm2/animations.h
Normal 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();
|
||||||
|
};
|
||||||
|
}
|
||||||
333
src/anm2/anm2.cpp
Normal file
333
src/anm2/anm2.cpp
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
#include "anm2.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "filesystem_.h"
|
||||||
|
#include "map_.h"
|
||||||
|
#include "time_.h"
|
||||||
|
#include "vector_.h"
|
||||||
|
#include "xml_.h"
|
||||||
|
|
||||||
|
using namespace tinyxml2;
|
||||||
|
using namespace anm2ed::types;
|
||||||
|
using namespace anm2ed::util;
|
||||||
|
using namespace glm;
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
Anm2::Anm2() { info.createdOn = time::get("%m/%d/%Y %I:%M:%S %p"); }
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Anm2::serialize(const std::string& path, std::string* errorString)
|
||||||
|
{
|
||||||
|
XMLDocument document;
|
||||||
|
document.InsertFirstChild(to_element(document));
|
||||||
|
|
||||||
|
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
|
||||||
|
{
|
||||||
|
if (errorString) *errorString = document.ErrorStr();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(int animationIndex, Type itemType, int frameIndex, int itemID)
|
||||||
|
{
|
||||||
|
if (auto item = item_get(animationIndex, itemType, itemID); item)
|
||||||
|
if (vector::in_bounds(item->frames, frameIndex)) return &item->frames[frameIndex];
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Anm2::merge(const Anm2& source, const std::filesystem::path& destinationDirectory,
|
||||||
|
const std::filesystem::path& sourceDirectory)
|
||||||
|
{
|
||||||
|
using util::map::next_id_get;
|
||||||
|
|
||||||
|
auto remap_path = [&](const std::filesystem::path& original) -> std::filesystem::path
|
||||||
|
{
|
||||||
|
if (destinationDirectory.empty()) return original;
|
||||||
|
std::error_code ec{};
|
||||||
|
std::filesystem::path absolute{};
|
||||||
|
bool hasAbsolute = false;
|
||||||
|
|
||||||
|
if (!original.empty())
|
||||||
|
{
|
||||||
|
if (original.is_absolute())
|
||||||
|
{
|
||||||
|
absolute = original;
|
||||||
|
hasAbsolute = true;
|
||||||
|
}
|
||||||
|
else if (!sourceDirectory.empty())
|
||||||
|
{
|
||||||
|
absolute = std::filesystem::weakly_canonical(sourceDirectory / original, ec);
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
ec.clear();
|
||||||
|
absolute = sourceDirectory / original;
|
||||||
|
}
|
||||||
|
hasAbsolute = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAbsolute) return original;
|
||||||
|
|
||||||
|
auto relative = std::filesystem::relative(absolute, destinationDirectory, ec);
|
||||||
|
if (!ec) return relative;
|
||||||
|
ec.clear();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return std::filesystem::relative(absolute, destinationDirectory);
|
||||||
|
}
|
||||||
|
catch (const std::filesystem::filesystem_error&)
|
||||||
|
{
|
||||||
|
return original.empty() ? absolute : original;
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto remap_id = [](const auto& table, int value)
|
||||||
|
{
|
||||||
|
if (value < 0) return value;
|
||||||
|
if (auto it = table.find(value); it != table.end()) return it->second;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<int, int> spritesheetRemap{};
|
||||||
|
std::unordered_map<int, int> layerRemap{};
|
||||||
|
std::unordered_map<int, int> nullRemap{};
|
||||||
|
std::unordered_map<int, int> eventRemap{};
|
||||||
|
std::unordered_map<int, int> soundRemap{};
|
||||||
|
|
||||||
|
// Spritesheets
|
||||||
|
for (auto& [sourceID, sprite] : source.content.spritesheets)
|
||||||
|
{
|
||||||
|
auto sheet = sprite;
|
||||||
|
sheet.path = remap_path(sheet.path);
|
||||||
|
if (!destinationDirectory.empty() && !sheet.path.empty()) sheet.reload(destinationDirectory);
|
||||||
|
|
||||||
|
int destinationID = next_id_get(content.spritesheets);
|
||||||
|
content.spritesheets[destinationID] = std::move(sheet);
|
||||||
|
spritesheetRemap[sourceID] = destinationID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sounds
|
||||||
|
for (auto& [sourceID, soundEntry] : source.content.sounds)
|
||||||
|
{
|
||||||
|
auto sound = soundEntry;
|
||||||
|
sound.path = remap_path(sound.path);
|
||||||
|
if (!destinationDirectory.empty() && !sound.path.empty()) sound.reload(destinationDirectory);
|
||||||
|
|
||||||
|
int destinationID = -1;
|
||||||
|
for (auto& [id, existing] : content.sounds)
|
||||||
|
if (existing.path == sound.path)
|
||||||
|
{
|
||||||
|
destinationID = id;
|
||||||
|
existing = sound;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationID == -1)
|
||||||
|
{
|
||||||
|
destinationID = next_id_get(content.sounds);
|
||||||
|
content.sounds[destinationID] = sound;
|
||||||
|
}
|
||||||
|
soundRemap[sourceID] = destinationID;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto find_by_name = [](auto& container, const std::string& name) -> int
|
||||||
|
{
|
||||||
|
for (auto& [id, value] : container)
|
||||||
|
if (value.name == name) return id;
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Layers
|
||||||
|
for (auto& [sourceID, sourceLayer] : source.content.layers)
|
||||||
|
{
|
||||||
|
auto layer = sourceLayer;
|
||||||
|
layer.spritesheetID = remap_id(spritesheetRemap, layer.spritesheetID);
|
||||||
|
|
||||||
|
int destinationID = find_by_name(content.layers, layer.name);
|
||||||
|
if (destinationID != -1)
|
||||||
|
content.layers[destinationID] = layer;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destinationID = next_id_get(content.layers);
|
||||||
|
content.layers[destinationID] = layer;
|
||||||
|
}
|
||||||
|
layerRemap[sourceID] = destinationID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nulls
|
||||||
|
for (auto& [sourceID, sourceNull] : source.content.nulls)
|
||||||
|
{
|
||||||
|
auto null = sourceNull;
|
||||||
|
int destinationID = find_by_name(content.nulls, null.name);
|
||||||
|
if (destinationID != -1)
|
||||||
|
content.nulls[destinationID] = null;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destinationID = next_id_get(content.nulls);
|
||||||
|
content.nulls[destinationID] = null;
|
||||||
|
}
|
||||||
|
nullRemap[sourceID] = destinationID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
for (auto& [sourceID, sourceEvent] : source.content.events)
|
||||||
|
{
|
||||||
|
auto event = sourceEvent;
|
||||||
|
event.soundID = remap_id(soundRemap, event.soundID);
|
||||||
|
|
||||||
|
int destinationID = find_by_name(content.events, event.name);
|
||||||
|
if (destinationID != -1)
|
||||||
|
content.events[destinationID] = event;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destinationID = next_id_get(content.events);
|
||||||
|
content.events[destinationID] = event;
|
||||||
|
}
|
||||||
|
eventRemap[sourceID] = destinationID;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto remap_item = [&](Item& item)
|
||||||
|
{
|
||||||
|
for (auto& frame : item.frames)
|
||||||
|
{
|
||||||
|
frame.soundID = remap_id(soundRemap, frame.soundID);
|
||||||
|
frame.eventID = remap_id(eventRemap, frame.eventID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto build_animation = [&](const Animation& incoming) -> Animation
|
||||||
|
{
|
||||||
|
Animation remapped{};
|
||||||
|
remapped.name = incoming.name;
|
||||||
|
remapped.frameNum = incoming.frameNum;
|
||||||
|
remapped.isLoop = incoming.isLoop;
|
||||||
|
remapped.rootAnimation = incoming.rootAnimation;
|
||||||
|
remapped.triggers = incoming.triggers;
|
||||||
|
remap_item(remapped.rootAnimation);
|
||||||
|
remap_item(remapped.triggers);
|
||||||
|
|
||||||
|
for (auto layerID : incoming.layerOrder)
|
||||||
|
{
|
||||||
|
auto mapped = remap_id(layerRemap, layerID);
|
||||||
|
if (mapped >= 0 &&
|
||||||
|
std::find(remapped.layerOrder.begin(), remapped.layerOrder.end(), mapped) == remapped.layerOrder.end())
|
||||||
|
remapped.layerOrder.push_back(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [layerID, item] : incoming.layerAnimations)
|
||||||
|
{
|
||||||
|
auto mapped = remap_id(layerRemap, layerID);
|
||||||
|
if (mapped < 0) continue;
|
||||||
|
auto copy = item;
|
||||||
|
remap_item(copy);
|
||||||
|
remapped.layerAnimations[mapped] = std::move(copy);
|
||||||
|
if (std::find(remapped.layerOrder.begin(), remapped.layerOrder.end(), mapped) == remapped.layerOrder.end())
|
||||||
|
remapped.layerOrder.push_back(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [nullID, item] : incoming.nullAnimations)
|
||||||
|
{
|
||||||
|
auto mapped = remap_id(nullRemap, nullID);
|
||||||
|
if (mapped < 0) continue;
|
||||||
|
auto copy = item;
|
||||||
|
remap_item(copy);
|
||||||
|
remapped.nullAnimations[mapped] = std::move(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
remap_item(remapped.triggers);
|
||||||
|
return remapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto find_animation = [&](const std::string& name) -> Animation*
|
||||||
|
{
|
||||||
|
for (auto& animation : animations.items)
|
||||||
|
if (animation.name == name) return &animation;
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto merge_item_map = [&](auto& destination, const auto& incoming)
|
||||||
|
{
|
||||||
|
for (auto& [id, item] : incoming)
|
||||||
|
{
|
||||||
|
if (!item.frames.empty())
|
||||||
|
destination[id] = item;
|
||||||
|
else if (!destination.contains(id))
|
||||||
|
destination[id] = item;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto& animation : source.animations.items)
|
||||||
|
{
|
||||||
|
auto processed = build_animation(animation);
|
||||||
|
if (auto destination = find_animation(processed.name))
|
||||||
|
{
|
||||||
|
destination->frameNum = std::max(destination->frameNum, processed.frameNum);
|
||||||
|
destination->isLoop = processed.isLoop;
|
||||||
|
if (!processed.rootAnimation.frames.empty()) destination->rootAnimation = processed.rootAnimation;
|
||||||
|
if (!processed.triggers.frames.empty()) destination->triggers = processed.triggers;
|
||||||
|
|
||||||
|
merge_item_map(destination->layerAnimations, processed.layerAnimations);
|
||||||
|
merge_item_map(destination->nullAnimations, processed.nullAnimations);
|
||||||
|
|
||||||
|
for (auto id : processed.layerOrder)
|
||||||
|
if (std::find(destination->layerOrder.begin(), destination->layerOrder.end(), id) ==
|
||||||
|
destination->layerOrder.end())
|
||||||
|
destination->layerOrder.push_back(id);
|
||||||
|
|
||||||
|
destination->fit_length();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
animations.items.push_back(std::move(processed));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animations.defaultAnimation.empty() && !source.animations.defaultAnimation.empty()) {
|
||||||
|
animations.defaultAnimation = source.animations.defaultAnimation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/anm2/anm2.h
Normal file
81
src/anm2/anm2.h
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#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};
|
||||||
|
|
||||||
|
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&);
|
||||||
|
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();
|
||||||
|
std::set<int> layers_unused(Animation&);
|
||||||
|
bool layers_deserialize(const std::string&, types::merge::Type, std::string*);
|
||||||
|
|
||||||
|
void null_add(int&);
|
||||||
|
std::set<int> nulls_unused();
|
||||||
|
std::set<int> nulls_unused(Animation&);
|
||||||
|
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();
|
||||||
|
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(int);
|
||||||
|
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(int, Type, int = -1);
|
||||||
|
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(int, Type, int, int = -1);
|
||||||
|
void merge(const Anm2& source, const std::filesystem::path& destinationDirectory = {},
|
||||||
|
const std::filesystem::path& sourceDirectory = {});
|
||||||
|
};
|
||||||
|
}
|
||||||
136
src/anm2/anm2_animations.cpp
Normal file
136
src/anm2/anm2_animations.cpp
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#include "anm2.h"
|
||||||
|
|
||||||
|
#include "vector_.h"
|
||||||
|
|
||||||
|
using namespace anm2ed::util;
|
||||||
|
using namespace anm2ed::types;
|
||||||
|
using namespace tinyxml2;
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
Animation* Anm2::animation_get(int animationIndex) { return vector::find(animations.items, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
71
src/anm2/anm2_events.cpp
Normal file
71
src/anm2/anm2_events.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#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()
|
||||||
|
{
|
||||||
|
std::set<int> used{};
|
||||||
|
|
||||||
|
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
83
src/anm2/anm2_items.cpp
Normal 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(int animationIndex, Type type, int id)
|
||||||
|
{
|
||||||
|
if (Animation* animation = animation_get(animationIndex))
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ROOT:
|
||||||
|
return &animation->rootAnimation;
|
||||||
|
case LAYER:
|
||||||
|
return unordered_map::find(animation->layerAnimations, id);
|
||||||
|
case NULL_:
|
||||||
|
return map::find(animation->nullAnimations, id);
|
||||||
|
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.animationIndex))
|
||||||
|
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.animationIndex))
|
||||||
|
if (!animation->nullAnimations.contains(id)) add(animation, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {reference.animationIndex, LAYER, id};
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/anm2/anm2_layers.cpp
Normal file
72
src/anm2/anm2_layers.cpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#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()
|
||||||
|
{
|
||||||
|
std::set<int> used{};
|
||||||
|
std::set<int> unused{};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<int> Anm2::layers_unused(Animation& animation)
|
||||||
|
{
|
||||||
|
std::set<int> unused{};
|
||||||
|
|
||||||
|
for (auto& id : content.layers | std::views::keys)
|
||||||
|
if (!animation.layerAnimations.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/anm2/anm2_nulls.cpp
Normal file
72
src/anm2/anm2_nulls.cpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#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()
|
||||||
|
{
|
||||||
|
std::set<int> used{};
|
||||||
|
std::set<int> unused{};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<int> Anm2::nulls_unused(Animation& animation)
|
||||||
|
{
|
||||||
|
std::set<int> unused{};
|
||||||
|
|
||||||
|
for (auto& id : content.nulls | std::views::keys)
|
||||||
|
if (!animation.nullAnimations.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/anm2/anm2_sounds.cpp
Normal file
73
src/anm2/anm2_sounds.cpp
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#include "anm2.h"
|
||||||
|
|
||||||
|
#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, sound] : content.sounds)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/anm2/anm2_spritesheets.cpp
Normal file
82
src/anm2/anm2_spritesheets.cpp
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#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); }
|
||||||
|
|
||||||
|
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(localize.get(BASIC_NONE));
|
||||||
|
for (auto& [id, spritesheet] : content.spritesheets)
|
||||||
|
{
|
||||||
|
auto string = spritesheet.path.string();
|
||||||
|
labels.emplace_back(std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, string)));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/anm2/anm2_type.h
Normal file
89
src/anm2/anm2_type.h
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "icon.h"
|
||||||
|
#include "strings.h"
|
||||||
|
|
||||||
|
#include <glm/glm/vec2.hpp>
|
||||||
|
#include <glm/glm/vec3.hpp>
|
||||||
|
#include <glm/glm/vec4.hpp>
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
inline const glm::vec4 ROOT_COLOR = glm::vec4(0.140f, 0.310f, 0.560f, 1.000f);
|
||||||
|
inline const glm::vec4 ROOT_COLOR_ACTIVE = glm::vec4(0.240f, 0.520f, 0.880f, 1.000f);
|
||||||
|
inline const glm::vec4 ROOT_COLOR_HOVERED = glm::vec4(0.320f, 0.640f, 1.000f, 1.000f);
|
||||||
|
|
||||||
|
inline const glm::vec4 LAYER_COLOR = glm::vec4(0.640f, 0.320f, 0.110f, 1.000f);
|
||||||
|
inline const glm::vec4 LAYER_COLOR_ACTIVE = glm::vec4(0.840f, 0.450f, 0.170f, 1.000f);
|
||||||
|
inline const glm::vec4 LAYER_COLOR_HOVERED = glm::vec4(0.960f, 0.560f, 0.240f, 1.000f);
|
||||||
|
|
||||||
|
inline const glm::vec4 NULL_COLOR = glm::vec4(0.140f, 0.430f, 0.200f, 1.000f);
|
||||||
|
inline const glm::vec4 NULL_COLOR_ACTIVE = glm::vec4(0.250f, 0.650f, 0.350f, 1.000f);
|
||||||
|
inline const glm::vec4 NULL_COLOR_HOVERED = glm::vec4(0.350f, 0.800f, 0.480f, 1.000f);
|
||||||
|
|
||||||
|
inline const glm::vec4 TRIGGER_COLOR = glm::vec4(0.620f, 0.150f, 0.260f, 1.000f);
|
||||||
|
inline const glm::vec4 TRIGGER_COLOR_ACTIVE = glm::vec4(0.820f, 0.250f, 0.380f, 1.000f);
|
||||||
|
inline const glm::vec4 TRIGGER_COLOR_HOVERED = glm::vec4(0.950f, 0.330f, 0.490f, 1.000f);
|
||||||
|
|
||||||
|
#define TYPE_LIST \
|
||||||
|
X(NONE, STRING_UNDEFINED, "", resource::icon::NONE, glm::vec4(), glm::vec4(), glm::vec4()) \
|
||||||
|
X(ROOT, BASIC_ROOT, "RootAnimation", resource::icon::ROOT, ROOT_COLOR, ROOT_COLOR_ACTIVE, ROOT_COLOR_HOVERED) \
|
||||||
|
X(LAYER, BASIC_LAYER_ANIMATION, "LayerAnimation", resource::icon::LAYER, LAYER_COLOR, LAYER_COLOR_ACTIVE, \
|
||||||
|
LAYER_COLOR_HOVERED) \
|
||||||
|
X(NULL_, BASIC_NULL_ANIMATION, "NullAnimation", resource::icon::NULL_, NULL_COLOR, NULL_COLOR_ACTIVE, \
|
||||||
|
NULL_COLOR_HOVERED) \
|
||||||
|
X(TRIGGER, BASIC_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 StringType 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
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const glm::vec4 TYPE_COLOR[] = {
|
||||||
|
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) color,
|
||||||
|
TYPE_LIST
|
||||||
|
#undef X
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const glm::vec4 TYPE_COLOR_ACTIVE[] = {
|
||||||
|
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorActive,
|
||||||
|
TYPE_LIST
|
||||||
|
#undef X
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const glm::vec4 TYPE_COLOR_HOVERED[] = {
|
||||||
|
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorHovered,
|
||||||
|
TYPE_LIST
|
||||||
|
#undef X
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ChangeType
|
||||||
|
{
|
||||||
|
ADJUST,
|
||||||
|
ADD,
|
||||||
|
SUBTRACT,
|
||||||
|
MULTIPLY,
|
||||||
|
DIVIDE
|
||||||
|
};
|
||||||
|
}
|
||||||
67
src/anm2/content.cpp
Normal file
67
src/anm2/content.cpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#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.emplace(id, Spritesheet(child, id));
|
||||||
|
|
||||||
|
if (auto layersElement = element->FirstChildElement("Layers"))
|
||||||
|
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
|
||||||
|
layers.emplace(id, Layer(child, id));
|
||||||
|
|
||||||
|
if (auto nullsElement = element->FirstChildElement("Nulls"))
|
||||||
|
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
|
||||||
|
nulls.emplace(id, Null(child, id));
|
||||||
|
|
||||||
|
if (auto eventsElement = element->FirstChildElement("Events"))
|
||||||
|
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
|
||||||
|
events.emplace(id, Event(child, id));
|
||||||
|
|
||||||
|
if (auto soundsElement = element->FirstChildElement("Sounds"))
|
||||||
|
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
|
||||||
|
sounds.emplace(id, Sound(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);
|
||||||
|
|
||||||
|
if (!sounds.empty())
|
||||||
|
{
|
||||||
|
auto soundsElement = document.NewElement("Sounds");
|
||||||
|
for (auto& [id, sound] : sounds)
|
||||||
|
sound.serialize(document, soundsElement, id);
|
||||||
|
element->InsertEndChild(soundsElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent->InsertEndChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
src/anm2/content.h
Normal file
26
src/anm2/content.h
Normal 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
36
src/anm2/event.cpp
Normal 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
20
src/anm2/event.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <tinyxml2/tinyxml2.h>
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
class Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::string name{};
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
123
src/anm2/frame.cpp
Normal file
123
src/anm2/frame.cpp
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#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", &duration);
|
||||||
|
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("SoundId", &soundID);
|
||||||
|
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", duration);
|
||||||
|
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", duration);
|
||||||
|
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("SoundId", soundID);
|
||||||
|
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() { duration = glm::clamp(--duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); }
|
||||||
|
|
||||||
|
void Frame::extend() { duration = glm::clamp(++duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); }
|
||||||
|
|
||||||
|
}
|
||||||
55
src/anm2/frame.h
Normal file
55
src/anm2/frame.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <tinyxml2/tinyxml2.h>
|
||||||
|
|
||||||
|
#include "anm2_type.h"
|
||||||
|
#include "types.h"
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
constexpr auto FRAME_DURATION_MIN = 1;
|
||||||
|
constexpr auto FRAME_DURATION_MAX = 1000000;
|
||||||
|
|
||||||
|
#define MEMBERS \
|
||||||
|
X(isVisible, bool, true) \
|
||||||
|
X(isInterpolated, bool, false) \
|
||||||
|
X(rotation, float, 0.0f) \
|
||||||
|
X(duration, int, FRAME_DURATION_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, glm::vec3()) \
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FrameChange
|
||||||
|
{
|
||||||
|
#define X(name, type, ...) std::optional<type> name{};
|
||||||
|
MEMBERS
|
||||||
|
#undef X
|
||||||
|
};
|
||||||
|
|
||||||
|
#undef MEMBERS
|
||||||
|
}
|
||||||
40
src/anm2/info.cpp
Normal file
40
src/anm2/info.cpp
Normal 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
25
src/anm2/info.h
Normal 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();
|
||||||
|
};
|
||||||
|
}
|
||||||
352
src/anm2/item.cpp
Normal file
352
src/anm2/item.cpp
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
#include "item.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#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);
|
||||||
|
|
||||||
|
if (type == TRIGGER) frames_sort_by_at_frame();
|
||||||
|
|
||||||
|
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.duration;
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Item::frames_sort_by_at_frame()
|
||||||
|
{
|
||||||
|
std::sort(frames.begin(), frames.end(), [](const Frame& a, const Frame& b) { return a.atFrame < b.atFrame; });
|
||||||
|
}
|
||||||
|
|
||||||
|
Frame Item::frame_generate(float time, Type type)
|
||||||
|
{
|
||||||
|
Frame frame{};
|
||||||
|
frame.isVisible = false;
|
||||||
|
|
||||||
|
if (frames.empty()) return frame;
|
||||||
|
|
||||||
|
time = time < 0.0f ? 0.0f : time;
|
||||||
|
|
||||||
|
Frame* frameNext = nullptr;
|
||||||
|
int durationCurrent = 0;
|
||||||
|
int durationNext = 0;
|
||||||
|
|
||||||
|
for (auto [i, iFrame] : std::views::enumerate(frames))
|
||||||
|
{
|
||||||
|
if (type == TRIGGER)
|
||||||
|
{
|
||||||
|
if ((int)time == iFrame.atFrame)
|
||||||
|
{
|
||||||
|
frame = iFrame;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
frame = iFrame;
|
||||||
|
|
||||||
|
durationNext += frame.duration;
|
||||||
|
|
||||||
|
if (time >= durationCurrent && time < durationNext)
|
||||||
|
{
|
||||||
|
if (i + 1 < (int)frames.size())
|
||||||
|
frameNext = &frames[i + 1];
|
||||||
|
else
|
||||||
|
frameNext = nullptr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
durationCurrent += frame.duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != TRIGGER && frame.isInterpolated && frameNext && frame.duration > 1)
|
||||||
|
{
|
||||||
|
auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
|
||||||
|
|
||||||
|
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.duration) frame.duration = std::max(FRAME_DURATION_MIN, *change.duration);
|
||||||
|
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.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration + *change.duration);
|
||||||
|
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.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration - *change.duration);
|
||||||
|
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 MULTIPLY:
|
||||||
|
if (change.rotation) frame.rotation *= *change.rotation;
|
||||||
|
if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration * *change.duration);
|
||||||
|
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 DIVIDE:
|
||||||
|
if (change.rotation && *change.rotation != 0.0f) frame.rotation /= *change.rotation;
|
||||||
|
if (change.duration && *change.duration != 0)
|
||||||
|
frame.duration = std::max(FRAME_DURATION_MIN, frame.duration / *change.duration);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
int count{};
|
||||||
|
if (document.FirstChildElement("Frame") && type != anm2::TRIGGER)
|
||||||
|
{
|
||||||
|
start = std::clamp(start, 0, (int)frames.size());
|
||||||
|
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 (document.FirstChildElement("Trigger") && type == anm2::TRIGGER)
|
||||||
|
{
|
||||||
|
auto has_conflict = [&](int value)
|
||||||
|
{
|
||||||
|
for (auto& trigger : frames)
|
||||||
|
if (trigger.atFrame == value) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto element = document.FirstChildElement("Trigger"); element;
|
||||||
|
element = element->NextSiblingElement("Trigger"))
|
||||||
|
{
|
||||||
|
Frame trigger(element, type);
|
||||||
|
trigger.atFrame = start + count;
|
||||||
|
while (has_conflict(trigger.atFrame))
|
||||||
|
trigger.atFrame++;
|
||||||
|
frames.push_back(trigger);
|
||||||
|
indices.insert(trigger.atFrame);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
frames_sort_by_at_frame();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (errorString) *errorString = type == anm2::TRIGGER ? "No valid trigger(s)." : "No valid frame(s).";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
|
auto original = frames[index];
|
||||||
|
if (original.duration == FRAME_DURATION_MIN) return;
|
||||||
|
|
||||||
|
auto nextFrame = vector::in_bounds(frames, index + 1) ? frames[index + 1] : original;
|
||||||
|
|
||||||
|
int duration{};
|
||||||
|
int i = index;
|
||||||
|
|
||||||
|
while (duration < original.duration)
|
||||||
|
{
|
||||||
|
Frame baked = original;
|
||||||
|
float interpolation = (float)duration / original.duration;
|
||||||
|
baked.duration = std::min(interval, original.duration - duration);
|
||||||
|
baked.isInterpolated = (i == index) ? original.isInterpolated : false;
|
||||||
|
baked.rotation = glm::mix(original.rotation, nextFrame.rotation, interpolation);
|
||||||
|
baked.position = glm::mix(original.position, nextFrame.position, interpolation);
|
||||||
|
baked.scale = glm::mix(original.scale, nextFrame.scale, interpolation);
|
||||||
|
baked.colorOffset = glm::mix(original.colorOffset, nextFrame.colorOffset, interpolation);
|
||||||
|
baked.tint = glm::mix(original.tint, nextFrame.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++;
|
||||||
|
|
||||||
|
duration += baked.duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Item::frames_generate_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count,
|
||||||
|
int duration)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
Frame frame{};
|
||||||
|
frame.duration = duration;
|
||||||
|
frame.pivot = pivot;
|
||||||
|
frame.size = size;
|
||||||
|
frame.crop = startPosition + ivec2(size.x * (i % columns), size.y * (i / columns));
|
||||||
|
|
||||||
|
frames.emplace_back(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int Item::frame_index_from_at_frame_get(int atFrame)
|
||||||
|
{
|
||||||
|
for (auto [i, frame] : std::views::enumerate(frames))
|
||||||
|
if (frame.atFrame == atFrame) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Item::frame_time_from_index_get(int index)
|
||||||
|
{
|
||||||
|
if (!vector::in_bounds(frames, index)) return 0.0f;
|
||||||
|
|
||||||
|
float time{};
|
||||||
|
for (auto [i, frame] : std::views::enumerate(frames))
|
||||||
|
{
|
||||||
|
if (i == index) return time;
|
||||||
|
time += frame.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Item::frame_index_from_time_get(float time)
|
||||||
|
{
|
||||||
|
if (frames.empty()) return -1;
|
||||||
|
if (time <= 0.0f) return 0;
|
||||||
|
|
||||||
|
float duration{};
|
||||||
|
for (auto [i, frame] : std::views::enumerate(frames))
|
||||||
|
{
|
||||||
|
duration += frame.duration;
|
||||||
|
if (time < duration) return (int)i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)frames.size() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/anm2/item.h
Normal file
32
src/anm2/item.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#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);
|
||||||
|
void frames_sort_by_at_frame();
|
||||||
|
int frame_index_from_at_frame_get(int);
|
||||||
|
int frame_index_from_time_get(float);
|
||||||
|
float frame_time_from_index_get(int);
|
||||||
|
};
|
||||||
|
}
|
||||||
43
src/anm2/layer.cpp
Normal file
43
src/anm2/layer.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/anm2/layer.h
Normal file
21
src/anm2/layer.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <tinyxml2/tinyxml2.h>
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
class Layer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::string name{};
|
||||||
|
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
38
src/anm2/null.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/anm2/null.h
Normal file
20
src/anm2/null.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <tinyxml2/tinyxml2.h>
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
class Null
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::string name{};
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
84
src/anm2/sound.cpp
Normal file
84
src/anm2/sound.cpp
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#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); }
|
||||||
|
|
||||||
|
Sound& Sound::operator=(const Sound& other)
|
||||||
|
{
|
||||||
|
if (this != &other)
|
||||||
|
{
|
||||||
|
path = other.path;
|
||||||
|
audio = path.empty() ? Audio() : Audio(path);
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::filesystem::path make_relative_or_keep(const std::filesystem::path& input)
|
||||||
|
{
|
||||||
|
if (input.empty()) return input;
|
||||||
|
std::error_code ec{};
|
||||||
|
auto relative = std::filesystem::relative(input, ec);
|
||||||
|
if (!ec) return relative;
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sound::Sound(const std::string& directory, const std::string& path)
|
||||||
|
{
|
||||||
|
filesystem::WorkingDirectory workingDirectory(directory);
|
||||||
|
this->path = !path.empty() ? make_relative_or_keep(path) : this->path;
|
||||||
|
this->path = filesystem::path_lower_case_backslash_handle(this->path);
|
||||||
|
audio = Audio(this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sound::Sound(const std::filesystem::path& directory, const std::filesystem::path& path)
|
||||||
|
: Sound(directory.string(), path.string())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
XMLElement* Sound::to_element(XMLDocument& document, int id)
|
||||||
|
{
|
||||||
|
auto element = document.NewElement("Sound");
|
||||||
|
element->SetAttribute("Id", id);
|
||||||
|
auto pathString = path.generic_string();
|
||||||
|
element->SetAttribute("Path", pathString.c_str());
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sound::serialize(XMLDocument& document, XMLElement* parent, int id)
|
||||||
|
{
|
||||||
|
parent->InsertEndChild(to_element(document, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
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::filesystem::path& 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
32
src/anm2/sound.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <tinyxml2/tinyxml2.h>
|
||||||
|
|
||||||
|
#include "audio.h"
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
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&);
|
||||||
|
Sound(const std::filesystem::path&, const std::filesystem::path&);
|
||||||
|
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
|
||||||
|
std::string to_string(int);
|
||||||
|
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
|
||||||
|
void reload(const std::filesystem::path&);
|
||||||
|
bool is_valid();
|
||||||
|
void play();
|
||||||
|
};
|
||||||
|
}
|
||||||
81
src/anm2/spritesheet.cpp
Normal file
81
src/anm2/spritesheet.cpp
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::filesystem::path make_relative_or_keep(const std::filesystem::path& input)
|
||||||
|
{
|
||||||
|
if (input.empty()) return input;
|
||||||
|
std::error_code ec{};
|
||||||
|
auto relative = std::filesystem::relative(input, ec);
|
||||||
|
if (!ec) return relative;
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spritesheet::Spritesheet(const std::string& directory, const std::string& path)
|
||||||
|
{
|
||||||
|
filesystem::WorkingDirectory workingDirectory(directory);
|
||||||
|
this->path = !path.empty() ? make_relative_or_keep(path) : this->path;
|
||||||
|
this->path = filesystem::path_lower_case_backslash_handle(this->path);
|
||||||
|
texture = Texture(this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Spritesheet::Spritesheet(const std::filesystem::path& directory, const std::filesystem::path& path)
|
||||||
|
: Spritesheet(directory.string(), path.string())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
XMLElement* Spritesheet::to_element(XMLDocument& document, int id)
|
||||||
|
{
|
||||||
|
auto element = document.NewElement("Spritesheet");
|
||||||
|
element->SetAttribute("Id", id);
|
||||||
|
auto pathString = path.generic_string();
|
||||||
|
element->SetAttribute("Path", pathString.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() ? make_relative_or_keep(path) : this->path;
|
||||||
|
return texture.write_png(this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Spritesheet::reload(const std::filesystem::path& directory) { *this = Spritesheet(directory, this->path); }
|
||||||
|
|
||||||
|
bool Spritesheet::is_valid() { return texture.is_valid(); }
|
||||||
|
|
||||||
|
}
|
||||||
28
src/anm2/spritesheet.h
Normal file
28
src/anm2/spritesheet.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <tinyxml2/tinyxml2.h>
|
||||||
|
|
||||||
|
#include "texture.h"
|
||||||
|
|
||||||
|
namespace anm2ed::anm2
|
||||||
|
{
|
||||||
|
class Spritesheet
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::filesystem::path path{};
|
||||||
|
resource::Texture texture;
|
||||||
|
|
||||||
|
Spritesheet() = default;
|
||||||
|
Spritesheet(tinyxml2::XMLElement*, int&);
|
||||||
|
Spritesheet(const std::string&, const std::string& = {});
|
||||||
|
Spritesheet(const std::filesystem::path&, const std::filesystem::path& = {});
|
||||||
|
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::filesystem::path&);
|
||||||
|
bool is_valid();
|
||||||
|
};
|
||||||
|
}
|
||||||
373
src/canvas.cpp
373
src/canvas.cpp
@@ -1,244 +1,253 @@
|
|||||||
#include "canvas.h"
|
#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"
|
||||||
|
|
||||||
|
using namespace glm;
|
||||||
|
using namespace anm2ed::resource;
|
||||||
|
using namespace anm2ed::util;
|
||||||
|
using namespace anm2ed::canvas;
|
||||||
|
|
||||||
|
namespace anm2ed
|
||||||
{
|
{
|
||||||
self->size = size;
|
constexpr float AXIS_VERTICES[] = {-1.0f, 0.0f, 1.0f, 0.0f};
|
||||||
self->previousSize = size;
|
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);
|
Canvas::Canvas(vec2 size)
|
||||||
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);
|
Framebuffer::size_set(size);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
||||||
|
|
||||||
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
|
// Axis
|
||||||
glGenVertexArrays(1, &self->axisVAO);
|
glGenVertexArrays(1, &axisVAO);
|
||||||
glGenBuffers(1, &self->axisVBO);
|
glGenBuffers(1, &axisVBO);
|
||||||
|
|
||||||
glBindVertexArray(self->axisVAO);
|
glBindVertexArray(axisVAO);
|
||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, self->axisVBO);
|
glBindBuffer(GL_ARRAY_BUFFER, axisVBO);
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_AXIS_VERTICES), CANVAS_AXIS_VERTICES, GL_STATIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, sizeof(AXIS_VERTICES), AXIS_VERTICES, GL_STATIC_DRAW);
|
||||||
|
|
||||||
glEnableVertexAttribArray(0);
|
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
|
// Grid
|
||||||
glGenVertexArrays(1, &self->gridVAO);
|
glGenVertexArrays(1, &gridVAO);
|
||||||
glGenBuffers(1, &self->gridVBO);
|
glBindVertexArray(gridVAO);
|
||||||
|
|
||||||
glBindVertexArray(self->gridVAO);
|
glGenBuffers(1, &gridVBO);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, self->gridVBO);
|
glBindBuffer(GL_ARRAY_BUFFER, gridVBO);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(GRID_VERTICES), GRID_VERTICES, GL_STATIC_DRAW);
|
||||||
|
|
||||||
glEnableVertexAttribArray(0);
|
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);
|
|
||||||
|
|
||||||
glBindVertexArray(0);
|
|
||||||
|
|
||||||
// Texture
|
|
||||||
glGenVertexArrays(1, &self->textureVAO);
|
|
||||||
glGenBuffers(1, &self->textureVBO);
|
|
||||||
glGenBuffers(1, &self->textureEBO);
|
|
||||||
|
|
||||||
glBindVertexArray(self->textureVAO);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
glEnableVertexAttribArray(0);
|
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(f32), (void*)0);
|
|
||||||
|
|
||||||
glEnableVertexAttribArray(1);
|
glEnableVertexAttribArray(1);
|
||||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(f32), (void*)(2 * sizeof(f32)));
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, (void*)(sizeof(float) * 2));
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
// Framebuffer
|
// Rect
|
||||||
glGenTextures(1, &self->framebuffer);
|
glGenVertexArrays(1, &rectVAO);
|
||||||
glGenFramebuffers(1, &self->fbo);
|
glGenBuffers(1, &rectVBO);
|
||||||
glGenRenderbuffers(1, &self->rbo);
|
|
||||||
_canvas_framebuffer_set(self, size);
|
|
||||||
|
|
||||||
self->isInit = true;
|
glBindVertexArray(rectVAO);
|
||||||
}
|
|
||||||
|
|
||||||
mat4 canvas_transform_get(Canvas* self, vec2 pan, f32 zoom, OriginType origin)
|
glBindBuffer(GL_ARRAY_BUFFER, rectVBO);
|
||||||
{
|
glBufferData(GL_ARRAY_BUFFER, sizeof(RECT_VERTICES), RECT_VERTICES, GL_STATIC_DRAW);
|
||||||
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)
|
glEnableVertexAttribArray(0);
|
||||||
{
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
|
||||||
case ORIGIN_TOP_LEFT:
|
|
||||||
view = glm::translate(view, vec3(pan, 0.0f));
|
// Texture
|
||||||
break;
|
glGenVertexArrays(1, &textureVAO);
|
||||||
default:
|
glGenBuffers(1, &textureVBO);
|
||||||
view = glm::translate(view, vec3((size * 0.5f) + pan, 0.0f));
|
glGenBuffers(1, &textureEBO);
|
||||||
break;
|
|
||||||
|
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(float), (void*)(2 * sizeof(float)));
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Canvas::~Canvas()
|
||||||
|
{
|
||||||
|
if (!Framebuffer::is_valid()) return;
|
||||||
|
|
||||||
|
glDeleteVertexArrays(1, &axisVAO);
|
||||||
|
glDeleteBuffers(1, &axisVBO);
|
||||||
|
|
||||||
|
glDeleteVertexArrays(1, &gridVAO);
|
||||||
|
glDeleteBuffers(1, &gridVBO);
|
||||||
|
|
||||||
|
glDeleteVertexArrays(1, &rectVAO);
|
||||||
|
glDeleteBuffers(1, &rectVBO);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 Canvas::transform_get(float zoom, vec2 pan) const
|
||||||
|
{
|
||||||
|
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));
|
view = glm::scale(view, vec3(zoomFactor, zoomFactor, 1.0f));
|
||||||
|
|
||||||
return projection * view;
|
return projection * view;
|
||||||
}
|
}
|
||||||
|
|
||||||
void canvas_clear(vec4& color)
|
void Canvas::axes_render(Shader& shader, float zoom, vec2 pan, vec4 color) const
|
||||||
{
|
{
|
||||||
glClearColor(color.r, color.g, color.b, color.a);
|
auto originNDC = transform_get(zoom, pan) * vec4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
originNDC /= originNDC.w;
|
||||||
}
|
|
||||||
|
|
||||||
void canvas_viewport_set(Canvas* self)
|
glUseProgram(shader.id);
|
||||||
{
|
|
||||||
glViewport(0, 0, (s32)self->size.x, (s32)self->size.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
void canvas_framebuffer_resize_check(Canvas* self)
|
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), 1, value_ptr(color));
|
||||||
{
|
glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_ORIGIN_NDC), originNDC.x, originNDC.y);
|
||||||
if (self->previousSize != self->size)
|
|
||||||
_canvas_framebuffer_set(self, self->size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void canvas_grid_draw(Canvas* self, GLuint& shader, mat4& transform, ivec2& size, ivec2& offset, vec4& color)
|
glBindVertexArray(axisVAO);
|
||||||
{
|
|
||||||
mat4 inverseTransform = glm::inverse(transform);
|
|
||||||
|
|
||||||
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));
|
glUniform1i(glGetUniformLocation(shader.id, shader::UNIFORM_AXIS), 1);
|
||||||
glUniform2f(glGetUniformLocation(shader, SHADER_UNIFORM_SIZE), size.x, size.y);
|
glDrawArrays(GL_LINES, 0, 2);
|
||||||
glUniform2f(glGetUniformLocation(shader, SHADER_UNIFORM_OFFSET), offset.x, offset.y);
|
|
||||||
glUniform4f(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), color.r, color.g, color.b, color.a);
|
|
||||||
|
|
||||||
glBindVertexArray(self->gridVAO);
|
glBindVertexArray(0);
|
||||||
|
glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Canvas::grid_render(Shader& shader, float zoom, vec2 pan, ivec2 size, ivec2 offset, vec4 color) const
|
||||||
|
{
|
||||||
|
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);
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void canvas_texture_draw(Canvas* self, GLuint& shader, GLuint& texture, mat4& transform, const f32* vertices, vec4 tint, vec3 colorOffset)
|
void Canvas::texture_render(Shader& shader, GLuint& texture, mat4 transform, vec4 tint, vec3 colorOffset,
|
||||||
{
|
float* vertices) const
|
||||||
glUseProgram(shader);
|
{
|
||||||
|
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);
|
glBindVertexArray(textureVAO);
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW);
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, textureVBO);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
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);
|
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
}
|
|
||||||
|
|
||||||
void canvas_rect_draw(Canvas* self, const GLuint& shader, const mat4& transform, const vec4& color)
|
glEnable(GL_BLEND);
|
||||||
{
|
}
|
||||||
glUseProgram(shader);
|
|
||||||
|
|
||||||
glBindVertexArray(self->rectVAO);
|
void Canvas::rect_render(Shader& shader, const mat4& transform, const mat4& model, vec4 color, float dashLength,
|
||||||
|
float dashGap, float dashOffset) const
|
||||||
|
{
|
||||||
|
glUseProgram(shader.id);
|
||||||
|
|
||||||
glUniformMatrix4fv(glGetUniformLocation(shader, SHADER_UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
|
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
|
||||||
glUniform4fv(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), 1, value_ptr(color));
|
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));
|
||||||
|
|
||||||
|
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);
|
glDrawArrays(GL_LINE_LOOP, 0, 4);
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Canvas::zoom_set(float& zoom, vec2& pan, vec2 focus, float step) const
|
||||||
void canvas_axes_draw(Canvas* self, GLuint& shader, mat4& transform, vec4& color)
|
{
|
||||||
{
|
auto zoomFactor = math::percent_to_unit(zoom);
|
||||||
vec4 originNDC = transform * vec4(0.0f, 0.0f, 0.0f, 1.0f);
|
float newZoom = glm::clamp(math::round_nearest_multiple(zoom + step, step), ZOOM_MIN, ZOOM_MAX);
|
||||||
originNDC /= originNDC.w;
|
if (newZoom != zoom)
|
||||||
|
{
|
||||||
glUseProgram(shader);
|
float newZoomFactor = math::percent_to_unit(newZoom);
|
||||||
glBindVertexArray(self->axisVAO);
|
pan += focus * (zoomFactor - newZoomFactor);
|
||||||
glUniform4fv(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), 1, value_ptr(color));
|
zoom = newZoom;
|
||||||
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);
|
vec2 Canvas::position_translate(float& zoom, vec2& pan, vec2 position) const
|
||||||
glDrawArrays(GL_LINES, 0, 2);
|
{
|
||||||
glBindVertexArray(0);
|
auto zoomFactor = math::percent_to_unit(zoom);
|
||||||
glUseProgram(0);
|
return (position - pan - (size * 0.5f)) / zoomFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
void canvas_bind(Canvas* self)
|
void Canvas::set_to_rect(float& zoom, vec2& pan, vec4 rect) const
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
|
if (rect != vec4(-1.0f) && (rect.z > 0 && rect.w > 0))
|
||||||
}
|
{
|
||||||
|
f32 scaleX = size.x / rect.z;
|
||||||
void canvas_unbind(void)
|
f32 scaleY = size.y / rect.w;
|
||||||
{
|
f32 fitScale = std::min(scaleX, scaleY);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
}
|
zoom = math::unit_to_percent(fitScale);
|
||||||
|
|
||||||
void canvas_free(Canvas* self)
|
vec2 rectCenter = {rect.x + rect.z * 0.5f, rect.y + rect.w * 0.5f};
|
||||||
{
|
pan = -rectCenter * fitScale;
|
||||||
if (!self->isInit) return;
|
}
|
||||||
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
119
src/canvas.h
119
src/canvas.h
@@ -1,95 +1,64 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "resources.h"
|
#include <glad/glad.h>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
#define CANVAS_ZOOM_MIN 1.0f
|
#include "framebuffer.h"
|
||||||
#define CANVAS_ZOOM_MAX 2000.0f
|
#include "shader.h"
|
||||||
#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
|
|
||||||
|
|
||||||
const inline vec2 CANVAS_PIVOT_SIZE = {4, 4};
|
namespace anm2ed::canvas
|
||||||
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[] =
|
|
||||||
{
|
{
|
||||||
-1.0f, -1.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};
|
||||||
3.0f, -1.0f,
|
|
||||||
-1.0f, 3.0f
|
|
||||||
};
|
|
||||||
|
|
||||||
const inline f32 CANVAS_RECT_VERTICES[] =
|
constexpr auto PIVOT_SIZE = glm::vec2(8, 8);
|
||||||
{
|
constexpr auto ZOOM_MIN = 1.0f;
|
||||||
0, 0,
|
constexpr auto ZOOM_MAX = 2000.0f;
|
||||||
1, 0,
|
|
||||||
1, 1,
|
|
||||||
0, 1
|
|
||||||
};
|
|
||||||
|
|
||||||
const inline f32 CANVAS_TEXTURE_VERTICES[] =
|
constexpr auto DASH_LENGTH = 4.0f;
|
||||||
{
|
constexpr auto DASH_GAP = 1.0f;
|
||||||
0, 0, 0.0f, 0.0f,
|
constexpr auto DASH_OFFSET = 1.0f;
|
||||||
1, 0, 1.0f, 0.0f,
|
|
||||||
1, 1, 1.0f, 1.0f,
|
|
||||||
0, 1, 0.0f, 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;
|
||||||
|
|
||||||
|
constexpr auto CHECKER_SIZE = 32.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace anm2ed
|
||||||
{
|
{
|
||||||
GLuint fbo{};
|
|
||||||
GLuint rbo{};
|
class Canvas : public Framebuffer
|
||||||
|
{
|
||||||
|
public:
|
||||||
GLuint axisVAO{};
|
GLuint axisVAO{};
|
||||||
GLuint axisVBO{};
|
GLuint axisVBO{};
|
||||||
GLuint rectVAO{};
|
GLuint rectVAO{};
|
||||||
GLuint rectVBO{};
|
GLuint rectVBO{};
|
||||||
GLuint gridVAO{};
|
GLuint gridVAO{};
|
||||||
GLuint gridVBO{};
|
GLuint gridVBO{};
|
||||||
GLuint framebuffer{};
|
|
||||||
GLuint textureVAO{};
|
GLuint textureVAO{};
|
||||||
GLuint textureVBO{};
|
GLuint textureVBO{};
|
||||||
GLuint textureEBO{};
|
GLuint textureEBO{};
|
||||||
ivec2 size{};
|
|
||||||
ivec2 previousSize{};
|
|
||||||
bool isInit = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#define UV_VERTICES(uvMin, uvMax) \
|
Canvas();
|
||||||
{ \
|
Canvas(glm::vec2);
|
||||||
0, 0, uvMin.x, uvMin.y, \
|
~Canvas();
|
||||||
1, 0, uvMax.x, uvMin.y, \
|
glm::mat4 transform_get(float = 100.0f, glm::vec2 = {}) const;
|
||||||
1, 1, uvMax.x, uvMax.y, \
|
void axes_render(resource::Shader&, float, glm::vec2, glm::vec4 = glm::vec4(1.0f)) const;
|
||||||
0, 1, uvMin.x, uvMax.y \
|
void grid_render(resource::Shader&, float, glm::vec2, glm::ivec2 = glm::ivec2(32, 32), glm::ivec2 = {},
|
||||||
|
glm::vec4 = glm::vec4(1.0f)) const;
|
||||||
|
void texture_render(resource::Shader&, GLuint&, glm::mat4 = {1.0f}, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {},
|
||||||
|
float* = (float*)canvas::TEXTURE_VERTICES) const;
|
||||||
|
void rect_render(resource::Shader&, const glm::mat4&, const glm::mat4&, glm::vec4 = glm::vec4(1.0f),
|
||||||
|
float dashLength = canvas::DASH_LENGTH, float dashGap = canvas::DASH_GAP,
|
||||||
|
float dashOffset = canvas::DASH_OFFSET) const;
|
||||||
|
void zoom_set(float&, glm::vec2&, glm::vec2, float) const;
|
||||||
|
glm::vec2 position_translate(float&, glm::vec2&, glm::vec2) const;
|
||||||
|
void set_to_rect(float& zoom, glm::vec2& pan, glm::vec4 rect) const;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#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
|
|
||||||
);
|
|
||||||
@@ -1,113 +1,21 @@
|
|||||||
#include "clipboard.h"
|
#include "clipboard.h"
|
||||||
|
|
||||||
void clipboard_copy(Clipboard* self)
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace anm2ed
|
||||||
{
|
{
|
||||||
std::string clipboardText{};
|
Clipboard::Clipboard() { set(""); }
|
||||||
|
|
||||||
auto clipboard_text_set = [&]()
|
std::string Clipboard::get()
|
||||||
{
|
{
|
||||||
if (!SDL_SetClipboardText(clipboardText.c_str()))
|
auto text = SDL_GetClipboardText();
|
||||||
log_warning(std::format(CLIPBOARD_TEXT_SET_WARNING, SDL_GetError()));
|
auto string = std::string(text);
|
||||||
};
|
SDL_free(text);
|
||||||
|
|
||||||
switch (self->type)
|
return string;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
case CLIPBOARD_ANIMATION:
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
bool Clipboard::is_empty() { return get().empty(); }
|
||||||
void clipboard_cut(Clipboard* self)
|
|
||||||
{
|
void Clipboard::set(const std::string& string) { SDL_SetClipboardText(string.data()); }
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "anm2.h"
|
#include <string>
|
||||||
|
|
||||||
#define CLIPBOARD_TEXT_SET_WARNING "Unable to set clipboard text! ({})"
|
namespace anm2ed
|
||||||
|
|
||||||
enum ClipboardType
|
|
||||||
{
|
{
|
||||||
CLIPBOARD_NONE,
|
class Clipboard
|
||||||
CLIPBOARD_FRAME,
|
{
|
||||||
CLIPBOARD_ANIMATION
|
public:
|
||||||
};
|
Clipboard();
|
||||||
|
bool is_empty();
|
||||||
using ClipboardLocation = std::variant<std::monostate, Anm2Reference, s32>;
|
std::string get();
|
||||||
|
void set(const std::string&);
|
||||||
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);
|
|
||||||
|
|||||||
119
src/dialog.cpp
119
src/dialog.cpp
@@ -2,90 +2,83 @@
|
|||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#elif __unix__
|
||||||
|
#else
|
||||||
|
#include "log.h"
|
||||||
|
#include "strings.h"
|
||||||
|
#include "toast.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static void _dialog_callback(void* userdata, const char* const* filelist, s32 filter)
|
#include <format>
|
||||||
{
|
|
||||||
Dialog* self;
|
|
||||||
|
|
||||||
self = (Dialog*)userdata;
|
namespace anm2ed::dialog
|
||||||
|
{
|
||||||
|
static void callback(void* userData, const char* const* filelist, int filter)
|
||||||
|
{
|
||||||
|
auto self = (Dialog*)(userData);
|
||||||
|
|
||||||
if (filelist && filelist[0] && strlen(filelist[0]) > 0)
|
if (filelist && filelist[0] && strlen(filelist[0]) > 0)
|
||||||
{
|
{
|
||||||
self->path = filelist[0];
|
self->path = filelist[0];
|
||||||
self->isSelected = true;
|
|
||||||
self->selectedFilter = filter;
|
self->selectedFilter = filter;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
self->selectedFilter = -1;
|
||||||
self->isSelected = false;
|
|
||||||
self->selectedFilter = INDEX_NONE;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dialog_init(Dialog* self, SDL_Window* window)
|
using namespace anm2ed::dialog;
|
||||||
{
|
|
||||||
self->window = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
Dialog::Dialog(SDL_Window* window)
|
||||||
{
|
{
|
||||||
SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, std::size(DIALOG_FILE_FILTER_ANM2), nullptr);
|
*this = Dialog();
|
||||||
self->type = DIALOG_ANM2_SAVE;
|
this->window = window;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dialog_spritesheet_add(Dialog* self)
|
void Dialog::file_open(dialog::Type type)
|
||||||
{
|
{
|
||||||
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, std::size(DIALOG_FILE_FILTER_PNG), nullptr, false);
|
SDL_ShowOpenFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]),
|
||||||
self->type = DIALOG_SPRITESHEET_ADD;
|
nullptr, false);
|
||||||
}
|
this->type = type;
|
||||||
|
}
|
||||||
|
|
||||||
void dialog_spritesheet_replace(Dialog* self, s32 id)
|
void Dialog::file_save(dialog::Type type)
|
||||||
{
|
{
|
||||||
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, std::size(DIALOG_FILE_FILTER_PNG), nullptr, false);
|
SDL_ShowSaveFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]),
|
||||||
self->replaceID = id;
|
nullptr);
|
||||||
self->type = DIALOG_SPRITESHEET_REPLACE;
|
this->type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dialog_render_path_set(Dialog* self, RenderType type)
|
void Dialog::folder_open(dialog::Type type)
|
||||||
{
|
{
|
||||||
SDL_DialogFileFilter filter = DIALOG_RENDER_FILE_FILTERS[type];
|
SDL_ShowOpenFolderDialog(callback, this, window, nullptr, false);
|
||||||
|
this->type = type;
|
||||||
|
}
|
||||||
|
|
||||||
if (type == RENDER_PNG)
|
void Dialog::file_explorer_open(const std::string& path)
|
||||||
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)
|
|
||||||
{
|
|
||||||
#ifdef _WIN32
|
#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
|
#else
|
||||||
char command[DIALOG_FILE_EXPLORER_COMMAND_SIZE];
|
toasts.push(localize.get(TOAST_NOT_SUPPORTED));
|
||||||
snprintf(command, sizeof(command), DIALOG_FILE_EXPLORER_COMMAND, path.c_str());
|
logger.warning(localize.get(TOAST_NOT_SUPPORTED, anm2ed::ENGLISH));
|
||||||
system(command);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Dialog::reset() { *this = Dialog(this->window); }
|
||||||
dialog_reset(Dialog* self)
|
|
||||||
{
|
bool Dialog::is_selected(dialog::Type type) const { return this->type == type && !path.empty(); }
|
||||||
self->replaceID = ID_NONE;
|
|
||||||
self->type = DIALOG_NONE;
|
void Dialog::set_string_to_selected_path(std::string& string, dialog::Type type)
|
||||||
self->path.clear();
|
{
|
||||||
self->isSelected = false;
|
if (type == NONE) return;
|
||||||
}
|
if (!is_selected(type)) return;
|
||||||
|
string = path;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
133
src/dialog.h
133
src/dialog.h
@@ -1,71 +1,92 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "render.h"
|
#include <string>
|
||||||
#include "window.h"
|
|
||||||
|
|
||||||
#define DIALOG_FILE_EXPLORER_COMMAND_SIZE 512
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#ifdef _WIN32
|
namespace anm2ed::dialog
|
||||||
#define DIALOG_FILE_EXPLORER_COMMAND "open"
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define EXECUTABLE_FILTER {"Executable", "exe"}
|
||||||
#else
|
#else
|
||||||
#define DIALOG_FILE_EXPLORER_COMMAND "xdg-open \"%s\" &"
|
#define EXECUTABLE_FILTER {"Executable", "*"}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const SDL_DialogFileFilter DIALOG_FILE_FILTER_ANM2[] =
|
#define FILTER_LIST \
|
||||||
{
|
X(NO_FILTER, {}) \
|
||||||
{"Anm2 file", "anm2;xml"}
|
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[] =
|
enum Filter
|
||||||
{
|
{
|
||||||
{"PNG image", "png"}
|
#define X(symbol, ...) symbol,
|
||||||
};
|
FILTER_LIST
|
||||||
|
#undef X
|
||||||
|
};
|
||||||
|
|
||||||
const SDL_DialogFileFilter DIALOG_RENDER_FILE_FILTERS[] =
|
constexpr SDL_DialogFileFilter FILTERS[][1] = {
|
||||||
{
|
#define X(symbol, ...) {__VA_ARGS__},
|
||||||
{"PNG image", "png"},
|
FILTER_LIST
|
||||||
{"GIF image", "gif"},
|
#undef X
|
||||||
{"WebM video", "webm"},
|
};
|
||||||
{"MP4 video", "mp4"}
|
|
||||||
};
|
|
||||||
|
|
||||||
const SDL_DialogFileFilter DIALOG_FILE_FILTER_FFMPEG[] =
|
#undef FILTER_LIST
|
||||||
{
|
|
||||||
#ifdef _WIN32
|
|
||||||
{"Executable", "exe"}
|
|
||||||
#else
|
|
||||||
{"Executable", ""}
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
|
|
||||||
enum DialogType
|
#define DIALOG_LIST \
|
||||||
{
|
X(NONE, NO_FILTER) \
|
||||||
DIALOG_NONE,
|
X(ANM2_NEW, ANM2) \
|
||||||
DIALOG_ANM2_OPEN,
|
X(ANM2_OPEN, ANM2) \
|
||||||
DIALOG_ANM2_SAVE,
|
X(ANM2_SAVE, ANM2) \
|
||||||
DIALOG_SPRITESHEET_ADD,
|
X(SOUND_OPEN, SOUND) \
|
||||||
DIALOG_SPRITESHEET_REPLACE,
|
X(SPRITESHEET_OPEN, PNG) \
|
||||||
DIALOG_RENDER_PATH_SET,
|
X(SPRITESHEET_REPLACE, PNG) \
|
||||||
DIALOG_FFMPEG_PATH_SET
|
X(FFMPEG_PATH_SET, EXECUTABLE) \
|
||||||
};
|
X(PNG_DIRECTORY_SET, NO_FILTER) \
|
||||||
|
X(PNG_PATH_SET, PNG) \
|
||||||
|
X(GIF_PATH_SET, GIF) \
|
||||||
|
X(WEBM_PATH_SET, WEBM) \
|
||||||
|
X(MP4_PATH_SET, MP4)
|
||||||
|
|
||||||
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{};
|
std::string path{};
|
||||||
s32 replaceID = ID_NONE;
|
dialog::Type type{dialog::NONE};
|
||||||
DialogType type = DIALOG_NONE;
|
int selectedFilter{-1};
|
||||||
bool isSelected = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
void dialog_init(Dialog* self, SDL_Window* window);
|
Dialog() = default;
|
||||||
void dialog_anm2_open(Dialog* self);
|
Dialog(SDL_Window*);
|
||||||
void dialog_spritesheet_add(Dialog* self);
|
void file_open(dialog::Type type);
|
||||||
void dialog_spritesheet_replace(Dialog* self, s32 id);
|
void file_save(dialog::Type type);
|
||||||
void dialog_anm2_save(Dialog* self);
|
void folder_open(dialog::Type type);
|
||||||
void dialog_render_path_set(Dialog* self, RenderType type);
|
bool is_selected(dialog::Type type) const;
|
||||||
void dialog_render_directory_set(Dialog* self);
|
void reset();
|
||||||
void dialog_ffmpeg_path_set(Dialog* self);
|
void file_explorer_open(const std::string&);
|
||||||
void dialog_reset(Dialog* self);
|
void set_string_to_selected_path(std::string& set, dialog::Type type);
|
||||||
void dialog_explorer_open(const std::string& path);
|
};
|
||||||
|
}
|
||||||
|
|||||||
318
src/document.cpp
Normal file
318
src/document.cpp
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
#include "document.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
#include "strings.h"
|
||||||
|
#include "toast.h"
|
||||||
|
|
||||||
|
using namespace anm2ed::anm2;
|
||||||
|
using namespace anm2ed::imgui;
|
||||||
|
using namespace anm2ed::types;
|
||||||
|
|
||||||
|
using namespace glm;
|
||||||
|
|
||||||
|
namespace anm2ed
|
||||||
|
{
|
||||||
|
Document::Document(Anm2& anm2, const std::string& path)
|
||||||
|
{
|
||||||
|
this->anm2 = std::move(anm2);
|
||||||
|
this->path = path;
|
||||||
|
clean();
|
||||||
|
change(Document::ALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
Document::Document(const std::string& path, bool isNew, std::string* errorString)
|
||||||
|
{
|
||||||
|
if (isNew)
|
||||||
|
{
|
||||||
|
anm2 = anm2::Anm2();
|
||||||
|
if (!save(path)) return;
|
||||||
|
}
|
||||||
|
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), frames(current.frames), message(current.message),
|
||||||
|
previewZoom(other.previewZoom), previewPan(other.previewPan), editorPan(other.editorPan),
|
||||||
|
editorZoom(other.editorZoom), overlayIndex(other.overlayIndex), 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;
|
||||||
|
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();
|
||||||
|
|
||||||
|
auto absolutePath = this->path.string();
|
||||||
|
if (anm2.serialize(absolutePath, errorString))
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT), std::make_format_args(absolutePath)));
|
||||||
|
logger.info(
|
||||||
|
std::vformat(localize.get(TOAST_SAVE_DOCUMENT, anm2ed::ENGLISH), std::make_format_args(absolutePath)));
|
||||||
|
clean();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (errorString)
|
||||||
|
{
|
||||||
|
toasts.push(
|
||||||
|
std::vformat(localize.get(TOAST_SAVE_DOCUMENT_FAILED), std::make_format_args(absolutePath, *errorString)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_SAVE_DOCUMENT_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(absolutePath, *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();
|
||||||
|
auto autosavePathString = autosavePath.string();
|
||||||
|
if (anm2.serialize(autosavePathString, errorString))
|
||||||
|
{
|
||||||
|
autosaveHash = hash;
|
||||||
|
lastAutosaveTime = 0.0f;
|
||||||
|
toasts.push(localize.get(TOAST_AUTOSAVING));
|
||||||
|
logger.info(localize.get(TOAST_AUTOSAVING, anm2ed::ENGLISH));
|
||||||
|
logger.info(std::format("Autosaved document to: {}", autosavePath.string()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (errorString)
|
||||||
|
{
|
||||||
|
toasts.push(
|
||||||
|
std::vformat(localize.get(TOAST_AUTOSAVE_FAILED), std::make_format_args(autosavePathString, *errorString)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_AUTOSAVE_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(autosavePathString, *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 FRAMES:
|
||||||
|
events_set();
|
||||||
|
sounds_set();
|
||||||
|
break;
|
||||||
|
case ALL:
|
||||||
|
layers_set();
|
||||||
|
nulls_set();
|
||||||
|
events_set();
|
||||||
|
spritesheets_set();
|
||||||
|
animations_set();
|
||||||
|
sounds_set();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Document::is_dirty() const { return hash != saveHash; }
|
||||||
|
bool Document::is_autosave_dirty() const { return hash != autosaveHash; }
|
||||||
|
std::filesystem::path Document::directory_get() const { return path.parent_path(); }
|
||||||
|
std::filesystem::path Document::filename_get() const { return path.filename(); }
|
||||||
|
bool Document::is_valid() const { return !path.empty(); }
|
||||||
|
|
||||||
|
anm2::Frame* Document::frame_get()
|
||||||
|
{
|
||||||
|
return anm2.frame_get(reference.animationIndex, reference.itemType, reference.frameIndex, reference.itemID);
|
||||||
|
}
|
||||||
|
|
||||||
|
anm2::Item* Document::item_get()
|
||||||
|
{
|
||||||
|
return anm2.item_get(reference.animationIndex, reference.itemType, reference.itemID);
|
||||||
|
}
|
||||||
|
anm2::Animation* Document::animation_get() { return anm2.animation_get(reference.animationIndex); }
|
||||||
|
anm2::Spritesheet* Document::spritesheet_get() { return anm2.spritesheet_get(spritesheet.reference); }
|
||||||
|
|
||||||
|
void Document::spritesheet_add(const std::string& path)
|
||||||
|
{
|
||||||
|
auto add = [&]()
|
||||||
|
{
|
||||||
|
int id{};
|
||||||
|
auto pathCopy = path;
|
||||||
|
if (anm2.spritesheet_add(directory_get().string(), path, id))
|
||||||
|
{
|
||||||
|
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||||
|
auto path = spritesheet.path.string();
|
||||||
|
this->spritesheet.selection = {id};
|
||||||
|
this->spritesheet.reference = id;
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED), std::make_format_args(id, path)));
|
||||||
|
logger.info(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(id, path)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED), std::make_format_args(pathCopy)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(pathCopy)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT_PTR(this, localize.get(EDIT_ADD_SPRITESHEET), Document::SPRITESHEETS, add());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Document::sound_add(const std::string& path)
|
||||||
|
{
|
||||||
|
auto add = [&]()
|
||||||
|
{
|
||||||
|
int id{};
|
||||||
|
auto pathCopy = path;
|
||||||
|
if (anm2.sound_add(directory_get().string(), path, id))
|
||||||
|
{
|
||||||
|
auto& soundInfo = anm2.content.sounds[id];
|
||||||
|
auto soundPath = soundInfo.path.string();
|
||||||
|
sound.selection = {id};
|
||||||
|
sound.reference = id;
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, soundPath)));
|
||||||
|
logger.info(
|
||||||
|
std::vformat(localize.get(TOAST_SOUND_INITIALIZED, anm2ed::ENGLISH), std::make_format_args(id, soundPath)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED), std::make_format_args(pathCopy)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(pathCopy)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT_PTR(this, localize.get(EDIT_ADD_SOUND), Document::SOUNDS, add());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Document::snapshot(const std::string& message)
|
||||||
|
{
|
||||||
|
this->message = message;
|
||||||
|
snapshots.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Document::undo()
|
||||||
|
{
|
||||||
|
snapshots.undo();
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_UNDO), std::make_format_args(message)));
|
||||||
|
logger.info(std::vformat(localize.get(TOAST_UNDO, anm2ed::ENGLISH), std::make_format_args(message)));
|
||||||
|
change(Document::ALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Document::redo()
|
||||||
|
{
|
||||||
|
snapshots.redo();
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_REDO), std::make_format_args(message)));
|
||||||
|
logger.info(std::vformat(localize.get(TOAST_REDO, anm2ed::ENGLISH), std::make_format_args(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(); }
|
||||||
|
}
|
||||||
115
src/document.h
Normal file
115
src/document.h
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "snapshots.h"
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
namespace anm2ed
|
||||||
|
{
|
||||||
|
|
||||||
|
class Document
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum ChangeType
|
||||||
|
{
|
||||||
|
INFO,
|
||||||
|
LAYERS,
|
||||||
|
NULLS,
|
||||||
|
SPRITESHEETS,
|
||||||
|
TEXTURES,
|
||||||
|
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;
|
||||||
|
float& frameTime = current.frameTime;
|
||||||
|
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;
|
||||||
|
Storage& items = current.items;
|
||||||
|
Storage& frames = current.frames;
|
||||||
|
std::string& message = current.message;
|
||||||
|
|
||||||
|
float previewZoom{200};
|
||||||
|
glm::vec2 previewPan{};
|
||||||
|
glm::vec2 editorPan{};
|
||||||
|
float editorZoom{200};
|
||||||
|
int overlayIndex{-1};
|
||||||
|
|
||||||
|
uint64_t hash{};
|
||||||
|
uint64_t saveHash{};
|
||||||
|
uint64_t autosaveHash{};
|
||||||
|
double lastAutosaveTime{};
|
||||||
|
bool isOpen{true};
|
||||||
|
bool isForceDirty{false};
|
||||||
|
bool isAnimationPreviewSet{false};
|
||||||
|
bool isSpritesheetEditorSet{false};
|
||||||
|
|
||||||
|
Document(anm2::Anm2& anm2, const std::string&);
|
||||||
|
Document(const std::string&, bool = false, std::string* = nullptr);
|
||||||
|
Document(const Document&) = delete;
|
||||||
|
Document& operator=(const Document&) = delete;
|
||||||
|
Document(Document&&) noexcept;
|
||||||
|
Document& operator=(Document&&) noexcept;
|
||||||
|
bool save(const std::string& = {}, std::string* = nullptr);
|
||||||
|
void hash_set();
|
||||||
|
void clean();
|
||||||
|
void change(ChangeType);
|
||||||
|
bool is_dirty() const;
|
||||||
|
bool is_autosave_dirty() const;
|
||||||
|
std::filesystem::path directory_get() const;
|
||||||
|
std::filesystem::path filename_get() const;
|
||||||
|
bool is_valid() const;
|
||||||
|
|
||||||
|
anm2::Frame* frame_get();
|
||||||
|
anm2::Item* item_get();
|
||||||
|
anm2::Spritesheet* spritesheet_get();
|
||||||
|
anm2::Animation* animation_get();
|
||||||
|
|
||||||
|
void spritesheet_add(const std::string&);
|
||||||
|
void sound_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); \
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
42
src/editor.h
42
src/editor.h
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
38
src/ffmpeg.h
38
src/ffmpeg.h
@@ -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
|
|
||||||
);
|
|
||||||
88
src/framebuffer.cpp
Normal file
88
src/framebuffer.cpp
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#include "framebuffer.h"
|
||||||
|
|
||||||
|
#include "texture.h"
|
||||||
|
|
||||||
|
using namespace anm2ed::resource;
|
||||||
|
using namespace glm;
|
||||||
|
|
||||||
|
namespace anm2ed
|
||||||
|
{
|
||||||
|
Framebuffer::Framebuffer()
|
||||||
|
{
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glGenRenderbuffers(1, &rbo);
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
set();
|
||||||
|
}
|
||||||
|
|
||||||
|
Framebuffer::~Framebuffer()
|
||||||
|
{
|
||||||
|
if (!is_valid()) return;
|
||||||
|
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteRenderbuffers(1, &rbo);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Framebuffer::set()
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, size.x, size.y);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Framebuffer::resize_check()
|
||||||
|
{
|
||||||
|
if (size != previousSize)
|
||||||
|
{
|
||||||
|
set();
|
||||||
|
previousSize = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Framebuffer::size_set(vec2 size)
|
||||||
|
{
|
||||||
|
previousSize = this->size;
|
||||||
|
this->size = size;
|
||||||
|
resize_check();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> Framebuffer::pixels_get() const
|
||||||
|
{
|
||||||
|
auto count = size.x * size.y * texture::CHANNELS;
|
||||||
|
std::vector<uint8_t> pixels(count);
|
||||||
|
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
|
glReadPixels(0, 0, size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||||
|
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Framebuffer::clear(vec4 color) const
|
||||||
|
{
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glClearColor(color.r, color.g, color.b, color.a);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
if (color.a == 0.0f) glDisable(GL_BLEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Framebuffer::is_valid() const { return fbo != 0; }
|
||||||
|
void Framebuffer::viewport_set() const { glViewport(0, 0, size.x, size.y); }
|
||||||
|
void Framebuffer::bind() const { glBindFramebuffer(GL_FRAMEBUFFER, fbo); }
|
||||||
|
void Framebuffer::unbind() const { glBindFramebuffer(GL_FRAMEBUFFER, 0); }
|
||||||
|
}
|
||||||
37
src/framebuffer.h
Normal file
37
src/framebuffer.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
namespace anm2ed
|
||||||
|
{
|
||||||
|
class Framebuffer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Type
|
||||||
|
{
|
||||||
|
UNDERLAY,
|
||||||
|
LAYER,
|
||||||
|
OVERLAY,
|
||||||
|
};
|
||||||
|
|
||||||
|
GLuint fbo{};
|
||||||
|
GLuint rbo{};
|
||||||
|
GLuint texture{};
|
||||||
|
glm::vec2 size{};
|
||||||
|
glm::vec2 previousSize{};
|
||||||
|
|
||||||
|
Framebuffer();
|
||||||
|
~Framebuffer();
|
||||||
|
|
||||||
|
void set();
|
||||||
|
void resize_check();
|
||||||
|
void size_set(glm::vec2);
|
||||||
|
void viewport_set() const;
|
||||||
|
void clear(glm::vec4 = glm::vec4(1.0f, 1.0f, 1.0f, 0.0f)) const;
|
||||||
|
std::vector<uint8_t> pixels_get() const;
|
||||||
|
bool is_valid() const;
|
||||||
|
void bind() const;
|
||||||
|
void unbind() const;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
3114
src/imgui.cpp
3114
src/imgui.cpp
File diff suppressed because it is too large
Load Diff
2556
src/imgui.h
2556
src/imgui.h
File diff suppressed because it is too large
Load Diff
50
src/imgui/dockspace.cpp
Normal file
50
src/imgui/dockspace.cpp
Normal 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, 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(manager, 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
41
src/imgui/dockspace.h
Normal 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&);
|
||||||
|
};
|
||||||
|
}
|
||||||
236
src/imgui/documents.cpp
Normal file
236
src/imgui/documents.cpp
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
#include "documents.h"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "strings.h"
|
||||||
|
#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();
|
||||||
|
bool isLightTheme = settings.theme == theme::LIGHT;
|
||||||
|
bool pushedStyle = false;
|
||||||
|
|
||||||
|
ImGui::SetNextWindowViewport(viewport->ID);
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height));
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, windowHeight));
|
||||||
|
if (isLightTheme)
|
||||||
|
{
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_TitleBgActive));
|
||||||
|
pushedStyle = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& document : manager.documents)
|
||||||
|
{
|
||||||
|
auto isDirty = document.is_dirty() && document.is_autosave_dirty();
|
||||||
|
if (isDirty)
|
||||||
|
{
|
||||||
|
document.lastAutosaveTime += ImGui::GetIO().DeltaTime;
|
||||||
|
if (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 isCloseShortcut = shortcut(manager.chords[SHORTCUT_CLOSE], shortcut::GLOBAL) && !closePopup.is_open();
|
||||||
|
int closeShortcutIndex =
|
||||||
|
isCloseShortcut && 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 filename = document.filename_get().string();
|
||||||
|
auto string =
|
||||||
|
isDirty ? std::vformat(localize.get(FORMAT_NOT_SAVED), std::make_format_args(filename)) : filename;
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
if (manager.selected != i) manager.set(i);
|
||||||
|
|
||||||
|
if (isRequested) manager.pendingSelected = -1;
|
||||||
|
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
ImGui::SetItemTooltip("%s", document.path.string().c_str());
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
auto filename = closeDocument.filename_get().string();
|
||||||
|
auto prompt = std::vformat(localize.get(LABEL_DOCUMENT_MODIFIED_PROMPT), std::make_format_args(filename));
|
||||||
|
ImGui::TextUnformatted(prompt.c_str());
|
||||||
|
|
||||||
|
auto widgetSize = imgui::widget_size_with_row_get(3);
|
||||||
|
|
||||||
|
auto close = [&]()
|
||||||
|
{
|
||||||
|
closeDocumentIndex = -1;
|
||||||
|
closePopup.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
|
||||||
|
{
|
||||||
|
manager.save(closeDocumentIndex);
|
||||||
|
manager.close(closeDocumentIndex);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_NO), widgetSize))
|
||||||
|
{
|
||||||
|
manager.close(closeDocumentIndex);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize))
|
||||||
|
{
|
||||||
|
isQuitting = false;
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
closeDocumentIndex = -1;
|
||||||
|
closePopup.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
if (pushedStyle) ImGui::PopStyleColor();
|
||||||
|
|
||||||
|
if (manager.isAnm2DragDrop)
|
||||||
|
{
|
||||||
|
auto drag_drop_reset = [&]()
|
||||||
|
{
|
||||||
|
manager.isAnm2DragDrop = false;
|
||||||
|
manager.anm2DragDropPaths.clear();
|
||||||
|
manager.anm2DragDropPopup.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (manager.anm2DragDropPaths.empty())
|
||||||
|
drag_drop_reset();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!manager.anm2DragDropPopup.is_open()) manager.anm2DragDropPopup.open();
|
||||||
|
|
||||||
|
manager.anm2DragDropPopup.trigger();
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupContextWindow(manager.anm2DragDropPopup.label(), ImGuiPopupFlags_None))
|
||||||
|
{
|
||||||
|
auto document = manager.get();
|
||||||
|
if (ImGui::MenuItem(manager.anm2DragDropPaths.size() > 1 ? localize.get(LABEL_DOCUMENTS_OPEN_MANY)
|
||||||
|
: localize.get(LABEL_DOCUMENTS_OPEN_NEW)))
|
||||||
|
{
|
||||||
|
for (auto& path : manager.anm2DragDropPaths)
|
||||||
|
manager.open(path);
|
||||||
|
drag_drop_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::MenuItem(localize.get(LABEL_DOCUMENTS_MERGE_INTO_CURRENT), nullptr, false,
|
||||||
|
document && !manager.anm2DragDropPaths.empty()))
|
||||||
|
{
|
||||||
|
if (document)
|
||||||
|
{
|
||||||
|
auto merge_anm2s = [&]()
|
||||||
|
{
|
||||||
|
for (auto& path : manager.anm2DragDropPaths)
|
||||||
|
{
|
||||||
|
anm2::Anm2 source(path.string());
|
||||||
|
document->anm2.merge(source, document->directory_get(), path.parent_path());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT_PTR(document, localize.get(EDIT_MERGE_ANM2), Document::ALL, merge_anm2s());
|
||||||
|
drag_drop_reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_CANCEL))) drag_drop_reset();
|
||||||
|
|
||||||
|
manager.anm2DragDropPopup.end();
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
else if (!ImGui::IsPopupOpen(manager.anm2DragDropPopup.label()))
|
||||||
|
drag_drop_reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
src/imgui/documents.h
Normal file
21
src/imgui/documents.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "manager.h"
|
||||||
|
#include "resources.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "strings.h"
|
||||||
|
#include "taskbar.h"
|
||||||
|
|
||||||
|
namespace anm2ed::imgui
|
||||||
|
{
|
||||||
|
class Documents
|
||||||
|
{
|
||||||
|
int closeDocumentIndex{-1};
|
||||||
|
imgui::PopupHelper closePopup{imgui::PopupHelper(LABEL_DOCUMENT_CLOSE, imgui::POPUP_TO_CONTENT)};
|
||||||
|
|
||||||
|
public:
|
||||||
|
float height{};
|
||||||
|
|
||||||
|
void update(Taskbar&, Manager&, Settings&, Resources&, bool&);
|
||||||
|
};
|
||||||
|
}
|
||||||
392
src/imgui/imgui_.cpp
Normal file
392
src/imgui/imgui_.cpp
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
#include "imgui_.h"
|
||||||
|
#include "strings.h"
|
||||||
|
|
||||||
|
#include <imgui/imgui_internal.h>
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <cmath>
|
||||||
|
#include <sstream>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
using namespace anm2ed::types;
|
||||||
|
using namespace glm;
|
||||||
|
|
||||||
|
namespace anm2ed::imgui
|
||||||
|
{
|
||||||
|
static auto isRenaming = false;
|
||||||
|
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_BUTTON{0.98f, 0.98f, 0.98f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TITLE_BG{0.78f, 0.78f, 0.78f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TITLE_BG_ACTIVE{0.64f, 0.64f, 0.64f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TITLE_BG_COLLAPSED{0.74f, 0.74f, 0.74f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TABLE_HEADER{0.78f, 0.78f, 0.78f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TAB{0.74f, 0.74f, 0.74f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TAB_HOVERED{0.82f, 0.82f, 0.82f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TAB_SELECTED{0.92f, 0.92f, 0.92f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED{0.70f, 0.70f, 0.70f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_SELECTED{0.86f, 0.86f, 0.86f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TAB_OVERLINE{0.55f, 0.55f, 0.55f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_OVERLINE{0.50f, 0.50f, 0.50f, 1.0f};
|
||||||
|
constexpr ImVec4 COLOR_LIGHT_CHECK_MARK{0.0f, 0.0f, 0.0f, 1.0f};
|
||||||
|
constexpr auto FRAME_BORDER_SIZE = 1.0f;
|
||||||
|
|
||||||
|
void theme_set(theme::Type theme)
|
||||||
|
{
|
||||||
|
switch (theme)
|
||||||
|
{
|
||||||
|
case theme::LIGHT:
|
||||||
|
ImGui::StyleColorsLight();
|
||||||
|
break;
|
||||||
|
case theme::DARK:
|
||||||
|
default:
|
||||||
|
ImGui::StyleColorsDark();
|
||||||
|
break;
|
||||||
|
case theme::CLASSIC:
|
||||||
|
ImGui::StyleColorsClassic();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto& style = ImGui::GetStyle();
|
||||||
|
style.FrameBorderSize = FRAME_BORDER_SIZE;
|
||||||
|
|
||||||
|
if (theme == theme::LIGHT)
|
||||||
|
{
|
||||||
|
auto& colors = style.Colors;
|
||||||
|
colors[ImGuiCol_Button] = COLOR_LIGHT_BUTTON;
|
||||||
|
colors[ImGuiCol_TitleBg] = COLOR_LIGHT_TITLE_BG;
|
||||||
|
colors[ImGuiCol_TitleBgActive] = COLOR_LIGHT_TITLE_BG_ACTIVE;
|
||||||
|
colors[ImGuiCol_TitleBgCollapsed] = COLOR_LIGHT_TITLE_BG_COLLAPSED;
|
||||||
|
colors[ImGuiCol_TableHeaderBg] = COLOR_LIGHT_TABLE_HEADER;
|
||||||
|
colors[ImGuiCol_Tab] = COLOR_LIGHT_TAB;
|
||||||
|
colors[ImGuiCol_TabHovered] = COLOR_LIGHT_TAB_HOVERED;
|
||||||
|
colors[ImGuiCol_TabSelected] = COLOR_LIGHT_TAB_SELECTED;
|
||||||
|
colors[ImGuiCol_TabSelectedOverline] = COLOR_LIGHT_TAB_OVERLINE;
|
||||||
|
colors[ImGuiCol_TabDimmed] = COLOR_LIGHT_TAB_DIMMED;
|
||||||
|
colors[ImGuiCol_TabDimmedSelected] = COLOR_LIGHT_TAB_DIMMED_SELECTED;
|
||||||
|
colors[ImGuiCol_TabDimmedSelectedOverline] = COLOR_LIGHT_TAB_DIMMED_OVERLINE;
|
||||||
|
colors[ImGuiCol_CheckMark] = COLOR_LIGHT_CHECK_MARK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, RenameState& state)
|
||||||
|
{
|
||||||
|
static std::string editID{};
|
||||||
|
auto isRename = editID == id;
|
||||||
|
bool isActivated{};
|
||||||
|
|
||||||
|
if (isRename)
|
||||||
|
{
|
||||||
|
auto finish = [&]()
|
||||||
|
{
|
||||||
|
editID.clear();
|
||||||
|
isActivated = true;
|
||||||
|
state = RENAME_FINISHED;
|
||||||
|
isRenaming = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state == RENAME_BEGIN)
|
||||||
|
{
|
||||||
|
ImGui::SetKeyboardFocusHere();
|
||||||
|
state = RENAME_EDITING;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||||
|
if (input_text_string("##Edit", &text, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll))
|
||||||
|
finish();
|
||||||
|
if (ImGui::IsItemDeactivatedAfterEdit() || ImGui::IsKeyPressed(ImGuiKey_Escape)) finish();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ImGui::Selectable(label.c_str(), isSelected, flags)) isActivated = true;
|
||||||
|
|
||||||
|
if (state == RENAME_FORCE_EDIT || (ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_F2)) ||
|
||||||
|
(ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)))
|
||||||
|
{
|
||||||
|
state = RENAME_BEGIN;
|
||||||
|
editID = id;
|
||||||
|
isActivated = true;
|
||||||
|
isRenaming = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isActivated;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut)
|
||||||
|
{
|
||||||
|
ImGui::SetItemTooltip(
|
||||||
|
"%s", std::vformat(localize.get(FORMAT_TOOLTIP_SHORTCUT), std::make_format_args(tooltip, shortcut)).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct CheckerStart
|
||||||
|
{
|
||||||
|
float position{};
|
||||||
|
long long index{};
|
||||||
|
};
|
||||||
|
|
||||||
|
CheckerStart checker_start(float minCoord, float offset, float step)
|
||||||
|
{
|
||||||
|
float world = minCoord + offset;
|
||||||
|
long long idx = static_cast<long long>(std::floor(world / step));
|
||||||
|
float first = minCoord - (world - static_cast<float>(idx) * step);
|
||||||
|
return {first, idx};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void render_checker_background(ImDrawList* drawList, ImVec2 min, ImVec2 max, vec2 offset, float step)
|
||||||
|
{
|
||||||
|
if (!drawList || step <= 0.0f) return;
|
||||||
|
|
||||||
|
const ImU32 colorLight = IM_COL32(204, 204, 204, 255);
|
||||||
|
const ImU32 colorDark = IM_COL32(128, 128, 128, 255);
|
||||||
|
|
||||||
|
auto [startY, rowIndex] = checker_start(min.y, offset.y, step);
|
||||||
|
for (float y = startY; y < max.y; y += step, ++rowIndex)
|
||||||
|
{
|
||||||
|
float y1 = glm::max(y, min.y);
|
||||||
|
float y2 = glm::min(y + step, max.y);
|
||||||
|
if (y2 <= y1) continue;
|
||||||
|
|
||||||
|
auto [startX, columnIndex] = checker_start(min.x, offset.x, step);
|
||||||
|
for (float x = startX; x < max.x; x += step, ++columnIndex)
|
||||||
|
{
|
||||||
|
float x1 = glm::max(x, min.x);
|
||||||
|
float x2 = glm::min(x + step, max.x);
|
||||||
|
if (x2 <= x1) continue;
|
||||||
|
|
||||||
|
bool isDark = ((rowIndex + columnIndex) & 1LL) != 0;
|
||||||
|
drawList->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), isDark ? colorDark : colorLight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected)
|
||||||
|
{
|
||||||
|
auto* storage = static_cast<MultiSelectStorage*>(self->UserData);
|
||||||
|
auto value = storage ? storage->resolve_index(id) : id;
|
||||||
|
if (isSelected)
|
||||||
|
storage->insert(value);
|
||||||
|
else
|
||||||
|
storage->erase(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 shortcut(ImGuiKeyChord chord, shortcut::Type type)
|
||||||
|
{
|
||||||
|
if (ImGui::GetTopMostPopupModal() != nullptr) return false;
|
||||||
|
|
||||||
|
int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal
|
||||||
|
: ImGuiInputFlags_RouteFocused;
|
||||||
|
flags |= ImGuiInputFlags_Repeat;
|
||||||
|
|
||||||
|
if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET)
|
||||||
|
{
|
||||||
|
ImGui::SetNextItemShortcut(chord, flags);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImGui::Shortcut(chord, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; }
|
||||||
|
|
||||||
|
void MultiSelectStorage::start(size_t size, ImGuiMultiSelectFlags flags)
|
||||||
|
{
|
||||||
|
internal.UserData = this;
|
||||||
|
|
||||||
|
io = ImGui::BeginMultiSelect(flags, this->size(), size);
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiSelectStorage::apply() { internal.ApplyRequests(io); }
|
||||||
|
|
||||||
|
void MultiSelectStorage::finish()
|
||||||
|
{
|
||||||
|
io = ImGui::EndMultiSelect();
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiSelectStorage::set_index_map(std::vector<int>* map) { indexMap = map; }
|
||||||
|
|
||||||
|
int MultiSelectStorage::resolve_index(int index) const
|
||||||
|
{
|
||||||
|
if (!indexMap) return index;
|
||||||
|
if (index < 0 || index >= (int)indexMap->size()) return index;
|
||||||
|
return (*indexMap)[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
PopupHelper::PopupHelper(StringType labelId, PopupType type, PopupPosition position)
|
||||||
|
{
|
||||||
|
this->labelId = labelId;
|
||||||
|
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(localize.get(labelId));
|
||||||
|
isTriggered = false;
|
||||||
|
|
||||||
|
auto viewport = ImGui::GetMainViewport();
|
||||||
|
|
||||||
|
switch (position)
|
||||||
|
{
|
||||||
|
case POPUP_CENTER:
|
||||||
|
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f)));
|
||||||
|
if (POPUP_IS_HEIGHT_SET[type])
|
||||||
|
ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * POPUP_MULTIPLIERS[type]));
|
||||||
|
else
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0));
|
||||||
|
break;
|
||||||
|
case POPUP_BY_ITEM:
|
||||||
|
ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None);
|
||||||
|
case POPUP_BY_CURSOR:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PopupHelper::end() { isJustOpened = false; }
|
||||||
|
|
||||||
|
void PopupHelper::close() { isOpen = false; }
|
||||||
|
}
|
||||||
235
src/imgui/imgui_.h
Normal file
235
src/imgui/imgui_.h
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <imgui/imgui.h>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "strings.h"
|
||||||
|
#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,
|
||||||
|
POPUP_BY_CURSOR
|
||||||
|
};
|
||||||
|
|
||||||
|
enum RenameState
|
||||||
|
{
|
||||||
|
RENAME_SELECTABLE,
|
||||||
|
RENAME_BEGIN,
|
||||||
|
RENAME_EDITING,
|
||||||
|
RENAME_FINISHED,
|
||||||
|
RENAME_FORCE_EDIT
|
||||||
|
};
|
||||||
|
|
||||||
|
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},
|
||||||
|
};
|
||||||
|
|
||||||
|
void theme_set(types::theme::Type theme);
|
||||||
|
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& label, const std::string& id, std::string& text, bool isSelected,
|
||||||
|
ImGuiSelectableFlags flags, RenameState& state);
|
||||||
|
void set_item_tooltip_shortcut(const char*, const std::string& = {});
|
||||||
|
void external_storage_set(ImGuiSelectionExternalStorage*, int, bool);
|
||||||
|
void render_checker_background(ImDrawList*, ImVec2, ImVec2, glm::vec2, float);
|
||||||
|
ImVec2 icon_size_get();
|
||||||
|
bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET);
|
||||||
|
|
||||||
|
class MultiSelectStorage : public std::set<int>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ImGuiSelectionExternalStorage internal{};
|
||||||
|
ImGuiMultiSelectIO* io{};
|
||||||
|
std::vector<int>* indexMap{};
|
||||||
|
|
||||||
|
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 apply();
|
||||||
|
void finish();
|
||||||
|
void set_index_map(std::vector<int>*);
|
||||||
|
int resolve_index(int) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PopupHelper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
StringType labelId{};
|
||||||
|
PopupType type{};
|
||||||
|
PopupPosition position{};
|
||||||
|
bool isOpen{};
|
||||||
|
bool isTriggered{};
|
||||||
|
bool isJustOpened{};
|
||||||
|
|
||||||
|
PopupHelper(StringType, PopupType = POPUP_NORMAL, PopupPosition = POPUP_CENTER);
|
||||||
|
const char* label() const { return localize.get(labelId); }
|
||||||
|
bool is_open();
|
||||||
|
void open();
|
||||||
|
void trigger();
|
||||||
|
void end();
|
||||||
|
void close();
|
||||||
|
};
|
||||||
|
}
|
||||||
991
src/imgui/taskbar.cpp
Normal file
991
src/imgui/taskbar.cpp
Normal file
@@ -0,0 +1,991 @@
|
|||||||
|
#include "taskbar.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cfloat>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <system_error>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <imgui/imgui.h>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
#include "math_.h"
|
||||||
|
#include "render.h"
|
||||||
|
#include "shader.h"
|
||||||
|
#include "snapshots.h"
|
||||||
|
#include "types.h"
|
||||||
|
|
||||||
|
#include "icon.h"
|
||||||
|
#include "toast.h"
|
||||||
|
|
||||||
|
#include "strings.h"
|
||||||
|
|
||||||
|
using namespace anm2ed::resource;
|
||||||
|
using namespace anm2ed::types;
|
||||||
|
using namespace anm2ed::canvas;
|
||||||
|
using namespace anm2ed::util;
|
||||||
|
using namespace glm;
|
||||||
|
|
||||||
|
namespace anm2ed::imgui
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
#ifndef _WIN32
|
||||||
|
constexpr auto EXEC_PERMS =
|
||||||
|
std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool ffmpeg_is_executable(const std::string& pathString)
|
||||||
|
{
|
||||||
|
if (pathString.empty()) return false;
|
||||||
|
|
||||||
|
std::error_code ec{};
|
||||||
|
auto status = std::filesystem::status(pathString, ec);
|
||||||
|
if (ec || !std::filesystem::is_regular_file(status)) return false;
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
if ((status.permissions() & EXEC_PERMS) == std::filesystem::perms::none) return false;
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool png_directory_ensure(const std::string& directory)
|
||||||
|
{
|
||||||
|
if (directory.empty())
|
||||||
|
{
|
||||||
|
toasts.push(localize.get(TOAST_PNG_DIRECTORY_NOT_SET));
|
||||||
|
logger.warning(localize.get(TOAST_PNG_DIRECTORY_NOT_SET, anm2ed::ENGLISH));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code ec{};
|
||||||
|
auto pathValue = std::filesystem::path(directory);
|
||||||
|
auto exists = std::filesystem::exists(pathValue, ec);
|
||||||
|
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
auto errorMessage = ec.message();
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_PNG_DIRECTORY_ACCESS_ERROR),
|
||||||
|
std::make_format_args(directory, errorMessage)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_PNG_DIRECTORY_ACCESS_ERROR, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(directory, errorMessage)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
if (!std::filesystem::is_directory(pathValue, ec) || ec)
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_PNG_DIRECTORY_NOT_DIRECTORY), std::make_format_args(directory)));
|
||||||
|
logger.warning(std::vformat(localize.get(TOAST_PNG_DIRECTORY_NOT_DIRECTORY, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(directory)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::filesystem::create_directories(pathValue, ec) || ec)
|
||||||
|
{
|
||||||
|
auto errorMessage = ec.message();
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_PNG_DIRECTORY_CREATE_ERROR),
|
||||||
|
std::make_format_args(directory, errorMessage)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_PNG_DIRECTORY_CREATE_ERROR, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(directory, errorMessage)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr auto CREDIT_DELAY = 1.0f;
|
||||||
|
static constexpr auto CREDIT_SCROLL_SPEED = 25.0f;
|
||||||
|
|
||||||
|
struct Credit
|
||||||
|
{
|
||||||
|
const char* string{};
|
||||||
|
font::Type font{font::REGULAR};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScrollingCredit
|
||||||
|
{
|
||||||
|
int index{};
|
||||||
|
float offset{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CreditsState
|
||||||
|
{
|
||||||
|
std::vector<ScrollingCredit> active{};
|
||||||
|
float spawnTimer{1.0f};
|
||||||
|
int nextIndex{};
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr Credit CREDITS[] = {
|
||||||
|
{"Anm2Ed", font::BOLD},
|
||||||
|
{"License: GPLv3"},
|
||||||
|
{""},
|
||||||
|
{"Designer", font::BOLD},
|
||||||
|
{"Shweet"},
|
||||||
|
{""},
|
||||||
|
{"Additional Help", font::BOLD},
|
||||||
|
{"im-tem"},
|
||||||
|
{""},
|
||||||
|
{"Localization", font::BOLD},
|
||||||
|
{"ExtremeThreat (Russian)"},
|
||||||
|
{"CxRedix (Chinese)"},
|
||||||
|
{"sawalk/사왈이 (Korean)"},
|
||||||
|
{""},
|
||||||
|
{"Based on the work of:", font::BOLD},
|
||||||
|
{"Adrian Gavrilita"},
|
||||||
|
{"Simon Parzer"},
|
||||||
|
{"Matt Kapuszczak"},
|
||||||
|
{""},
|
||||||
|
{"XM Music", font::BOLD},
|
||||||
|
{"Drozerix"},
|
||||||
|
{"\"Keygen Wraith\""},
|
||||||
|
{"https://modarchive.org/module.php?207854"},
|
||||||
|
{"License: CC0"},
|
||||||
|
{""},
|
||||||
|
{"Libraries", font::BOLD},
|
||||||
|
{"Dear ImGui"},
|
||||||
|
{"https://github.com/ocornut/imgui"},
|
||||||
|
{"License: MIT"},
|
||||||
|
{""},
|
||||||
|
{"SDL"},
|
||||||
|
{"https://github.com/libsdl-org/SDL"},
|
||||||
|
{"License: zlib"},
|
||||||
|
{""},
|
||||||
|
{"SDL_mixer"},
|
||||||
|
{"https://github.com/libsdl-org/SDL_mixer"},
|
||||||
|
{"License: zlib"},
|
||||||
|
{""},
|
||||||
|
{"tinyxml2"},
|
||||||
|
{"https://github.com/leethomason/tinyxml2"},
|
||||||
|
{"License: zlib"},
|
||||||
|
{""},
|
||||||
|
{"glm"},
|
||||||
|
{"https://github.com/g-truc/glm"},
|
||||||
|
{"License: MIT"},
|
||||||
|
{""},
|
||||||
|
{"lunasvg"},
|
||||||
|
{"https://github.com/sammycage/lunasvg"},
|
||||||
|
{"License: MIT"},
|
||||||
|
{""},
|
||||||
|
{"Icons", font::BOLD},
|
||||||
|
{"Remix Icons"},
|
||||||
|
{"remixicon.com"},
|
||||||
|
{"License: Apache"},
|
||||||
|
{""},
|
||||||
|
{"Font", font::BOLD},
|
||||||
|
{"Noto Sans"},
|
||||||
|
{"https://fonts.google.com/noto/specimen/Noto+Sans"},
|
||||||
|
{"License: OFL"},
|
||||||
|
{""},
|
||||||
|
{"Special Thanks", font::BOLD},
|
||||||
|
{"Edmund McMillen"},
|
||||||
|
{"Florian Himsl"},
|
||||||
|
{"Tyrone Rodriguez"},
|
||||||
|
{"The-Vinh Truong (_kilburn)"},
|
||||||
|
{"Everyone who waited patiently for this to be finished"},
|
||||||
|
{"Everyone else who has worked on The Binding of Isaac!"},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{"enjoy the jams :)"},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
{""},
|
||||||
|
};
|
||||||
|
static constexpr auto CREDIT_COUNT = (int)(sizeof(CREDITS) / sizeof(Credit));
|
||||||
|
|
||||||
|
Taskbar::Taskbar() : generate(vec2()) {}
|
||||||
|
|
||||||
|
void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting)
|
||||||
|
{
|
||||||
|
auto document = manager.get();
|
||||||
|
auto animation = document ? document->animation_get() : nullptr;
|
||||||
|
auto item = document ? document->item_get() : nullptr;
|
||||||
|
|
||||||
|
if (ImGui::BeginMainMenuBar())
|
||||||
|
{
|
||||||
|
height = ImGui::GetWindowSize().y;
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu(localize.get(LABEL_FILE_MENU)))
|
||||||
|
{
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_NEW), settings.shortcutNew.c_str())) dialog.file_save(dialog::ANM2_NEW);
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_OPEN), settings.shortcutOpen.c_str()))
|
||||||
|
dialog.file_open(dialog::ANM2_OPEN);
|
||||||
|
|
||||||
|
auto recentFiles = manager.recent_files_ordered();
|
||||||
|
if (ImGui::BeginMenu(localize.get(LABEL_OPEN_RECENT), !recentFiles.empty()))
|
||||||
|
{
|
||||||
|
for (std::size_t index = 0; index < recentFiles.size(); ++index)
|
||||||
|
{
|
||||||
|
const auto& file = recentFiles[index];
|
||||||
|
auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string());
|
||||||
|
|
||||||
|
ImGui::PushID((int)index);
|
||||||
|
if (ImGui::MenuItem(label.c_str())) manager.open(file.string());
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recentFiles.empty())
|
||||||
|
if (ImGui::MenuItem(localize.get(LABEL_CLEAR_LIST))) manager.recent_files_clear();
|
||||||
|
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_SAVE), settings.shortcutSave.c_str(), false, document))
|
||||||
|
{
|
||||||
|
if (settings.fileIsWarnOverwrite)
|
||||||
|
overwritePopup.open();
|
||||||
|
else
|
||||||
|
manager.save(document->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::MenuItem(localize.get(LABEL_SAVE_AS), settings.shortcutSaveAs.c_str(), false, document))
|
||||||
|
dialog.file_save(dialog::ANM2_SAVE);
|
||||||
|
if (ImGui::MenuItem(localize.get(LABEL_EXPLORE_XML_LOCATION), nullptr, false, document))
|
||||||
|
dialog.file_explorer_open(document->directory_get().string());
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::MenuItem(localize.get(LABEL_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(localize.get(LABEL_WIZARD_MENU)))
|
||||||
|
{
|
||||||
|
if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_GENERATE_ANIMATION_FROM_GRID), nullptr, false,
|
||||||
|
item && document->reference.itemType == anm2::LAYER))
|
||||||
|
generatePopup.open();
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_RENDER_ANIMATION), nullptr, false, animation))
|
||||||
|
renderPopup.open();
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu(localize.get(LABEL_PLAYBACK_MENU)))
|
||||||
|
{
|
||||||
|
ImGui::MenuItem(localize.get(LABEL_PLAYBACK_ALWAYS_LOOP), nullptr, &settings.playbackIsLoop);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PLAYBACK_ALWAYS_LOOP));
|
||||||
|
|
||||||
|
ImGui::MenuItem(localize.get(LABEL_CLAMP), nullptr, &settings.playbackIsClamp);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PLAYBACK_CLAMP));
|
||||||
|
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu(localize.get(LABEL_WINDOW_MENU)))
|
||||||
|
{
|
||||||
|
for (std::size_t index = 0; index < WINDOW_COUNT; ++index)
|
||||||
|
{
|
||||||
|
auto member = WINDOW_MEMBERS[index];
|
||||||
|
ImGui::MenuItem(localize.get(::anm2ed::WINDOW_STRING_TYPES[index]), nullptr, &(settings.*member));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu(localize.get(LABEL_SETTINGS_MENU)))
|
||||||
|
{
|
||||||
|
if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_CONFIGURE)))
|
||||||
|
{
|
||||||
|
editSettings = settings;
|
||||||
|
configurePopup.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu(localize.get(LABEL_HELP_MENU)))
|
||||||
|
{
|
||||||
|
|
||||||
|
if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_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.generateDuration;
|
||||||
|
auto& zoom = settings.generateZoom;
|
||||||
|
auto& zoomStep = settings.inputZoomStep;
|
||||||
|
|
||||||
|
auto childSize = ImVec2(row_widget_width_get(2), size_without_footer_get().y);
|
||||||
|
|
||||||
|
if (ImGui::BeginChild("##Options Child", childSize, ImGuiChildFlags_Borders))
|
||||||
|
{
|
||||||
|
ImGui::InputInt2(localize.get(LABEL_GENERATE_START_POSITION), value_ptr(startPosition));
|
||||||
|
ImGui::InputInt2(localize.get(LABEL_GENERATE_FRAME_SIZE), value_ptr(size));
|
||||||
|
ImGui::InputInt2(localize.get(BASIC_PIVOT), value_ptr(pivot));
|
||||||
|
ImGui::InputInt(localize.get(LABEL_GENERATE_ROWS), &rows, STEP, STEP_FAST);
|
||||||
|
ImGui::InputInt(localize.get(LABEL_GENERATE_COLUMNS), &columns, STEP, STEP_FAST);
|
||||||
|
|
||||||
|
input_int_range(localize.get(LABEL_GENERATE_COUNT), count, anm2::FRAME_NUM_MIN, rows * columns);
|
||||||
|
|
||||||
|
ImGui::InputInt(localize.get(BASIC_DURATION), &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(vec4(backgroundColor, 1.0f));
|
||||||
|
|
||||||
|
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(localize.get(LABEL_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, localize.get(EDIT_GENERATE_ANIMATION_FROM_GRID), Document::FRAMES,
|
||||||
|
generate_from_grid());
|
||||||
|
|
||||||
|
generatePopup.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) generatePopup.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(localize.get(LABEL_DISPLAY)))
|
||||||
|
{
|
||||||
|
if (ImGui::BeginChild("##Tab Child", childSize, true))
|
||||||
|
{
|
||||||
|
ImGui::SeparatorText(localize.get(LABEL_WINDOW_MENU));
|
||||||
|
input_float_range(localize.get(LABEL_UI_SCALE), editSettings.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f");
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_UI_SCALE));
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_VSYNC), &editSettings.isVsync);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_VSYNC));
|
||||||
|
|
||||||
|
ImGui::SeparatorText(localize.get(LABEL_LOCALIZATION));
|
||||||
|
ImGui::Combo(localize.get(LABEL_LANGUAGE), &editSettings.language, LANGUAGE_STRINGS, LANGUAGE_COUNT);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LANGUAGE));
|
||||||
|
|
||||||
|
ImGui::SeparatorText(localize.get(LABEL_THEME));
|
||||||
|
|
||||||
|
for (int i = 0; i < theme::COUNT; i++)
|
||||||
|
{
|
||||||
|
ImGui::RadioButton(localize.get(theme::STRINGS[i]), &editSettings.theme, i);
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginTabItem(localize.get(LABEL_FILE_MENU)))
|
||||||
|
{
|
||||||
|
if (ImGui::BeginChild("##Tab Child", childSize, true))
|
||||||
|
{
|
||||||
|
ImGui::SeparatorText(localize.get(LABEL_AUTOSAVE));
|
||||||
|
|
||||||
|
ImGui::Checkbox(localize.get(BASIC_ENABLED), &editSettings.fileIsAutosave);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTOSAVE_ENABLED));
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(!editSettings.fileIsAutosave);
|
||||||
|
input_int_range(localize.get(LABEL_TIME_MINUTES), editSettings.fileAutosaveTime, 0, 10);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTOSAVE_INTERVAL));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
ImGui::SeparatorText(localize.get(LABEL_SNAPSHOTS));
|
||||||
|
input_int_range(localize.get(LABEL_STACK_SIZE), editSettings.fileSnapshotStackSize, 0, 100);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_STACK_SIZE));
|
||||||
|
|
||||||
|
ImGui::SeparatorText(localize.get(LABEL_OPTIONS));
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_OVERWRITE_WARNING), &editSettings.fileIsWarnOverwrite);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERWRITE_WARNING));
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginTabItem(localize.get(LABEL_INPUT)))
|
||||||
|
{
|
||||||
|
if (ImGui::BeginChild("##Tab Child", childSize, true))
|
||||||
|
{
|
||||||
|
ImGui::SeparatorText(localize.get(LABEL_KEYBOARD));
|
||||||
|
|
||||||
|
input_float_range(localize.get(LABEL_REPEAT_DELAY), editSettings.keyboardRepeatDelay, 0.05f, 1.0f, 0.05f,
|
||||||
|
0.05f, "%.2f");
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPEAT_DELAY));
|
||||||
|
|
||||||
|
input_float_range(localize.get(LABEL_REPEAT_RATE), editSettings.keyboardRepeatRate, 0.005f, 1.0f, 0.005f,
|
||||||
|
0.005f, "%.3f");
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPEAT_DELAY));
|
||||||
|
|
||||||
|
ImGui::SeparatorText(localize.get(LABEL_ZOOM));
|
||||||
|
|
||||||
|
input_float_range(localize.get(LABEL_ZOOM_STEP), editSettings.inputZoomStep, 10.0f, 250.0f, 10.0f, 10.0f,
|
||||||
|
"%.0f%%");
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ZOOM_STEP));
|
||||||
|
|
||||||
|
ImGui::SeparatorText(localize.get(LABEL_TOOL));
|
||||||
|
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_MOVE_TOOL_SNAP), &editSettings.inputIsMoveToolSnapToMouse);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MOVE_TOOL_SNAP));
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginTabItem(localize.get(LABEL_SHORTCUTS_TAB)))
|
||||||
|
{
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||||
|
|
||||||
|
if (ImGui::BeginChild("##Tab Child", childSize, true))
|
||||||
|
{
|
||||||
|
if (ImGui::BeginTable(localize.get(LABEL_SHORTCUTS_TAB), 2,
|
||||||
|
ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY))
|
||||||
|
{
|
||||||
|
ImGui::TableSetupScrollFreeze(0, 1);
|
||||||
|
ImGui::TableSetupColumn(localize.get(LABEL_SHORTCUT_COLUMN));
|
||||||
|
ImGui::TableSetupColumn(localize.get(LABEL_VALUE_COLUMN));
|
||||||
|
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(localize.get(::anm2ed::SHORTCUT_STRING_TYPES[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 (const auto& entry : KEY_MAP)
|
||||||
|
{
|
||||||
|
auto key = entry.second;
|
||||||
|
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(localize.get(BASIC_SAVE), widgetSize))
|
||||||
|
{
|
||||||
|
settings = editSettings;
|
||||||
|
|
||||||
|
ImGui::GetIO().KeyRepeatDelay = settings.keyboardRepeatDelay;
|
||||||
|
ImGui::GetIO().KeyRepeatRate = settings.keyboardRepeatRate;
|
||||||
|
ImGui::GetStyle().FontScaleMain = settings.uiScale;
|
||||||
|
SnapshotStack::max_size_set(settings.fileSnapshotStackSize);
|
||||||
|
imgui::theme_set((theme::Type)settings.theme);
|
||||||
|
localize.language = (Language)settings.language;
|
||||||
|
manager.chords_set(settings);
|
||||||
|
|
||||||
|
for (auto& document : manager.documents)
|
||||||
|
document.snapshots.apply_limit();
|
||||||
|
|
||||||
|
configurePopup.close();
|
||||||
|
}
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SETTINGS_SAVE));
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(LABEL_USE_DEFAULT_SETTINGS), widgetSize)) editSettings = Settings();
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_USE_DEFAULT_SETTINGS));
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) configurePopup.close();
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CLOSE_SETTINGS));
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPopup.trigger();
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupModal(renderPopup.label(), &renderPopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||||
|
{
|
||||||
|
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& rows = settings.renderRows;
|
||||||
|
auto& columns = settings.renderColumns;
|
||||||
|
auto& isRange = manager.isRecordingRange;
|
||||||
|
auto& frames = document->frames.selection;
|
||||||
|
int length = std::max(1, end - start + 1);
|
||||||
|
|
||||||
|
auto range_to_frames_set = [&]()
|
||||||
|
{
|
||||||
|
if (auto item = document->item_get())
|
||||||
|
{
|
||||||
|
int duration{};
|
||||||
|
for (std::size_t index = 0; index < item->frames.size(); ++index)
|
||||||
|
{
|
||||||
|
const auto& frame = item->frames[index];
|
||||||
|
|
||||||
|
if ((int)index == *frames.begin())
|
||||||
|
start = duration;
|
||||||
|
else if ((int)index == *frames.rbegin())
|
||||||
|
{
|
||||||
|
end = duration;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
duration += frame.duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto range_to_animation_set = [&]()
|
||||||
|
{
|
||||||
|
start = 0;
|
||||||
|
end = animation->frameNum - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto range_set = [&]()
|
||||||
|
{
|
||||||
|
if (!isRange) range_to_animation_set();
|
||||||
|
length = std::max(1, end - (start + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
auto rows_columns_set = [&]()
|
||||||
|
{
|
||||||
|
auto framesNeeded = std::max(1, length);
|
||||||
|
int bestRows = 1;
|
||||||
|
int bestColumns = framesNeeded;
|
||||||
|
|
||||||
|
auto bestScore = std::make_tuple(bestColumns - bestRows, bestColumns * bestRows - framesNeeded, -bestColumns);
|
||||||
|
|
||||||
|
for (int candidateRows = 1; candidateRows <= framesNeeded; ++candidateRows)
|
||||||
|
{
|
||||||
|
int candidateColumns = (framesNeeded + candidateRows - 1) / candidateRows;
|
||||||
|
if (candidateColumns < candidateRows) break;
|
||||||
|
|
||||||
|
auto candidateScore = std::make_tuple(candidateColumns - candidateRows,
|
||||||
|
candidateColumns * candidateRows - framesNeeded, -candidateColumns);
|
||||||
|
|
||||||
|
if (candidateScore < bestScore)
|
||||||
|
{
|
||||||
|
bestScore = candidateScore;
|
||||||
|
bestRows = candidateRows;
|
||||||
|
bestColumns = candidateColumns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = bestRows;
|
||||||
|
columns = bestColumns;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto replace_extension = [&]()
|
||||||
|
{ path = std::filesystem::path(path).replace_extension(render::EXTENSIONS[type]).string(); };
|
||||||
|
|
||||||
|
auto render_set = [&]()
|
||||||
|
{
|
||||||
|
replace_extension();
|
||||||
|
range_set();
|
||||||
|
rows_columns_set();
|
||||||
|
};
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
auto dialogType = type == render::PNGS ? dialog::PNG_DIRECTORY_SET
|
||||||
|
: type == render::SPRITESHEET ? dialog::PNG_PATH_SET
|
||||||
|
: type == render::GIF ? dialog::GIF_PATH_SET
|
||||||
|
: type == render::WEBM ? dialog::WEBM_PATH_SET
|
||||||
|
: dialog::NONE;
|
||||||
|
|
||||||
|
if (renderPopup.isJustOpened) render_set();
|
||||||
|
|
||||||
|
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(localize.get(LABEL_FFMPEG_PATH), &ffmpegPath);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FFMPEG_PATH));
|
||||||
|
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();
|
||||||
|
auto pathLabel = type == render::PNGS ? LABEL_OUTPUT_DIRECTORY : LABEL_OUTPUT_PATH;
|
||||||
|
input_text_string(localize.get(pathLabel), &path);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OUTPUT_PATH));
|
||||||
|
dialog.set_string_to_selected_path(path, dialogType);
|
||||||
|
|
||||||
|
if (ImGui::Combo(localize.get(LABEL_TYPE), &type, render::STRINGS, render::COUNT)) render_set();
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RENDER_TYPE));
|
||||||
|
|
||||||
|
if (type == render::PNGS || type == render::SPRITESHEET) ImGui::Separator();
|
||||||
|
|
||||||
|
if (type == render::PNGS)
|
||||||
|
{
|
||||||
|
if (input_text_string(localize.get(LABEL_FORMAT), &format))
|
||||||
|
format = std::filesystem::path(format).replace_extension(".png").string();
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FORMAT));
|
||||||
|
}
|
||||||
|
else if (type == render::SPRITESHEET)
|
||||||
|
{
|
||||||
|
input_int_range(localize.get(LABEL_GENERATE_ROWS), rows, 1, length);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROWS));
|
||||||
|
|
||||||
|
input_int_range(localize.get(LABEL_GENERATE_COLUMNS), columns, 1, length);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COLUMNS));
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(LABEL_SET_TO_RECOMMENDED))) rows_columns_set();
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SET_TO_RECOMMENDED));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (ImGui::Checkbox(localize.get(LABEL_CUSTOM_RANGE), &isRange))
|
||||||
|
{
|
||||||
|
range_set();
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CUSTOM_RANGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(frames.empty());
|
||||||
|
if (ImGui::Button(localize.get(LABEL_TO_SELECTED_FRAMES))) range_to_frames_set();
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TO_SELECTED_FRAMES));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(LABEL_TO_ANIMATION_RANGE))) range_to_animation_set();
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TO_ANIMATION_RANGE));
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(!isRange);
|
||||||
|
{
|
||||||
|
input_int_range(localize.get(LABEL_START), start, 0, animation->frameNum);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_START));
|
||||||
|
input_int_range(localize.get(LABEL_END), end, start, animation->frameNum);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_END));
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_RAW), &isRaw);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RAW));
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(!isRaw);
|
||||||
|
{
|
||||||
|
input_float_range(localize.get(BASIC_SCALE), scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx");
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SCALE_OUTPUT));
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_SOUND), &settings.timelineIsSound);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SOUND));
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(LABEL_RENDER), widgetSize))
|
||||||
|
{
|
||||||
|
bool isRender = true;
|
||||||
|
if (!ffmpeg_is_executable(ffmpegPath))
|
||||||
|
{
|
||||||
|
toasts.push(localize.get(TOAST_INVALID_FFMPEG_PATH));
|
||||||
|
logger.error(localize.get(TOAST_INVALID_FFMPEG_PATH, anm2ed::ENGLISH));
|
||||||
|
isRender = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRender && type == render::PNGS) isRender = png_directory_ensure(path);
|
||||||
|
|
||||||
|
if (isRender)
|
||||||
|
{
|
||||||
|
manager.isRecordingStart = true;
|
||||||
|
manager.progressPopup.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPopup.close();
|
||||||
|
}
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RENDER_BUTTON));
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) renderPopup.close();
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPopup.end();
|
||||||
|
|
||||||
|
aboutPopup.trigger();
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupModal(aboutPopup.label(), &aboutPopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||||
|
{
|
||||||
|
static CreditsState creditsState{};
|
||||||
|
|
||||||
|
auto credits_reset = [&]()
|
||||||
|
{
|
||||||
|
resources.music_track().play(true);
|
||||||
|
creditsState = {};
|
||||||
|
creditsState.spawnTimer = CREDIT_DELAY;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (aboutPopup.isJustOpened) credits_reset();
|
||||||
|
|
||||||
|
auto size = ImGui::GetContentRegionAvail();
|
||||||
|
auto applicationLabel = localize.get(LABEL_APPLICATION_NAME);
|
||||||
|
auto versionLabel = localize.get(LABEL_APPLICATION_VERSION);
|
||||||
|
|
||||||
|
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE);
|
||||||
|
|
||||||
|
ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(applicationLabel).x) / 2);
|
||||||
|
ImGui::TextUnformatted(applicationLabel);
|
||||||
|
|
||||||
|
ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(versionLabel).x) / 2);
|
||||||
|
ImGui::TextUnformatted(versionLabel);
|
||||||
|
|
||||||
|
ImGui::PopFont();
|
||||||
|
|
||||||
|
auto creditRegionPos = ImGui::GetCursorScreenPos();
|
||||||
|
auto creditRegionSize = ImGui::GetContentRegionAvail();
|
||||||
|
|
||||||
|
if (creditRegionSize.y > 0.0f && creditRegionSize.x > 0.0f)
|
||||||
|
{
|
||||||
|
auto fontSize = ImGui::GetFontSize();
|
||||||
|
auto drawList = ImGui::GetWindowDrawList();
|
||||||
|
auto clipMax = ImVec2(creditRegionPos.x + creditRegionSize.x, creditRegionPos.y + creditRegionSize.y);
|
||||||
|
drawList->PushClipRect(creditRegionPos, clipMax, true);
|
||||||
|
|
||||||
|
auto delta = ImGui::GetIO().DeltaTime;
|
||||||
|
creditsState.spawnTimer -= delta;
|
||||||
|
auto maxVisible = std::max(1, (int)std::floor(creditRegionSize.y / (float)fontSize));
|
||||||
|
|
||||||
|
while (creditsState.active.size() < (size_t)maxVisible && creditsState.spawnTimer <= 0.0f)
|
||||||
|
{
|
||||||
|
creditsState.active.push_back({creditsState.nextIndex, 0.0f});
|
||||||
|
creditsState.nextIndex = (creditsState.nextIndex + 1) % CREDIT_COUNT;
|
||||||
|
creditsState.spawnTimer += CREDIT_DELAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto baseY = clipMax.y - (float)fontSize;
|
||||||
|
const auto& baseColor = ImGui::GetStyleColorVec4(ImGuiCol_Text);
|
||||||
|
auto fadeSpan = (float)fontSize * 2.0f;
|
||||||
|
|
||||||
|
for (auto it = creditsState.active.begin(); it != creditsState.active.end();)
|
||||||
|
{
|
||||||
|
it->offset += CREDIT_SCROLL_SPEED * delta;
|
||||||
|
auto yPos = baseY - it->offset;
|
||||||
|
if (yPos + fontSize < creditRegionPos.y)
|
||||||
|
{
|
||||||
|
it = creditsState.active.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& credit = CREDITS[it->index];
|
||||||
|
auto fontPtr = resources.fonts[credit.font].get();
|
||||||
|
auto textSize = fontPtr->CalcTextSizeA((float)fontSize, FLT_MAX, 0.0f, credit.string);
|
||||||
|
auto xPos = creditRegionPos.x + (creditRegionSize.x - textSize.x) * 0.5f;
|
||||||
|
|
||||||
|
auto alpha = 1.0f;
|
||||||
|
auto topDist = yPos - creditRegionPos.y;
|
||||||
|
if (topDist < fadeSpan) alpha *= std::clamp(topDist / fadeSpan, 0.0f, 1.0f);
|
||||||
|
auto bottomDist = (creditRegionPos.y + creditRegionSize.y) - (yPos + fontSize);
|
||||||
|
if (bottomDist < fadeSpan) alpha *= std::clamp(bottomDist / fadeSpan, 0.0f, 1.0f);
|
||||||
|
if (alpha <= 0.0f)
|
||||||
|
{
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto color = baseColor;
|
||||||
|
color.w *= alpha;
|
||||||
|
|
||||||
|
drawList->AddText(fontPtr, fontSize, ImVec2(xPos, yPos), ImGui::GetColorU32(color), credit.string);
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawList->PopClipRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto* music = resources.music_track_if_loaded())
|
||||||
|
if (music->is_playing() && !aboutPopup.isOpen) music->stop();
|
||||||
|
|
||||||
|
overwritePopup.trigger();
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupModal(overwritePopup.label(), &overwritePopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted(localize.get(LABEL_OVERWRITE_CONFIRMATION));
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
|
||||||
|
{
|
||||||
|
manager.save();
|
||||||
|
overwritePopup.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_NO), widgetSize)) overwritePopup.close();
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
aboutPopup.end();
|
||||||
|
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_NEW], shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_NEW);
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_OPEN], shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_OPEN);
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_SAVE], shortcut::GLOBAL))
|
||||||
|
{
|
||||||
|
if (settings.fileIsWarnOverwrite)
|
||||||
|
overwritePopup.open();
|
||||||
|
else
|
||||||
|
manager.save();
|
||||||
|
}
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_SAVE_AS], shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_SAVE);
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_EXIT], shortcut::GLOBAL)) isQuitting = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/imgui/taskbar.h
Normal file
35
src/imgui/taskbar.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "canvas.h"
|
||||||
|
#include "dialog.h"
|
||||||
|
#include "imgui_.h"
|
||||||
|
#include "manager.h"
|
||||||
|
#include "resources.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "strings.h"
|
||||||
|
|
||||||
|
namespace anm2ed::imgui
|
||||||
|
{
|
||||||
|
class Taskbar
|
||||||
|
{
|
||||||
|
Canvas generate;
|
||||||
|
float generateTime{};
|
||||||
|
PopupHelper generatePopup{PopupHelper(LABEL_TASKBAR_GENERATE_ANIMATION_FROM_GRID)};
|
||||||
|
PopupHelper overwritePopup{
|
||||||
|
PopupHelper(LABEL_TASKBAR_OVERWRITE_FILE, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||||
|
PopupHelper renderPopup{
|
||||||
|
PopupHelper(LABEL_TASKBAR_RENDER_ANIMATION, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||||
|
PopupHelper configurePopup{PopupHelper(LABEL_TASKBAR_CONFIGURE)};
|
||||||
|
PopupHelper aboutPopup{PopupHelper(LABEL_TASKBAR_ABOUT)};
|
||||||
|
Settings editSettings{};
|
||||||
|
int selectedShortcut{-1};
|
||||||
|
int creditsIndex{};
|
||||||
|
bool isQuittingMode{};
|
||||||
|
|
||||||
|
public:
|
||||||
|
float height{};
|
||||||
|
|
||||||
|
Taskbar();
|
||||||
|
void update(Manager&, Settings&, Resources&, Dialog&, bool&);
|
||||||
|
};
|
||||||
|
};
|
||||||
72
src/imgui/toast.cpp
Normal file
72
src/imgui/toast.cpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#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::push(const std::string& message) { toasts.emplace_back(Toast(message)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
anm2ed::imgui::Toasts toasts;
|
||||||
28
src/imgui/toast.h
Normal file
28
src/imgui/toast.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#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 push(const std::string&);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extern anm2ed::imgui::Toasts toasts;
|
||||||
864
src/imgui/window/animation_preview.cpp
Normal file
864
src/imgui/window/animation_preview.cpp
Normal file
@@ -0,0 +1,864 @@
|
|||||||
|
#include "animation_preview.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <optional>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
|
||||||
|
#include "imgui_.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "math_.h"
|
||||||
|
#include "strings.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 anm2ed::resource::texture;
|
||||||
|
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_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.5f);
|
||||||
|
constexpr auto TRIGGER_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.5f);
|
||||||
|
|
||||||
|
AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
|
||||||
|
|
||||||
|
void AnimationPreview::tick(Manager& manager, Settings& settings)
|
||||||
|
{
|
||||||
|
auto& document = *manager.get();
|
||||||
|
auto& anm2 = document.anm2;
|
||||||
|
auto& playback = document.playback;
|
||||||
|
auto& frameTime = document.frameTime;
|
||||||
|
auto& end = manager.recordingEnd;
|
||||||
|
auto& zoom = document.previewZoom;
|
||||||
|
auto& overlayIndex = document.overlayIndex;
|
||||||
|
auto& pan = document.previewPan;
|
||||||
|
|
||||||
|
if (manager.isRecording)
|
||||||
|
{
|
||||||
|
auto& ffmpegPath = settings.renderFFmpegPath;
|
||||||
|
auto& path = settings.renderPath;
|
||||||
|
auto& type = settings.renderType;
|
||||||
|
|
||||||
|
if (playback.time > end || playback.isFinished)
|
||||||
|
{
|
||||||
|
if (type == render::PNGS)
|
||||||
|
{
|
||||||
|
auto& format = settings.renderFormat;
|
||||||
|
bool isSuccess{true};
|
||||||
|
for (auto [i, frame] : std::views::enumerate(renderFrames))
|
||||||
|
{
|
||||||
|
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.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES), std::make_format_args(path)));
|
||||||
|
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(path)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED),
|
||||||
|
std::make_format_args(path)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(path)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == render::SPRITESHEET)
|
||||||
|
{
|
||||||
|
auto& rows = settings.renderRows;
|
||||||
|
auto& columns = settings.renderColumns;
|
||||||
|
|
||||||
|
if (renderFrames.empty())
|
||||||
|
{
|
||||||
|
toasts.push(localize.get(TOAST_SPRITESHEET_NO_FRAMES));
|
||||||
|
logger.warning(localize.get(TOAST_SPRITESHEET_NO_FRAMES, anm2ed::ENGLISH));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto& firstFrame = renderFrames.front();
|
||||||
|
if (firstFrame.size.x <= 0 || firstFrame.size.y <= 0 || firstFrame.pixels.empty())
|
||||||
|
{
|
||||||
|
toasts.push(localize.get(TOAST_SPRITESHEET_EMPTY));
|
||||||
|
logger.error(localize.get(TOAST_SPRITESHEET_EMPTY, anm2ed::ENGLISH));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto frameWidth = firstFrame.size.x;
|
||||||
|
auto frameHeight = firstFrame.size.y;
|
||||||
|
ivec2 spritesheetSize = ivec2(frameWidth * columns, frameHeight * rows);
|
||||||
|
|
||||||
|
std::vector<uint8_t> spritesheet((size_t)(spritesheetSize.x) * spritesheetSize.y * CHANNELS);
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < renderFrames.size(); ++index)
|
||||||
|
{
|
||||||
|
const auto& frame = renderFrames[index];
|
||||||
|
auto row = (int)(index / columns);
|
||||||
|
auto column = (int)(index % columns);
|
||||||
|
if (row >= rows || column >= columns) break;
|
||||||
|
if ((int)frame.pixels.size() < frameWidth * frameHeight * CHANNELS) continue;
|
||||||
|
|
||||||
|
for (int y = 0; y < frameHeight; ++y)
|
||||||
|
{
|
||||||
|
auto destY = (size_t)(row * frameHeight + y);
|
||||||
|
auto destX = (size_t)(column * frameWidth);
|
||||||
|
auto destOffset = (destY * spritesheetSize.x + destX) * CHANNELS;
|
||||||
|
auto srcOffset = (size_t)(y * frameWidth) * CHANNELS;
|
||||||
|
std::copy_n(frame.pixels.data() + srcOffset, frameWidth * CHANNELS, spritesheet.data() + destOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture spritesheetTexture(spritesheet.data(), spritesheetSize);
|
||||||
|
if (spritesheetTexture.write_png(path))
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET), std::make_format_args(path)));
|
||||||
|
logger.info(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(path)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED),
|
||||||
|
std::make_format_args(path)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(path)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (animation_render(ffmpegPath, path, renderFrames, audioStream, (render::Type)type, size, anm2.info.fps))
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(path)));
|
||||||
|
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(path)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED),
|
||||||
|
std::make_format_args(path)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(path)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFrames.clear();
|
||||||
|
|
||||||
|
if (settings.renderIsRawAnimation)
|
||||||
|
{
|
||||||
|
|
||||||
|
settings = savedSettings;
|
||||||
|
|
||||||
|
pan = savedPan;
|
||||||
|
zoom = savedZoom;
|
||||||
|
overlayIndex = savedOverlayIndex;
|
||||||
|
isSizeTrySet = true;
|
||||||
|
hasPendingZoomPanAdjust = false;
|
||||||
|
isCheckerPanInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.timelineIsSound) audioStream.capture_end(mixer);
|
||||||
|
|
||||||
|
playback.isPlaying = false;
|
||||||
|
playback.isFinished = false;
|
||||||
|
manager.isRecording = false;
|
||||||
|
manager.progressPopup.close();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
bind();
|
||||||
|
auto pixels = pixels_get();
|
||||||
|
renderFrames.push_back(Texture(pixels.data(), size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playback.isPlaying)
|
||||||
|
{
|
||||||
|
auto animation = document.animation_get();
|
||||||
|
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.isVisible)
|
||||||
|
if (anm2.content.sounds.contains(trigger.soundID))
|
||||||
|
anm2.content.sounds[trigger.soundID].audio.play(false, mixer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playback.tick(anm2.info.fps, animation->frameNum,
|
||||||
|
(animation->isLoop || settings.playbackIsLoop) && !manager.isRecording);
|
||||||
|
|
||||||
|
frameTime = playback.time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.inputZoomStep;
|
||||||
|
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];
|
||||||
|
bool isLightTheme = settings.theme == theme::LIGHT;
|
||||||
|
auto& shaderAxes = resources.shaders[shader::AXIS];
|
||||||
|
auto& shaderGrid = resources.shaders[shader::GRID];
|
||||||
|
auto& shaderTexture = resources.shaders[shader::TEXTURE];
|
||||||
|
|
||||||
|
auto reset_checker_pan = [&]()
|
||||||
|
{
|
||||||
|
checkerPan = pan;
|
||||||
|
checkerSyncPan = pan;
|
||||||
|
checkerSyncZoom = zoom;
|
||||||
|
isCheckerPanInitialized = true;
|
||||||
|
hasPendingZoomPanAdjust = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto sync_checker_pan = [&]()
|
||||||
|
{
|
||||||
|
if (!isCheckerPanInitialized)
|
||||||
|
{
|
||||||
|
reset_checker_pan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pan != checkerSyncPan || zoom != checkerSyncZoom)
|
||||||
|
{
|
||||||
|
bool ignorePanDelta = hasPendingZoomPanAdjust && zoom != checkerSyncZoom;
|
||||||
|
if (!ignorePanDelta) checkerPan += pan - checkerSyncPan;
|
||||||
|
checkerSyncPan = pan;
|
||||||
|
checkerSyncZoom = zoom;
|
||||||
|
if (ignorePanDelta) hasPendingZoomPanAdjust = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto center_view = [&]() { pan = vec2(); };
|
||||||
|
|
||||||
|
if (ImGui::Begin(localize.get(LABEL_ANIMATION_PREVIEW_WINDOW), &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(localize.get(BASIC_GRID), &isGrid);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_GRID_VISIBILITY));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::ColorEdit4(localize.get(BASIC_COLOR), value_ptr(gridColor), ImGuiColorEditFlags_NoInputs);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_GRID_COLOR));
|
||||||
|
|
||||||
|
input_int2_range(localize.get(BASIC_SIZE), gridSize, ivec2(GRID_SIZE_MIN), ivec2(GRID_SIZE_MAX));
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_GRID_SIZE));
|
||||||
|
|
||||||
|
input_int2_range(localize.get(BASIC_OFFSET), gridOffset, ivec2(GRID_OFFSET_MIN), ivec2(GRID_OFFSET_MAX));
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_GRID_OFFSET));
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::BeginChild("##View Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
|
||||||
|
{
|
||||||
|
ImGui::InputFloat(localize.get(BASIC_ZOOM), &zoom, zoomStep, zoomStep, "%.0f%%");
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PREVIEW_ZOOM));
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_CENTER_VIEW]);
|
||||||
|
if (ImGui::Button(localize.get(LABEL_CENTER_VIEW), widgetSize)) pan = vec2();
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_CENTER_VIEW), settings.shortcutCenterView);
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_FIT]);
|
||||||
|
if (ImGui::Button(localize.get(LABEL_FIT), widgetSize))
|
||||||
|
if (animation) set_to_rect(zoom, pan, animation->rect(isRootTransform));
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_FIT), settings.shortcutFit);
|
||||||
|
|
||||||
|
auto mousePosInt = ivec2(mousePos);
|
||||||
|
ImGui::TextUnformatted(
|
||||||
|
std::vformat(localize.get(FORMAT_POSITION_SPACED), std::make_format_args(mousePosInt.x, mousePosInt.y))
|
||||||
|
.c_str());
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
|
||||||
|
{
|
||||||
|
ImGui::ColorEdit3(localize.get(LABEL_BACKGROUND_COLOR), value_ptr(backgroundColor),
|
||||||
|
ImGuiColorEditFlags_NoInputs);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_BACKGROUND_COLOR));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_AXES), &isAxes);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AXES));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::ColorEdit4(localize.get(BASIC_COLOR), value_ptr(axesColor), ImGuiColorEditFlags_NoInputs);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AXES_COLOR));
|
||||||
|
|
||||||
|
combo_negative_one_indexed(localize.get(LABEL_OVERLAY), &overlayIndex, document.animation.labels);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERLAY));
|
||||||
|
|
||||||
|
ImGui::InputFloat(localize.get(BASIC_ALPHA), &overlayTransparency, 0, 0, "%.0f");
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERLAY_ALPHA));
|
||||||
|
}
|
||||||
|
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(localize.get(LABEL_ROOT_TRANSFORM), &isRootTransform);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROOT_TRANSFORM));
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_PIVOTS), &isPivots);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PIVOTS));
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::BeginChild("##Helpers Child 2", helpersChildSize))
|
||||||
|
{
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_ALT_ICONS), &isAltIcons);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ALT_ICONS));
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_BORDER), &isBorder);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_BORDER));
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
auto cursorScreenPos = ImGui::GetCursorScreenPos();
|
||||||
|
auto min = cursorScreenPos;
|
||||||
|
auto max = to_imvec2(to_vec2(min) + size);
|
||||||
|
|
||||||
|
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.previewIsBorder = false;
|
||||||
|
settings.timelineIsOnlyShowLayers = true;
|
||||||
|
settings.onionskinIsEnabled = false;
|
||||||
|
|
||||||
|
savedOverlayIndex = overlayIndex;
|
||||||
|
savedZoom = zoom;
|
||||||
|
savedPan = pan;
|
||||||
|
|
||||||
|
if (auto rect = document.animation_get()->rect(isRootTransform); rect != vec4(-1.0f))
|
||||||
|
{
|
||||||
|
size_set(vec2(rect.z, rect.w) * settings.renderScale);
|
||||||
|
set_to_rect(zoom, pan, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSizeTrySet = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.isRecordingStart = false;
|
||||||
|
manager.isRecording = true;
|
||||||
|
playback.isPlaying = true;
|
||||||
|
playback.time = manager.recordingStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail()));
|
||||||
|
|
||||||
|
bind();
|
||||||
|
viewport_set();
|
||||||
|
clear(manager.isRecording && settings.renderIsRawAnimation ? vec4() : vec4(backgroundColor, 1.0f));
|
||||||
|
|
||||||
|
if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor);
|
||||||
|
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
|
||||||
|
|
||||||
|
auto baseTransform = transform_get(zoom, pan);
|
||||||
|
auto frameTime = document.frameTime > -1 && !playback.isPlaying ? document.frameTime : playback.time;
|
||||||
|
|
||||||
|
struct OnionskinSample
|
||||||
|
{
|
||||||
|
float time{};
|
||||||
|
int indexOffset{};
|
||||||
|
vec3 colorOffset{};
|
||||||
|
float alphaOffset{};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<OnionskinSample> onionskinSamples;
|
||||||
|
|
||||||
|
if (animation && settings.onionskinIsEnabled)
|
||||||
|
{
|
||||||
|
auto add_samples = [&](int count, int direction, vec3 color)
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= count; ++i)
|
||||||
|
{
|
||||||
|
float useTime = frameTime + (float)(direction * i);
|
||||||
|
|
||||||
|
float alphaOffset = (1.0f / (count + 1)) * i;
|
||||||
|
OnionskinSample sample{};
|
||||||
|
sample.time = useTime;
|
||||||
|
sample.colorOffset = color;
|
||||||
|
sample.alphaOffset = alphaOffset;
|
||||||
|
sample.indexOffset = direction * i;
|
||||||
|
onionskinSamples.push_back(sample);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
add_samples(settings.onionskinBeforeCount, -1, settings.onionskinBeforeColor);
|
||||||
|
add_samples(settings.onionskinAfterCount, 1, settings.onionskinAfterColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto render = [&](anm2::Animation* animation, float time, vec3 colorOffset = {}, float alphaOffset = {},
|
||||||
|
const std::vector<OnionskinSample>* layeredOnions = nullptr, bool isIndexMode = false)
|
||||||
|
{
|
||||||
|
auto sample_time_for_item = [&](anm2::Item& item, const OnionskinSample& sample) -> std::optional<float>
|
||||||
|
{
|
||||||
|
if (!isIndexMode)
|
||||||
|
{
|
||||||
|
if (sample.time < 0.0f || sample.time > animation->frameNum) return std::nullopt;
|
||||||
|
return sample.time;
|
||||||
|
}
|
||||||
|
if (item.frames.empty()) return std::nullopt;
|
||||||
|
int baseIndex = item.frame_index_from_time_get(frameTime);
|
||||||
|
if (baseIndex < 0) return std::nullopt;
|
||||||
|
int sampleIndex = baseIndex + sample.indexOffset;
|
||||||
|
if (sampleIndex < 0 || sampleIndex >= (int)item.frames.size()) return std::nullopt;
|
||||||
|
return item.frame_time_from_index_get(sampleIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto transform_for_time = [&](anm2::Animation* anim, float t)
|
||||||
|
{
|
||||||
|
auto sampleTransform = baseTransform;
|
||||||
|
if (isRootTransform)
|
||||||
|
{
|
||||||
|
auto rootFrame = anim->rootAnimation.frame_generate(t, anm2::ROOT);
|
||||||
|
sampleTransform *= math::quad_model_parent_get(rootFrame.position, {},
|
||||||
|
math::percent_to_unit(rootFrame.scale), rootFrame.rotation);
|
||||||
|
}
|
||||||
|
return sampleTransform;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto transform = transform_for_time(animation, time);
|
||||||
|
|
||||||
|
auto draw_root =
|
||||||
|
[&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion)
|
||||||
|
{
|
||||||
|
auto rootFrame = animation->rootAnimation.frame_generate(sampleTime, anm2::ROOT);
|
||||||
|
if (isOnlyShowLayers || !rootFrame.isVisible || !animation->rootAnimation.isVisible) return;
|
||||||
|
|
||||||
|
auto rootModel = isRootTransform
|
||||||
|
? math::quad_model_get(TARGET_SIZE, {}, TARGET_SIZE * 0.5f)
|
||||||
|
: math::quad_model_get(TARGET_SIZE, rootFrame.position, TARGET_SIZE * 0.5f,
|
||||||
|
math::percent_to_unit(rootFrame.scale), rootFrame.rotation);
|
||||||
|
auto rootTransform = sampleTransform * rootModel;
|
||||||
|
|
||||||
|
vec4 color = isOnion ? vec4(sampleColor, sampleAlpha) : color::GREEN;
|
||||||
|
|
||||||
|
auto icon = isAltIcons ? icon::TARGET_ALT : icon::TARGET;
|
||||||
|
texture_render(shaderTexture, resources.icons[icon].id, rootTransform, color);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layeredOnions)
|
||||||
|
for (auto& sample : *layeredOnions)
|
||||||
|
if (auto sampleTime = sample_time_for_item(animation->rootAnimation, sample))
|
||||||
|
{
|
||||||
|
auto sampleTransform = transform_for_time(animation, *sampleTime);
|
||||||
|
draw_root(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_root(time, transform, {}, 0.0f, false);
|
||||||
|
|
||||||
|
for (auto& id : animation->layerOrder)
|
||||||
|
{
|
||||||
|
auto& layerAnimation = animation->layerAnimations[id];
|
||||||
|
if (!layerAnimation.isVisible) continue;
|
||||||
|
|
||||||
|
auto& layer = anm2.content.layers.at(id);
|
||||||
|
|
||||||
|
auto spritesheet = anm2.spritesheet_get(layer.spritesheetID);
|
||||||
|
if (!spritesheet || !spritesheet->is_valid()) continue;
|
||||||
|
|
||||||
|
auto draw_layer =
|
||||||
|
[&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion)
|
||||||
|
{
|
||||||
|
if (auto frame = layerAnimation.frame_generate(sampleTime, anm2::LAYER); frame.isVisible)
|
||||||
|
{
|
||||||
|
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 = sampleTransform * layerModel;
|
||||||
|
|
||||||
|
auto texSize = vec2(texture.size);
|
||||||
|
if (texSize.x <= 0.0f || texSize.y <= 0.0f) return;
|
||||||
|
|
||||||
|
auto uvMin = frame.crop / texSize;
|
||||||
|
auto uvMax = (frame.crop + frame.size) / texSize;
|
||||||
|
vec3 frameColorOffset = frame.colorOffset + colorOffset + sampleColor;
|
||||||
|
vec4 frameTint = frame.tint;
|
||||||
|
frameTint.a = std::max(0.0f, frameTint.a - (alphaOffset + sampleAlpha));
|
||||||
|
|
||||||
|
auto inset = vec2(0.5f) / texSize;
|
||||||
|
uvMin += inset;
|
||||||
|
uvMax -= inset;
|
||||||
|
auto vertices = math::uv_vertices_get(uvMin, uvMax);
|
||||||
|
|
||||||
|
texture_render(shaderTexture, texture.id, layerTransform, frameTint, frameColorOffset, vertices.data());
|
||||||
|
|
||||||
|
auto color = isOnion ? vec4(sampleColor, 1.0f - sampleAlpha) : 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 = sampleTransform * pivotModel;
|
||||||
|
|
||||||
|
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layeredOnions)
|
||||||
|
for (auto& sample : *layeredOnions)
|
||||||
|
if (auto sampleTime = sample_time_for_item(layerAnimation, sample))
|
||||||
|
{
|
||||||
|
auto sampleTransform = transform_for_time(animation, *sampleTime);
|
||||||
|
draw_layer(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_layer(time, transform, {}, 0.0f, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [id, nullAnimation] : animation->nullAnimations)
|
||||||
|
{
|
||||||
|
if (!nullAnimation.isVisible || isOnlyShowLayers) continue;
|
||||||
|
|
||||||
|
auto& isShowRect = anm2.content.nulls[id].isShowRect;
|
||||||
|
|
||||||
|
auto draw_null =
|
||||||
|
[&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion)
|
||||||
|
{
|
||||||
|
if (auto frame = nullAnimation.frame_generate(sampleTime, anm2::NULL_); frame.isVisible)
|
||||||
|
{
|
||||||
|
auto icon = isShowRect ? icon::POINT : isAltIcons ? icon::TARGET_ALT : icon::TARGET;
|
||||||
|
|
||||||
|
auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE;
|
||||||
|
auto color = isOnion ? vec4(sampleColor, 1.0f - sampleAlpha)
|
||||||
|
: 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 = sampleTransform * 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 = sampleTransform * rectModel;
|
||||||
|
|
||||||
|
rect_render(shaderLine, rectTransform, rectModel, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layeredOnions)
|
||||||
|
for (auto& sample : *layeredOnions)
|
||||||
|
if (auto sampleTime = sample_time_for_item(nullAnimation, sample))
|
||||||
|
{
|
||||||
|
auto sampleTransform = transform_for_time(animation, *sampleTime);
|
||||||
|
draw_null(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_null(time, transform, {}, 0.0f, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (animation)
|
||||||
|
{
|
||||||
|
auto layeredOnions = settings.onionskinIsEnabled ? &onionskinSamples : nullptr;
|
||||||
|
|
||||||
|
render(animation, frameTime, {}, 0.0f, layeredOnions, settings.onionskinMode == (int)OnionskinMode::INDEX);
|
||||||
|
|
||||||
|
if (auto overlayAnimation = anm2.animation_get(overlayIndex))
|
||||||
|
render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency), layeredOnions,
|
||||||
|
settings.onionskinMode == (int)OnionskinMode::INDEX);
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind();
|
||||||
|
|
||||||
|
sync_checker_pan();
|
||||||
|
render_checker_background(ImGui::GetWindowDrawList(), min, max, -size - checkerPan, CHECKER_SIZE);
|
||||||
|
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);
|
||||||
|
auto triggerTextColor = isLightTheme ? TRIGGER_TEXT_COLOR_LIGHT : TRIGGER_TEXT_COLOR_DARK;
|
||||||
|
drawList->AddText(textPos, ImGui::GetColorU32(triggerTextColor),
|
||||||
|
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 isMouseLeftDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
||||||
|
auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
|
||||||
|
auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
|
||||||
|
auto isMouseDown = isMouseLeftDown || isMouseMiddleDown || isMouseRightDown;
|
||||||
|
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 = shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL);
|
||||||
|
auto isZoomOut = shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL);
|
||||||
|
|
||||||
|
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();
|
||||||
|
if (useTool != tool::MOVE) isMoveDragging = false;
|
||||||
|
switch (useTool)
|
||||||
|
{
|
||||||
|
case tool::PAN:
|
||||||
|
if (isMouseDown || isMouseMiddleDown) pan += vec2(mouseDelta.x, mouseDelta.y);
|
||||||
|
break;
|
||||||
|
case tool::MOVE:
|
||||||
|
if (!frame) break;
|
||||||
|
if (isBegin)
|
||||||
|
{
|
||||||
|
document.snapshot(localize.get(EDIT_FRAME_POSITION));
|
||||||
|
if (isMouseClicked)
|
||||||
|
{
|
||||||
|
moveOffset = settings.inputIsMoveToolSnapToMouse ? vec2() : mousePos - frame->position;
|
||||||
|
isMoveDragging = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isMouseDown && isMoveDragging) frame->position = ivec2(mousePos - moveOffset);
|
||||||
|
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 (isMouseReleased) isMoveDragging = false;
|
||||||
|
if (isEnd) document.change(Document::FRAMES);
|
||||||
|
if (isDuring)
|
||||||
|
{
|
||||||
|
if (ImGui::BeginTooltip())
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_POSITION),
|
||||||
|
std::make_format_args(frame->position.x, frame->position.y))
|
||||||
|
.c_str());
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case tool::SCALE:
|
||||||
|
if (!frame) break;
|
||||||
|
if (isBegin) document.snapshot(localize.get(EDIT_FRAME_SCALE));
|
||||||
|
if (isMouseDown)
|
||||||
|
{
|
||||||
|
frame->scale += vec2(mouseDelta.x, mouseDelta.y);
|
||||||
|
if (isMod) frame->scale = {frame->scale.x, frame->scale.x};
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted(
|
||||||
|
std::vformat(localize.get(FORMAT_SCALE), std::make_format_args(frame->scale.x, frame->scale.y))
|
||||||
|
.c_str());
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnd) document.change(Document::FRAMES);
|
||||||
|
break;
|
||||||
|
case tool::ROTATE:
|
||||||
|
if (!frame) break;
|
||||||
|
if (isBegin) document.snapshot(localize.get(EDIT_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())
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted(
|
||||||
|
std::vformat(localize.get(FORMAT_ROTATION), std::make_format_args(frame->rotation)).c_str());
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnd) document.change(Document::FRAMES);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouseWheel != 0 || isZoomIn || isZoomOut)
|
||||||
|
{
|
||||||
|
auto previousZoom = zoom;
|
||||||
|
zoom_set(zoom, pan, vec2(mousePos), (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep);
|
||||||
|
if (zoom != previousZoom) hasPendingZoomPanAdjust = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
||||||
|
ImGui::TextUnformatted(localize.get(TEXT_RECORDING_PROGRESS));
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_CANCEL), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
||||||
|
{
|
||||||
|
renderFrames.clear();
|
||||||
|
|
||||||
|
pan = savedPan;
|
||||||
|
zoom = savedZoom;
|
||||||
|
settings = savedSettings;
|
||||||
|
overlayIndex = savedOverlayIndex;
|
||||||
|
isSizeTrySet = true;
|
||||||
|
hasPendingZoomPanAdjust = false;
|
||||||
|
isCheckerPanInitialized = false;
|
||||||
|
|
||||||
|
if (settings.timelineIsSound) audioStream.capture_end(mixer);
|
||||||
|
|
||||||
|
playback.isPlaying = false;
|
||||||
|
playback.isFinished = false;
|
||||||
|
manager.isRecording = false;
|
||||||
|
manager.progressPopup.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document.isAnimationPreviewSet)
|
||||||
|
{
|
||||||
|
center_view();
|
||||||
|
zoom = settings.previewStartZoom;
|
||||||
|
reset_checker_pan();
|
||||||
|
document.isAnimationPreviewSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.previewStartZoom = zoom;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/imgui/window/animation_preview.h
Normal file
36
src/imgui/window/animation_preview.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#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{};
|
||||||
|
int savedOverlayIndex{};
|
||||||
|
glm::vec2 mousePos{};
|
||||||
|
glm::vec2 checkerPan{};
|
||||||
|
glm::vec2 checkerSyncPan{};
|
||||||
|
float checkerSyncZoom{};
|
||||||
|
bool isCheckerPanInitialized{};
|
||||||
|
bool hasPendingZoomPanAdjust{};
|
||||||
|
bool isMoveDragging{};
|
||||||
|
glm::vec2 moveOffset{};
|
||||||
|
std::vector<resource::Texture> renderFrames{};
|
||||||
|
|
||||||
|
public:
|
||||||
|
AnimationPreview();
|
||||||
|
void tick(Manager&, Settings&);
|
||||||
|
void update(Manager&, Settings&, Resources&);
|
||||||
|
};
|
||||||
|
}
|
||||||
436
src/imgui/window/animations.cpp
Normal file
436
src/imgui/window/animations.cpp
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
#include "animations.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <format>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
#include "strings.h"
|
||||||
|
#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;
|
||||||
|
|
||||||
|
auto animations_remove = [&]()
|
||||||
|
{
|
||||||
|
if (!selection.empty())
|
||||||
|
{
|
||||||
|
for (auto it = selection.rbegin(); it != selection.rend(); ++it)
|
||||||
|
{
|
||||||
|
auto i = *it;
|
||||||
|
if (overlayIndex == i) overlayIndex = -1;
|
||||||
|
if (reference.animationIndex == i) reference.animationIndex = -1;
|
||||||
|
anm2.animations.items.erase(anm2.animations.items.begin() + i);
|
||||||
|
}
|
||||||
|
selection.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ImGui::Begin(localize.get(LABEL_ANIMATIONS_WINDOW), &settings.windowIsAnimations))
|
||||||
|
{
|
||||||
|
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::IsKeyPressed(ImGuiKey_Escape))
|
||||||
|
reference = {};
|
||||||
|
|
||||||
|
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((int)i);
|
||||||
|
|
||||||
|
auto isDefault = anm2.animations.defaultAnimation == animation.name;
|
||||||
|
auto isReferenced = reference.animationIndex == (int)i;
|
||||||
|
auto isNewAnimation = newAnimationSelectedIndex == (int)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 (isNewAnimation) renameState = RENAME_FORCE_EDIT;
|
||||||
|
if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i),
|
||||||
|
animation.name, selection.contains((int)i), ImGuiSelectableFlags_None, renameState))
|
||||||
|
{
|
||||||
|
reference = {(int)i};
|
||||||
|
document.frames.clear();
|
||||||
|
|
||||||
|
if (renameState == RENAME_BEGIN)
|
||||||
|
document.snapshot(localize.get(SNAPSHOT_RENAME_ANIMATION));
|
||||||
|
else if (renameState == RENAME_FINISHED)
|
||||||
|
{
|
||||||
|
if (anm2.animations.items.size() == 1) anm2.animations.defaultAnimation = animation.name;
|
||||||
|
document.change(Document::ANIMATIONS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) hovered = (int)i;
|
||||||
|
|
||||||
|
if (isNewAnimation)
|
||||||
|
{
|
||||||
|
ImGui::SetScrollHereY(0.5f);
|
||||||
|
newAnimationSelectedIndex = -1;
|
||||||
|
}
|
||||||
|
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(localize.get(BASIC_DEFAULT));
|
||||||
|
ImGui::PopFont();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TextUnformatted(
|
||||||
|
std::vformat(localize.get(FORMAT_LENGTH), std::make_format_args(animation.frameNum)).c_str());
|
||||||
|
auto loopLabel = animation.isLoop ? localize.get(BASIC_YES) : localize.get(BASIC_NO);
|
||||||
|
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_LOOP), std::make_format_args(loopLabel)).c_str());
|
||||||
|
|
||||||
|
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, localize.get(EDIT_MOVE_ANIMATIONS), 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();
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_CUT_ANIMATIONS), Document::ANIMATIONS, 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.push(std::vformat(localize.get(TOAST_DESERIALIZE_ANIMATIONS_FAILED),
|
||||||
|
std::make_format_args(errorString)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_ANIMATIONS_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(errorString)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_ANIMATIONS), Document::ANIMATIONS, deserialize());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_CUT], shortcut::FOCUSED)) cut();
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
|
||||||
|
{
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false,
|
||||||
|
!selection.empty() || hovered > -1))
|
||||||
|
{
|
||||||
|
cut();
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false,
|
||||||
|
!selection.empty() || hovered > -1))
|
||||||
|
{
|
||||||
|
copy();
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||||
|
{
|
||||||
|
paste();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(5);
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||||
|
if (ImGui::Button(localize.get(BASIC_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 = (int)anm2.animations.items.size();
|
||||||
|
if (!selection.empty())
|
||||||
|
{
|
||||||
|
index = *selection.rbegin() + 1;
|
||||||
|
index = std::min(index, (int)anm2.animations.items.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anm2.animations.items.empty()) anm2.animations.defaultAnimation = animation.name;
|
||||||
|
|
||||||
|
anm2.animations.items.insert(anm2.animations.items.begin() + index, animation);
|
||||||
|
selection = {index};
|
||||||
|
reference = {index};
|
||||||
|
newAnimationSelectedIndex = index;
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_ANIMATION), Document::ANIMATIONS, add());
|
||||||
|
}
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_ANIMATION), settings.shortcutAdd);
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(selection.empty());
|
||||||
|
{
|
||||||
|
shortcut(manager.chords[SHORTCUT_DUPLICATE]);
|
||||||
|
if (ImGui::Button(localize.get(BASIC_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, localize.get(EDIT_DUPLICATE_ANIMATIONS), Document::ANIMATIONS, duplicate());
|
||||||
|
}
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_DUPLICATE_ANIMATION), settings.shortcutDuplicate);
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_MERGE], 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, localize.get(EDIT_MERGE_ANIMATIONS), Document::ANIMATIONS, merge_quick())
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(selection.size() != 1);
|
||||||
|
{
|
||||||
|
if (ImGui::Button(localize.get(LABEL_MERGE), widgetSize))
|
||||||
|
{
|
||||||
|
mergePopup.open();
|
||||||
|
mergeSelection.clear();
|
||||||
|
mergeReference = *selection.begin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_OPEN_MERGE_POPUP), settings.shortcutMerge);
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||||
|
if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_ANIMATIONS), Document::ANIMATIONS, animations_remove());
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_ANIMATION), settings.shortcutRemove);
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_DEFAULT]);
|
||||||
|
ImGui::BeginDisabled(selection.size() != 1);
|
||||||
|
if (ImGui::Button(localize.get(BASIC_DEFAULT), widgetSize))
|
||||||
|
{
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_DEFAULT_ANIMATION), Document::ANIMATIONS,
|
||||||
|
anm2.animations.defaultAnimation = anm2.animations.items[*selection.begin()].name);
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_SET_DEFAULT_ANIMATION), 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(localize.get(LABEL_ANIMATIONS_CHILD), animationsSize, ImGuiChildFlags_Borders))
|
||||||
|
{
|
||||||
|
mergeSelection.start(anm2.animations.items.size());
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < anm2.animations.items.size(); ++index)
|
||||||
|
{
|
||||||
|
if ((int)index == mergeReference) continue;
|
||||||
|
|
||||||
|
auto& animation = anm2.animations.items[index];
|
||||||
|
|
||||||
|
ImGui::PushID((int)index);
|
||||||
|
|
||||||
|
ImGui::SetNextItemSelectionUserData((int)index);
|
||||||
|
ImGui::Selectable(animation.name.c_str(), mergeSelection.contains((int)index));
|
||||||
|
|
||||||
|
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(localize.get(LABEL_APPEND_FRAMES), &type, merge::APPEND);
|
||||||
|
ImGui::RadioButton(localize.get(LABEL_PREPEND_FRAMES), &type, merge::PREPEND);
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::BeginChild("##Merge Options 2", size))
|
||||||
|
{
|
||||||
|
ImGui::RadioButton(localize.get(LABEL_REPLACE_FRAMES), &type, merge::REPLACE);
|
||||||
|
ImGui::RadioButton(localize.get(LABEL_IGNORE_FRAMES), &type, merge::IGNORE);
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
if (ImGui::BeginChild("##Merge Delete After", deleteAfterSize, ImGuiChildFlags_Borders))
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_DELETE_ANIMATIONS_AFTER), &isDeleteAnimationsAfter);
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(mergeSelection.empty());
|
||||||
|
{
|
||||||
|
if (ImGui::Button(localize.get(LABEL_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, localize.get(EDIT_MERGE_ANIMATIONS), Document::ANIMATIONS, merge());
|
||||||
|
merge_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) merge_close();
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/imgui/window/animations.h
Normal file
20
src/imgui/window/animations.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "clipboard.h"
|
||||||
|
#include "manager.h"
|
||||||
|
#include "resources.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "strings.h"
|
||||||
|
|
||||||
|
namespace anm2ed::imgui
|
||||||
|
{
|
||||||
|
class Animations
|
||||||
|
{
|
||||||
|
PopupHelper mergePopup{PopupHelper(LABEL_ANIMATIONS_MERGE_POPUP)};
|
||||||
|
int newAnimationSelectedIndex{-1};
|
||||||
|
RenameState renameState{RENAME_SELECTABLE};
|
||||||
|
|
||||||
|
public:
|
||||||
|
void update(Manager&, Settings&, Resources&, Clipboard&);
|
||||||
|
};
|
||||||
|
}
|
||||||
159
src/imgui/window/events.cpp
Normal file
159
src/imgui/window/events.cpp
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
#include "map_.h"
|
||||||
|
#include "strings.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(localize.get(LABEL_EVENTS_WINDOW), &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)
|
||||||
|
{
|
||||||
|
auto isNewEvent = (newEventId == id);
|
||||||
|
|
||||||
|
ImGui::PushID(id);
|
||||||
|
ImGui::SetNextItemSelectionUserData(id);
|
||||||
|
if (isNewEvent) renameState = RENAME_FORCE_EDIT;
|
||||||
|
if (selectable_input_text(event.name, std::format("###Document #{} Event #{}", manager.selected, id),
|
||||||
|
event.name, selection.contains(id), ImGuiSelectableFlags_None, renameState))
|
||||||
|
{
|
||||||
|
if (renameState == RENAME_BEGIN)
|
||||||
|
document.snapshot(localize.get(EDIT_RENAME_EVENT));
|
||||||
|
else if (renameState == RENAME_FINISHED)
|
||||||
|
document.change(Document::EVENTS);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) hovered = id;
|
||||||
|
|
||||||
|
if (isNewEvent)
|
||||||
|
{
|
||||||
|
ImGui::SetScrollHereY(0.5f);
|
||||||
|
newEventId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginItemTooltip())
|
||||||
|
{
|
||||||
|
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||||
|
ImGui::TextUnformatted(event.name.c_str());
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||||
|
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(localize.get(EDIT_PASTE_EVENTS));
|
||||||
|
if (anm2.events_deserialize(clipboard.get(), type, &errorString))
|
||||||
|
document.change(Document::EVENTS);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_EVENTS_FAILED),
|
||||||
|
std::make_format_args(errorString)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_EVENTS_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(errorString)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
|
||||||
|
{
|
||||||
|
ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false, false);
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false,
|
||||||
|
!selection.empty() || hovered > -1))
|
||||||
|
copy();
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu(localize.get(BASIC_PASTE), !clipboard.is_empty()))
|
||||||
|
{
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_APPEND), settings.shortcutPaste.c_str())) paste(merge::APPEND);
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_REPLACE))) paste(merge::REPLACE);
|
||||||
|
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||||
|
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize))
|
||||||
|
{
|
||||||
|
auto add = [&]()
|
||||||
|
{
|
||||||
|
auto id = map::next_id_get(anm2.content.events);
|
||||||
|
anm2.content.events[id] = anm2::Event();
|
||||||
|
selection = {id};
|
||||||
|
reference = {id};
|
||||||
|
newEventId = id;
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_EVENT), Document::EVENTS, add());
|
||||||
|
}
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_EVENT), settings.shortcutAdd);
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||||
|
ImGui::BeginDisabled(unused.empty());
|
||||||
|
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize))
|
||||||
|
{
|
||||||
|
auto remove_unused = [&]()
|
||||||
|
{
|
||||||
|
for (auto& id : unused)
|
||||||
|
anm2.content.events.erase(id);
|
||||||
|
unused.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_EVENTS), Document::EVENTS, remove_unused());
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_EVENTS), settings.shortcutRemove);
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/imgui/window/events.h
Normal file
18
src/imgui/window/events.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "clipboard.h"
|
||||||
|
#include "manager.h"
|
||||||
|
#include "resources.h"
|
||||||
|
#include "settings.h"
|
||||||
|
|
||||||
|
namespace anm2ed::imgui
|
||||||
|
{
|
||||||
|
class Events
|
||||||
|
{
|
||||||
|
int newEventId{-1};
|
||||||
|
RenameState renameState{RENAME_SELECTABLE};
|
||||||
|
|
||||||
|
public:
|
||||||
|
void update(Manager&, Settings&, Resources&, Clipboard&);
|
||||||
|
};
|
||||||
|
}
|
||||||
256
src/imgui/window/frame_properties.cpp
Normal file
256
src/imgui/window/frame_properties.cpp
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
#include "frame_properties.h"
|
||||||
|
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
|
||||||
|
#include "math_.h"
|
||||||
|
#include "strings.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(localize.get(LABEL_FRAME_PROPERTIES_WINDOW), &settings.windowIsFrameProperties))
|
||||||
|
{
|
||||||
|
auto& document = *manager.get();
|
||||||
|
auto& frames = document.frames.selection;
|
||||||
|
auto& type = document.reference.itemType;
|
||||||
|
|
||||||
|
if (frames.size() <= 1)
|
||||||
|
{
|
||||||
|
auto frame = document.frame_get();
|
||||||
|
auto useFrame = frame ? *frame : anm2::Frame();
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(!frame);
|
||||||
|
{
|
||||||
|
if (type == anm2::TRIGGER)
|
||||||
|
{
|
||||||
|
if (combo_negative_one_indexed(localize.get(BASIC_EVENT),
|
||||||
|
frame ? &useFrame.eventID : &dummy_value_negative<int>(),
|
||||||
|
document.event.labels))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_EVENT), Document::FRAMES,
|
||||||
|
frame->eventID = useFrame.eventID);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_EVENT));
|
||||||
|
|
||||||
|
if (combo_negative_one_indexed(localize.get(BASIC_SOUND),
|
||||||
|
frame ? &useFrame.soundID : &dummy_value_negative<int>(),
|
||||||
|
document.sound.labels))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_SOUND), Document::FRAMES,
|
||||||
|
frame->soundID = useFrame.soundID);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_SOUND));
|
||||||
|
|
||||||
|
if (ImGui::InputInt(localize.get(BASIC_AT_FRAME), frame ? &useFrame.atFrame : &dummy_value<int>(), STEP,
|
||||||
|
STEP_FAST, !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_AT_FRAME), Document::FRAMES,
|
||||||
|
frame->atFrame = useFrame.atFrame);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_AT_FRAME));
|
||||||
|
|
||||||
|
if (ImGui::Checkbox(localize.get(BASIC_VISIBLE), frame ? &useFrame.isVisible : &dummy_value<bool>()))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_VISIBILITY), Document::FRAMES,
|
||||||
|
frame->isVisible = useFrame.isVisible);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_VISIBILITY));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_);
|
||||||
|
{
|
||||||
|
if (ImGui::InputFloat2(localize.get(BASIC_CROP), frame ? value_ptr(useFrame.crop) : &dummy_value<float>(),
|
||||||
|
frame ? vec2_format_get(useFrame.crop) : ""))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_CROP), Document::FRAMES, frame->crop = useFrame.crop);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CROP));
|
||||||
|
|
||||||
|
if (ImGui::InputFloat2(localize.get(BASIC_SIZE), frame ? value_ptr(useFrame.size) : &dummy_value<float>(),
|
||||||
|
frame ? vec2_format_get(useFrame.size) : ""))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_SIZE), Document::FRAMES, frame->size = useFrame.size);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SIZE));
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
if (ImGui::InputFloat2(localize.get(BASIC_POSITION),
|
||||||
|
frame ? value_ptr(useFrame.position) : &dummy_value<float>(),
|
||||||
|
frame ? vec2_format_get(useFrame.position) : ""))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_POSITION), Document::FRAMES,
|
||||||
|
frame->position = useFrame.position);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_POSITION));
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_);
|
||||||
|
{
|
||||||
|
if (ImGui::InputFloat2(localize.get(BASIC_PIVOT),
|
||||||
|
frame ? value_ptr(useFrame.pivot) : &dummy_value<float>(),
|
||||||
|
frame ? vec2_format_get(useFrame.pivot) : ""))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_PIVOT), Document::FRAMES,
|
||||||
|
frame->pivot = useFrame.pivot);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PIVOT));
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
if (ImGui::InputFloat2(localize.get(BASIC_SCALE), frame ? value_ptr(useFrame.scale) : &dummy_value<float>(),
|
||||||
|
frame ? vec2_format_get(useFrame.scale) : ""))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_SCALE), Document::FRAMES, frame->scale = useFrame.scale);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SCALE));
|
||||||
|
|
||||||
|
if (ImGui::InputFloat(localize.get(BASIC_ROTATION), frame ? &useFrame.rotation : &dummy_value<float>(),
|
||||||
|
STEP, STEP_FAST, frame ? float_format_get(useFrame.rotation) : ""))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_ROTATION), Document::FRAMES,
|
||||||
|
frame->rotation = useFrame.rotation);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROTATION));
|
||||||
|
|
||||||
|
if (input_int_range(localize.get(BASIC_DURATION), frame ? useFrame.duration : dummy_value<int>(),
|
||||||
|
frame ? anm2::FRAME_DURATION_MIN : 0, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST,
|
||||||
|
!frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_DURATION), Document::FRAMES,
|
||||||
|
frame->duration = useFrame.duration);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DURATION));
|
||||||
|
|
||||||
|
if (ImGui::ColorEdit4(localize.get(BASIC_TINT), frame ? value_ptr(useFrame.tint) : &dummy_value<float>()))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_TINT), Document::FRAMES, frame->tint = useFrame.tint);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TINT));
|
||||||
|
|
||||||
|
if (ImGui::ColorEdit3(localize.get(BASIC_COLOR_OFFSET),
|
||||||
|
frame ? value_ptr(useFrame.colorOffset) : &dummy_value<float>()))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_COLOR_OFFSET), Document::FRAMES,
|
||||||
|
frame->colorOffset = useFrame.colorOffset);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COLOR_OFFSET));
|
||||||
|
|
||||||
|
if (ImGui::Checkbox(localize.get(BASIC_VISIBLE), frame ? &useFrame.isVisible : &dummy_value<bool>()))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_VISIBILITY), Document::FRAMES,
|
||||||
|
frame->isVisible = useFrame.isVisible);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FRAME_VISIBILITY));
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Checkbox(localize.get(BASIC_INTERPOLATED),
|
||||||
|
frame ? &useFrame.isInterpolated : &dummy_value<bool>()))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_INTERPOLATION), Document::FRAMES,
|
||||||
|
frame->isInterpolated = useFrame.isInterpolated);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FRAME_INTERPOLATION));
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(LABEL_FLIP_X), widgetSize))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_X), Document::FRAMES,
|
||||||
|
frame->scale.x = -frame->scale.x);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FLIP_X));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(localize.get(LABEL_FLIP_Y), widgetSize))
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_Y), Document::FRAMES,
|
||||||
|
frame->scale.y = -frame->scale.y);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FLIP_Y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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& isDuration = settings.changeIsDuration;
|
||||||
|
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& duration = settings.changeDuration;
|
||||||
|
auto& tint = settings.changeTint;
|
||||||
|
auto& colorOffset = settings.changeColorOffset;
|
||||||
|
auto& isVisible = settings.changeIsVisible;
|
||||||
|
auto& isInterpolated = settings.changeIsInterpolated;
|
||||||
|
|
||||||
|
#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), 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, float_format_get(value))); };
|
||||||
|
|
||||||
|
auto duration_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value)
|
||||||
|
{
|
||||||
|
PROPERTIES_WIDGET(
|
||||||
|
input_int_range(valueLabel, value, anm2::FRAME_DURATION_MIN, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST));
|
||||||
|
};
|
||||||
|
|
||||||
|
#undef PROPERTIES_WIDGET
|
||||||
|
|
||||||
|
float2_value("##Is Crop", localize.get(BASIC_CROP), isCrop, crop);
|
||||||
|
float2_value("##Is Size", localize.get(BASIC_SIZE), isSize, size);
|
||||||
|
float2_value("##Is Position", localize.get(BASIC_POSITION), isPosition, position);
|
||||||
|
float2_value("##Is Pivot", localize.get(BASIC_PIVOT), isPivot, pivot);
|
||||||
|
float2_value("##Is Scale", localize.get(BASIC_SCALE), isScale, scale);
|
||||||
|
float_value("##Is Rotation", localize.get(BASIC_ROTATION), isRotation, rotation);
|
||||||
|
duration_value("##Is Duration", localize.get(BASIC_DURATION), isDuration, duration);
|
||||||
|
color4_value("##Is Tint", localize.get(BASIC_TINT), isTint, tint);
|
||||||
|
color3_value("##Is Color Offset", localize.get(BASIC_COLOR_OFFSET), isColorOffset, colorOffset);
|
||||||
|
bool_value("##Is Visible", localize.get(BASIC_VISIBLE), isVisibleSet, isVisible);
|
||||||
|
ImGui::SameLine();
|
||||||
|
bool_value("##Is Interpolated", localize.get(BASIC_INTERPOLATED), isInterpolatedSet, isInterpolated);
|
||||||
|
|
||||||
|
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 (isDuration) frameChange.duration = std::make_optional(duration);
|
||||||
|
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(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES,
|
||||||
|
document.item_get()->frames_change(frameChange, type, *frames.begin(), (int)frames.size()));
|
||||||
|
};
|
||||||
|
|
||||||
|
auto rowOneWidgetSize = widget_size_with_row_get(1);
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(LABEL_ADJUST), rowOneWidgetSize)) frame_change(anm2::ADJUST);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADJUST));
|
||||||
|
|
||||||
|
auto rowTwoWidgetSize = widget_size_with_row_get(4);
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_ADD), rowTwoWidgetSize)) frame_change(anm2::ADD);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_VALUES));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(localize.get(LABEL_SUBTRACT), rowTwoWidgetSize)) frame_change(anm2::SUBTRACT);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SUBTRACT_VALUES));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(localize.get(LABEL_MULTIPLY), rowTwoWidgetSize)) frame_change(anm2::MULTIPLY);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MULTIPLY_VALUES));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(localize.get(LABEL_DIVIDE), rowTwoWidgetSize)) frame_change(anm2::DIVIDE);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DIVIDE_VALUES));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/imgui/window/frame_properties.h
Normal file
13
src/imgui/window/frame_properties.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "manager.h"
|
||||||
|
#include "settings.h"
|
||||||
|
|
||||||
|
namespace anm2ed::imgui
|
||||||
|
{
|
||||||
|
class FrameProperties
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void update(Manager&, Settings&);
|
||||||
|
};
|
||||||
|
}
|
||||||
206
src/imgui/window/layers.cpp
Normal file
206
src/imgui/window/layers.cpp
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#include "layers.h"
|
||||||
|
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
#include "map_.h"
|
||||||
|
#include "strings.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(localize.get(LABEL_LAYERS_WINDOW), &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::vformat(localize.get(FORMAT_LAYER), std::make_format_args(id, layer.name, layer.spritesheetID))
|
||||||
|
.c_str(),
|
||||||
|
isSelected);
|
||||||
|
if (newLayerId == id)
|
||||||
|
{
|
||||||
|
ImGui::SetScrollHereY(0.5f);
|
||||||
|
newLayerId = -1;
|
||||||
|
}
|
||||||
|
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::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||||
|
ImGui::TextUnformatted(
|
||||||
|
std::vformat(localize.get(FORMAT_SPRITESHEET_ID), std::make_format_args(layer.spritesheetID)).c_str());
|
||||||
|
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(localize.get(EDIT_PASTE_LAYERS));
|
||||||
|
if (anm2.layers_deserialize(clipboard.get(), type, &errorString))
|
||||||
|
document.change(Document::NULLS);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_LAYERS_FAILED),
|
||||||
|
std::make_format_args(errorString)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_LAYERS_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(errorString)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
|
||||||
|
{
|
||||||
|
ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false, false);
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false,
|
||||||
|
!selection.empty() || hovered > -1))
|
||||||
|
copy();
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu(localize.get(BASIC_PASTE), !clipboard.is_empty()))
|
||||||
|
{
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_APPEND), settings.shortcutPaste.c_str())) paste(merge::APPEND);
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_REPLACE))) paste(merge::REPLACE);
|
||||||
|
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||||
|
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) manager.layer_properties_open();
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_LAYER), settings.shortcutAdd);
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||||
|
ImGui::BeginDisabled(unused.empty());
|
||||||
|
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize))
|
||||||
|
{
|
||||||
|
auto remove_unused = [&]()
|
||||||
|
{
|
||||||
|
for (auto& id : unused)
|
||||||
|
anm2.content.layers.erase(id);
|
||||||
|
unused.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_LAYERS), Document::LAYERS, remove_unused());
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_LAYERS), 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(localize.get(BASIC_NAME), &layer.name);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME));
|
||||||
|
combo_negative_one_indexed(localize.get(LABEL_SPRITESHEET), &layer.spritesheetID, document.spritesheet.labels);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_SPRITESHEET));
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
|
if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize))
|
||||||
|
{
|
||||||
|
if (reference == -1)
|
||||||
|
{
|
||||||
|
auto add = [&]()
|
||||||
|
{
|
||||||
|
auto id = map::next_id_get(anm2.content.layers);
|
||||||
|
anm2.content.layers[id] = layer;
|
||||||
|
selection = {id};
|
||||||
|
newLayerId = id;
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_LAYER), Document::LAYERS, add());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto set = [&]()
|
||||||
|
{
|
||||||
|
anm2.content.layers[reference] = layer;
|
||||||
|
selection = {reference};
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_SET_LAYER_PROPERTIES), Document::LAYERS, set());
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.layer_properties_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.layer_properties_close();
|
||||||
|
|
||||||
|
manager.layer_properties_end();
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/imgui/window/layers.h
Normal file
17
src/imgui/window/layers.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "clipboard.h"
|
||||||
|
#include "manager.h"
|
||||||
|
#include "resources.h"
|
||||||
|
#include "settings.h"
|
||||||
|
|
||||||
|
namespace anm2ed::imgui
|
||||||
|
{
|
||||||
|
class Layers
|
||||||
|
{
|
||||||
|
int newLayerId{-1};
|
||||||
|
|
||||||
|
public:
|
||||||
|
void update(Manager&, Settings&, Resources&, Clipboard&);
|
||||||
|
};
|
||||||
|
}
|
||||||
205
src/imgui/window/nulls.cpp
Normal file
205
src/imgui/window/nulls.cpp
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
#include "nulls.h"
|
||||||
|
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
#include "map_.h"
|
||||||
|
#include "strings.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(localize.get(LABEL_NULLS_WINDOW), &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::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null.name)).c_str(),
|
||||||
|
isSelected);
|
||||||
|
if (newNullId == id)
|
||||||
|
{
|
||||||
|
ImGui::SetScrollHereY(0.5f);
|
||||||
|
newNullId = -1;
|
||||||
|
}
|
||||||
|
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::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||||
|
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(localize.get(EDIT_PASTE_NULLS));
|
||||||
|
if (anm2.nulls_deserialize(clipboard.get(), type, &errorString))
|
||||||
|
document.change(Document::NULLS);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_NULLS_FAILED),
|
||||||
|
std::make_format_args(errorString)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_NULLS_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(errorString)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||||
|
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
|
||||||
|
{
|
||||||
|
ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false, false);
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false,
|
||||||
|
selection.empty() || hovered > -1))
|
||||||
|
copy();
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu(localize.get(BASIC_PASTE), !clipboard.is_empty()))
|
||||||
|
{
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_APPEND), settings.shortcutPaste.c_str())) paste(merge::APPEND);
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_REPLACE))) paste(merge::REPLACE);
|
||||||
|
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||||
|
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) manager.null_properties_open();
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_NULL), settings.shortcutAdd);
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||||
|
ImGui::BeginDisabled(unused.empty());
|
||||||
|
if (ImGui::Button(localize.get(LABEL_REMOVE_UNUSED_NULLS), widgetSize))
|
||||||
|
{
|
||||||
|
auto remove_unused = [&]()
|
||||||
|
{
|
||||||
|
for (auto& id : unused)
|
||||||
|
anm2.content.nulls.erase(id);
|
||||||
|
unused.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_NULLS), Document::EVENTS, remove_unused());
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_NULLS), 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(localize.get(BASIC_NAME), &null.name);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_NAME));
|
||||||
|
|
||||||
|
ImGui::Checkbox(localize.get(LABEL_RECT), &null.isShowRect);
|
||||||
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_RECT));
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
auto widgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
|
if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize))
|
||||||
|
{
|
||||||
|
if (reference == -1)
|
||||||
|
{
|
||||||
|
auto add = [&]()
|
||||||
|
{
|
||||||
|
auto id = map::next_id_get(anm2.content.nulls);
|
||||||
|
anm2.content.nulls[id] = null;
|
||||||
|
selection = {id};
|
||||||
|
newNullId = id;
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_NULL), Document::NULLS, add());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto set = [&]()
|
||||||
|
{
|
||||||
|
anm2.content.nulls[reference] = null;
|
||||||
|
selection = {reference};
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_SET_NULL_PROPERTIES), Document::NULLS, set());
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.null_properties_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.null_properties_close();
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.null_properties_end();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user