Compare commits
127 Commits
24f98fe0a0
...
f1b46fbf60
| Author | SHA1 | Date | |
|---|---|---|---|
| f1b46fbf60 | |||
| 216a65c756 | |||
| 98d1bf1981 | |||
| c59ef05902 | |||
| b60c4bc295 | |||
| 15f85b84a9 | |||
| 8fbccb2257 | |||
| 378a7692e2 | |||
| 0b91382f0f | |||
| b1213d7d23 | |||
| ad125c15a2 | |||
| 4f5966dad6 | |||
| a83dbd5b6c | |||
| f58d89425f | |||
| bbfafd7331 | |||
| 1b5ba6b584 | |||
| c11b404392 | |||
| 2d27b7e8fb | |||
| 77f6e65b15 | |||
| 5a48b07321 | |||
| 400231f246 | |||
| 33e5846a6e | |||
| 85073a2ab9 | |||
| 64d6a1d95a | |||
| 00bff4a91f | |||
| 1b7a49c25d | |||
| ca3a0f6691 | |||
| 76f134fe60 | |||
| 044ef8b818 | |||
| cbfb3403b7 | |||
| b90c15eab4 | |||
| 3b551abb8a | |||
| 579ca08181 | |||
| 8a1058d559 | |||
| 7f4e05a927 | |||
| f9087b8ed3 | |||
| 945fd31735 | |||
| 6cac267998 | |||
| 2797a971eb | |||
| 47edb034b3 | |||
| 283d3fd852 | |||
| 3117f190a5 | |||
| 29abbb79fb | |||
| cc2f5455e1 | |||
| ea6f1e0711 | |||
| 6fd1d57306 | |||
| d2bc5696bb | |||
| e68dfa3c94 | |||
| d2e9204e25 | |||
| a39a816ea9 | |||
| bd3fdbf930 | |||
| 5124f77817 | |||
| b41b6df19e | |||
| a8143b6d0c | |||
| 95b441c4cf | |||
| 58077b5522 | |||
| f7109c24d0 | |||
| c0b4aaa63e | |||
| 7e988dbb7f | |||
| 33c55806a4 | |||
| 5539b73374 | |||
| c0de0c4079 | |||
| cd0f6e9438 | |||
| 23fac343a9 | |||
| 233b2139fd | |||
| 8096521f28 | |||
| e108ec15b8 | |||
| a6964c40bc | |||
| a8f38321ca | |||
| 5b8469d7ac | |||
| 9853e049b0 | |||
| f64d622d87 | |||
| ed68851f5c | |||
| 99e56eabdf | |||
| 4574ff0f84 | |||
| 82c5dcc176 | |||
| 0043cbd44a | |||
| 92d6d90130 | |||
| e896c7f9cc | |||
| 99d4d06594 | |||
| 50154973ef | |||
| d6fab7c738 | |||
| 09cfaf9af8 | |||
| 4475c79438 | |||
| d6c2ee3401 | |||
| df7dfde1d6 | |||
| 7e1b84e83c | |||
| 6257e7dffb | |||
| e81030d9e3 | |||
| 62c845f47b | |||
| 99a1caa096 | |||
| 3095a56a01 | |||
| 0ba585511c | |||
| 119bbc4081 | |||
| b4b4fe3714 | |||
| 4cf5304c79 | |||
| 4e1226739a | |||
| ed2f92d412 | |||
| ffed82a591 | |||
| ee89d8880d | |||
| 225a584a21 | |||
| efa9533a52 | |||
| 8fa103d0b7 | |||
| b9b0fb9974 | |||
| c23179c134 | |||
| 4fc004dbe0 | |||
| 29fc0f3449 | |||
| 898dfcc68f | |||
| 31ac9707e7 | |||
| 1a3c75a84a | |||
| d34ff73512 | |||
| 21b66d8337 | |||
| 4977f4476c | |||
| 61275d9e75 | |||
| 94a3371509 | |||
| c77701fe72 | |||
| 3be9f0746f | |||
| 9c1ccd3223 | |||
| dd55e6e2a2 | |||
| e6a4e5af35 | |||
| fef1ff50e3 | |||
| 10b37e9854 | |||
| 63c0ffe166 | |||
| 183f3390fa | |||
| cc6a502ff9 | |||
| 45f0271c29 | |||
| b4fddb7714 |
Vendored
+5
-3
@@ -5,7 +5,8 @@
|
||||
"name": "Debug",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/build/anm2ed",
|
||||
"preLaunchTask": "build-debug",
|
||||
"program": "${workspaceFolder}/out/build/linux-debug/bin/anm2ed",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -28,7 +29,8 @@
|
||||
"name": "Release",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/build-release/anm2ed",
|
||||
"preLaunchTask": "build-release",
|
||||
"program": "${workspaceFolder}/out/build/linux-release/bin/anm2ed",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -48,4 +50,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+65
-3
@@ -2,9 +2,33 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"label": "configure-debug",
|
||||
"type": "shell",
|
||||
"command": "cmake --build build --target anm2ed",
|
||||
"command": "cmake",
|
||||
"args": [
|
||||
"-S",
|
||||
".",
|
||||
"-B",
|
||||
"out/build/linux-debug",
|
||||
"-DCMAKE_BUILD_TYPE=Debug"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$gcc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "build-debug",
|
||||
"type": "shell",
|
||||
"command": "cmake",
|
||||
"args": [
|
||||
"--build",
|
||||
"out/build/linux-debug",
|
||||
"--parallel",
|
||||
"16",
|
||||
"--target",
|
||||
"anm2ed"
|
||||
],
|
||||
"dependsOn": "configure-debug",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
@@ -13,14 +37,52 @@
|
||||
"$gcc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "run-debug",
|
||||
"type": "shell",
|
||||
"command": "./out/build/linux-debug/bin/anm2ed",
|
||||
"dependsOn": "build-debug",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "configure-release",
|
||||
"type": "shell",
|
||||
"command": "cmake",
|
||||
"args": [
|
||||
"-S",
|
||||
".",
|
||||
"-B",
|
||||
"out/build/linux-release",
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$gcc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "build-release",
|
||||
"type": "shell",
|
||||
"command": "cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release && cmake --build build-release --target anm2ed",
|
||||
"command": "cmake",
|
||||
"args": [
|
||||
"--build",
|
||||
"out/build/linux-release",
|
||||
"--parallel",
|
||||
"16",
|
||||
"--target",
|
||||
"anm2ed"
|
||||
],
|
||||
"dependsOn": "configure-release",
|
||||
"group": "build",
|
||||
"problemMatcher": [
|
||||
"$gcc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "run-release",
|
||||
"type": "shell",
|
||||
"command": "./out/build/linux-release/bin/anm2ed",
|
||||
"dependsOn": "build-release",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+181
-171
@@ -1,220 +1,230 @@
|
||||
cmake_minimum_required(VERSION 3.27)
|
||||
project(anm2ed LANGUAGES C CXX)
|
||||
|
||||
if (WIN32 AND DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
|
||||
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
|
||||
CACHE STRING "Vcpkg toolchain file")
|
||||
endif ()
|
||||
if(WIN32 AND DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
|
||||
set(CMAKE_TOOLCHAIN_FILE
|
||||
"$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
|
||||
CACHE STRING "Vcpkg toolchain file")
|
||||
endif()
|
||||
|
||||
set(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 ()
|
||||
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()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
if (MSVC)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||
endif ()
|
||||
if(MSVC)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug>:ProgramDatabase>")
|
||||
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)
|
||||
macro(anm2ed_set_cache_bool name value)
|
||||
set(${name} ${value} CACHE BOOL "" FORCE)
|
||||
endmacro()
|
||||
|
||||
anm2ed_set_cache_bool(SDL_STATIC ON)
|
||||
anm2ed_set_cache_bool(SDL_SHARED OFF)
|
||||
if(NOT WIN32)
|
||||
anm2ed_set_cache_bool(SDL_ALSA ON)
|
||||
anm2ed_set_cache_bool(SDL_ALSA_SHARED ON)
|
||||
anm2ed_set_cache_bool(SDL_JACK ON)
|
||||
anm2ed_set_cache_bool(SDL_JACK_SHARED ON)
|
||||
anm2ed_set_cache_bool(SDL_PIPEWIRE ON)
|
||||
anm2ed_set_cache_bool(SDL_PIPEWIRE_SHARED ON)
|
||||
anm2ed_set_cache_bool(SDL_SNDIO ON)
|
||||
anm2ed_set_cache_bool(SDL_SNDIO_SHARED ON)
|
||||
endif()
|
||||
anm2ed_set_cache_bool(SDL_HAPTIC OFF)
|
||||
anm2ed_set_cache_bool(SDL_JOYSTICK OFF)
|
||||
anm2ed_set_cache_bool(SDL_SENSOR OFF)
|
||||
anm2ed_set_cache_bool(SDL_HIDAPI OFF)
|
||||
anm2ed_set_cache_bool(SDL_CAMERA OFF)
|
||||
anm2ed_set_cache_bool(SDL_TRAY OFF)
|
||||
anm2ed_set_cache_bool(SDL_VULKAN OFF)
|
||||
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)
|
||||
anm2ed_set_cache_bool(SDLMIXER_DEPS_SHARED OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_AIFF OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_AU OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_FLAC_LIBFLAC OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_FLAC_DRFLAC OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_GME OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_MOD_XMP OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_MP3_DRMP3 OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_MP3_MPG123 OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_MIDI_FLUIDSYNTH OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_MIDI_TIMIDITY OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_OPUS ON)
|
||||
anm2ed_set_cache_bool(SDLMIXER_VOC OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_VORBIS_STB ON)
|
||||
anm2ed_set_cache_bool(SDLMIXER_VORBIS_VORBISFILE OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_VORBIS_TREMOR OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_WAVE ON)
|
||||
anm2ed_set_cache_bool(SDLMIXER_WAVPACK OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_TEST OFF)
|
||||
anm2ed_set_cache_bool(SDLMIXER_INSTALL OFF)
|
||||
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
|
||||
external/imgui/imgui.cpp
|
||||
external/imgui/imgui_draw.cpp
|
||||
external/imgui/imgui_widgets.cpp
|
||||
external/imgui/imgui_tables.cpp
|
||||
external/imgui/backends/imgui_impl_sdl3.cpp
|
||||
external/imgui/backends/imgui_impl_opengl3.cpp
|
||||
)
|
||||
external/imgui/imgui.cpp
|
||||
external/imgui/imgui_draw.cpp
|
||||
external/imgui/imgui_widgets.cpp
|
||||
external/imgui/imgui_tables.cpp
|
||||
external/imgui/backends/imgui_impl_sdl3.cpp
|
||||
external/imgui/backends/imgui_impl_opengl3.cpp)
|
||||
|
||||
set(TINYXML2_SRC external/tinyxml2/tinyxml2.cpp)
|
||||
|
||||
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
|
||||
src/anm2/*.cpp
|
||||
src/anm2/*.h
|
||||
src/resource/*.cpp
|
||||
src/resource/*.h
|
||||
src/imgui/*.cpp
|
||||
src/imgui/*.h
|
||||
src/imgui/window/*.cpp
|
||||
src/imgui/window/*.h
|
||||
src/util/*.cpp
|
||||
src/util/*.h
|
||||
src/window/*.cpp
|
||||
src/window/*.h
|
||||
src/*.cpp
|
||||
src/*.h
|
||||
)
|
||||
src/anm2/*.cpp
|
||||
src/anm2/*.hpp
|
||||
src/resource/*.cpp
|
||||
src/resource/*.hpp
|
||||
src/imgui/*.cpp
|
||||
src/imgui/*.hpp
|
||||
src/imgui/window/*.cpp
|
||||
src/imgui/window/*.hpp
|
||||
src/imgui/wizard/*.cpp
|
||||
src/imgui/wizard/*.hpp
|
||||
src/util/*.cpp
|
||||
src/util/*.hpp
|
||||
src/window/*.cpp
|
||||
src/window/*.hpp
|
||||
src/*.cpp
|
||||
src/*.hpp)
|
||||
|
||||
add_executable(${PROJECT_NAME}
|
||||
${GLAD_SRC}
|
||||
${IMGUI_SRC}
|
||||
${TINYXML2_SRC}
|
||||
${PROJECT_SRC}
|
||||
)
|
||||
if (MSVC)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:/std:c++latest>")
|
||||
add_executable(${PROJECT_NAME} ${GLAD_SRC} ${IMGUI_SRC} ${TINYXML2_SRC} ${PROJECT_SRC})
|
||||
set_target_properties(
|
||||
${PROJECT_NAME}
|
||||
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
||||
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin"
|
||||
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin"
|
||||
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin"
|
||||
RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_BINARY_DIR}/bin")
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE IMGUI_DISABLE_OBSOLETE_FUNCTIONS IMGUI_DEBUG_PARANOID
|
||||
IMGUI_ENABLE_DOCKING)
|
||||
|
||||
target_include_directories(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE include
|
||||
include/glad
|
||||
external
|
||||
external/imgui
|
||||
external/glm
|
||||
external/tinyxml2
|
||||
external/lunasvg
|
||||
external/SDL
|
||||
external/SDL_mixer
|
||||
external/SDL_mixer/include
|
||||
src
|
||||
src/imgui
|
||||
src/resource
|
||||
src/util)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE "Release")
|
||||
endif()
|
||||
|
||||
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 ()
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE DEBUG)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE m)
|
||||
endif ()
|
||||
if(WIN32)
|
||||
# Keep Release as a GUI app (no console window).
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
if(MSVC)
|
||||
target_link_options(${PROJECT_NAME} PRIVATE /SUBSYSTEM:WINDOWS)
|
||||
else()
|
||||
target_link_options(${PROJECT_NAME} PRIVATE -mwindows)
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -O0 -pg)
|
||||
else()
|
||||
set(CMAKE_BUILD_TYPE "Release")
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -O3)
|
||||
endif()
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE m)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||
IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
IMGUI_DEBUG_PARANOID
|
||||
IMGUI_ENABLE_DOCKING
|
||||
)
|
||||
if(WIN32)
|
||||
enable_language(RC)
|
||||
target_sources(${PROJECT_NAME} PRIVATE anm2ed.rc)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:/std:c++latest>")
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /EHsc)
|
||||
target_link_options(${PROJECT_NAME} PRIVATE /STACK:0xffffff)
|
||||
|
||||
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
|
||||
)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /Zi)
|
||||
target_link_options(${PROJECT_NAME} PRIVATE /DEBUG)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
set(OPENGL_LIB opengl32)
|
||||
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)
|
||||
if(WIN32)
|
||||
set(OPENGL_LIB opengl32)
|
||||
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 ()
|
||||
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}
|
||||
)
|
||||
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 ()
|
||||
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 ()
|
||||
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 ()
|
||||
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}")
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "x64-Debug",
|
||||
"name": "windows-debug",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Debug",
|
||||
"inheritEnvironments": [
|
||||
@@ -12,7 +12,7 @@
|
||||
"cmakeCommandArgs": "-DCMAKE_BUILD_TYPE=\"Debug\""
|
||||
},
|
||||
{
|
||||
"name": "x64-Release",
|
||||
"name": "windows-release",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Release",
|
||||
"inheritEnvironments": [
|
||||
@@ -23,4 +23,4 @@
|
||||
"cmakeCommandArgs": "-DCMAKE_BUILD_TYPE=\"Release\""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
A reimplementation of *The Binding of Isaac: Rebirth*'s proprietary animation editor. Manipulates the XML-based ".anm2" format, used for in-game tweened animations.
|
||||
|
||||
### Clarification
|
||||
This application was developed with the assistance of a large language model.
|
||||
|
||||
## Features
|
||||
- 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 [Repentogon](https://repentogon.com/).
|
||||
@@ -15,9 +18,20 @@ A reimplementation of *The Binding of Isaac: Rebirth*'s proprietary animation ed
|
||||
- Can output .webm, .mp4 or *.png sequence (wih FFmpeg)
|
||||
- Cutting, copying and pasting
|
||||
- Onionskinning
|
||||
- Spritesheet "regions"; pre-set areas to be reused
|
||||
- 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)
|
||||
- Texture packing
|
||||
|
||||
### Supported Languages
|
||||
- English
|
||||
- Español (Latinoamérica) (Spanish (Latin America))
|
||||
- Pусский (Russian)
|
||||
- 中文 (Chinese)
|
||||
- 한국어 (Korean)
|
||||
|
||||
**If you want to help localize for your language, feel free to get in touch to contribute!**
|
||||
|
||||
### Note: Rendering Animations
|
||||
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!
|
||||
|
||||
@@ -1 +1 @@
|
||||
/home/anon/sda/Personal/Repos/anm2ed/build/compile_commands.json
|
||||
/home/anon/sda/Personal/Repos/anm2ed/out/build/linux-debug/compile_commands.json
|
||||
@@ -0,0 +1,147 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// DEAR IMGUI COMPILE-TIME OPTIONS
|
||||
// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure.
|
||||
// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions.
|
||||
//-----------------------------------------------------------------------------
|
||||
// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it)
|
||||
// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template.
|
||||
//-----------------------------------------------------------------------------
|
||||
// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp
|
||||
// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures.
|
||||
// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts.
|
||||
// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
//---- Define assertion handler. Defaults to calling assert().
|
||||
// - If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement.
|
||||
// - Compiling with NDEBUG will usually strip out assert() to nothing, which is NOT recommended because we use asserts to notify of programmer mistakes.
|
||||
//#define IM_ASSERT(_EXPR) MyAssert(_EXPR)
|
||||
//#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts
|
||||
|
||||
//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows
|
||||
// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility.
|
||||
// - Windows DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions()
|
||||
// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details.
|
||||
//#define IMGUI_API __declspec(dllexport) // MSVC Windows: DLL export
|
||||
//#define IMGUI_API __declspec(dllimport) // MSVC Windows: DLL import
|
||||
//#define IMGUI_API __attribute__((visibility("default"))) // GCC/Clang: override visibility when set is hidden
|
||||
|
||||
//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names.
|
||||
//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
|
||||
//---- Disable all of Dear ImGui or don't implement standard windows/tools.
|
||||
// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp.
|
||||
//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty.
|
||||
//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty.
|
||||
//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty.
|
||||
|
||||
//---- Don't implement some functions to reduce linkage requirements.
|
||||
//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a)
|
||||
//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW)
|
||||
//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a)
|
||||
//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME).
|
||||
//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default).
|
||||
//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")).
|
||||
//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf)
|
||||
//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself.
|
||||
//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies)
|
||||
//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function.
|
||||
//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions().
|
||||
//#define IMGUI_DISABLE_DEFAULT_FONT // Disable default embedded font (ProggyClean.ttf), remove ~9.5 KB from output binary. AddFontDefault() will assert.
|
||||
//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available
|
||||
|
||||
//---- Enable Test Engine / Automation features.
|
||||
//#define IMGUI_ENABLE_TEST_ENGINE // Enable imgui_test_engine hooks. Generally set automatically by include "imgui_te_config.h", see Test Engine for details.
|
||||
|
||||
//---- Include imgui_user.h at the end of imgui.h as a convenience
|
||||
// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included.
|
||||
//#define IMGUI_INCLUDE_IMGUI_USER_H
|
||||
//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h"
|
||||
|
||||
//---- Pack vertex colors as BGRA8 instead of RGBA8 (to avoid converting from one to another). Need dedicated backend support.
|
||||
//#define IMGUI_USE_BGRA_PACKED_COLOR
|
||||
|
||||
//---- Use legacy CRC32-adler tables (used before 1.91.6), in order to preserve old .ini data that you cannot afford to invalidate.
|
||||
//#define IMGUI_USE_LEGACY_CRC32_ADLER
|
||||
|
||||
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
|
||||
//#define IMGUI_USE_WCHAR32
|
||||
|
||||
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
|
||||
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
|
||||
//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h"
|
||||
//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h"
|
||||
//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if IMGUI_USE_STB_SPRINTF is defined.
|
||||
//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION
|
||||
//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION
|
||||
//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION // only disabled if IMGUI_USE_STB_SPRINTF is defined.
|
||||
|
||||
//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined)
|
||||
// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h.
|
||||
//#define IMGUI_USE_STB_SPRINTF
|
||||
|
||||
//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui)
|
||||
// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided).
|
||||
// Note that imgui_freetype.cpp may be used _without_ this define, if you manually call ImFontAtlas::SetFontLoader(). The define is simply a convenience.
|
||||
// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'.
|
||||
//#define IMGUI_ENABLE_FREETYPE
|
||||
|
||||
//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT)
|
||||
// Only works in combination with IMGUI_ENABLE_FREETYPE.
|
||||
// - plutosvg is currently easier to install, as e.g. it is part of vcpkg. It will support more fonts and may load them faster. See misc/freetype/README for instructions.
|
||||
// - Both require headers to be available in the include path + program to be linked with the library code (not provided).
|
||||
// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement)
|
||||
//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
//#define IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
//---- Use stb_truetype to build and rasterize the font atlas (default)
|
||||
// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend.
|
||||
//#define IMGUI_ENABLE_STB_TRUETYPE
|
||||
|
||||
//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4.
|
||||
// This will be inlined as part of ImVec2 and ImVec4 class declarations.
|
||||
/*
|
||||
#define IM_VEC2_CLASS_EXTRA \
|
||||
constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {} \
|
||||
operator MyVec2() const { return MyVec2(x,y); }
|
||||
|
||||
#define IM_VEC4_CLASS_EXTRA \
|
||||
constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {} \
|
||||
operator MyVec4() const { return MyVec4(x,y,z,w); }
|
||||
*/
|
||||
//---- ...Or use Dear ImGui's own very basic math operators.
|
||||
//#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
|
||||
//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices.
|
||||
// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices).
|
||||
// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer.
|
||||
// Read about ImGuiBackendFlags_RendererHasVtxOffset for details.
|
||||
//#define ImDrawIdx unsigned int
|
||||
|
||||
//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly)
|
||||
//struct ImDrawList;
|
||||
//struct ImDrawCmd;
|
||||
//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data);
|
||||
//#define ImDrawCallback MyImDrawCallback
|
||||
|
||||
//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase)
|
||||
// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.)
|
||||
//#define IM_DEBUG_BREAK IM_ASSERT(0)
|
||||
//#define IM_DEBUG_BREAK __debugbreak()
|
||||
|
||||
//---- Debug Tools: Enable highlight ID conflicts _before_ hovering items. When io.ConfigDebugHighlightIdConflicts is set.
|
||||
// (THIS WILL SLOW DOWN DEAR IMGUI. Only use occasionally and disable after use)
|
||||
//#define IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS
|
||||
|
||||
//---- Debug Tools: Enable slower asserts
|
||||
//#define IMGUI_DEBUG_PARANOID
|
||||
|
||||
//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files)
|
||||
/*
|
||||
namespace ImGui
|
||||
{
|
||||
void MyFunction(const char* name, MyMatrix44* mtx);
|
||||
}
|
||||
*/
|
||||
+17
-12
@@ -1,9 +1,9 @@
|
||||
#include "animation.h"
|
||||
#include "animation.hpp"
|
||||
|
||||
#include "map_.h"
|
||||
#include "math_.h"
|
||||
#include "unordered_map_.h"
|
||||
#include "xml_.h"
|
||||
#include "map_.hpp"
|
||||
#include "math_.hpp"
|
||||
#include "unordered_map_.hpp"
|
||||
#include "xml_.hpp"
|
||||
#include <ranges>
|
||||
|
||||
using namespace anm2ed::util;
|
||||
@@ -78,34 +78,37 @@ namespace anm2ed::anm2
|
||||
}
|
||||
}
|
||||
|
||||
XMLElement* Animation::to_element(XMLDocument& document)
|
||||
XMLElement* Animation::to_element(XMLDocument& document, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement("Animation");
|
||||
element->SetAttribute("Name", name.c_str());
|
||||
element->SetAttribute("FrameNum", frameNum);
|
||||
element->SetAttribute("Loop", isLoop);
|
||||
|
||||
rootAnimation.serialize(document, element, ROOT);
|
||||
rootAnimation.serialize(document, element, ROOT, -1, flags);
|
||||
|
||||
auto layerAnimationsElement = document.NewElement("LayerAnimations");
|
||||
for (auto& i : layerOrder)
|
||||
{
|
||||
Item& layerAnimation = layerAnimations.at(i);
|
||||
layerAnimation.serialize(document, layerAnimationsElement, LAYER, i);
|
||||
layerAnimation.serialize(document, layerAnimationsElement, LAYER, i, flags);
|
||||
}
|
||||
element->InsertEndChild(layerAnimationsElement);
|
||||
|
||||
auto nullAnimationsElement = document.NewElement("NullAnimations");
|
||||
for (auto& [id, nullAnimation] : nullAnimations)
|
||||
nullAnimation.serialize(document, nullAnimationsElement, NULL_, id);
|
||||
nullAnimation.serialize(document, nullAnimationsElement, NULL_, id, flags);
|
||||
element->InsertEndChild(nullAnimationsElement);
|
||||
|
||||
triggers.serialize(document, element, TRIGGER);
|
||||
triggers.serialize(document, element, TRIGGER, -1, flags);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
void Animation::serialize(XMLDocument& document, XMLElement* parent) { parent->InsertEndChild(to_element(document)); }
|
||||
void Animation::serialize(XMLDocument& document, XMLElement* parent, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, flags));
|
||||
}
|
||||
|
||||
std::string Animation::to_string()
|
||||
{
|
||||
@@ -158,6 +161,8 @@ namespace anm2ed::anm2
|
||||
|
||||
for (auto& [id, layerAnimation] : layerAnimations)
|
||||
{
|
||||
if (!layerAnimation.isVisible) continue;
|
||||
|
||||
auto frame = layerAnimation.frame_generate(t, LAYER);
|
||||
|
||||
if (frame.size == vec2() || !frame.isVisible) continue;
|
||||
@@ -179,4 +184,4 @@ namespace anm2ed::anm2
|
||||
if (!any) return vec4(-1.0f);
|
||||
return {minX, minY, maxX - minX, maxY - minY};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "item.h"
|
||||
#include "item.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
@@ -26,12 +26,12 @@ namespace anm2ed::anm2
|
||||
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*);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0);
|
||||
std::string to_string();
|
||||
int length();
|
||||
void fit_length();
|
||||
glm::vec4 rect(bool);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "animations.h"
|
||||
#include "animations.hpp"
|
||||
|
||||
#include "xml_.h"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace tinyxml2;
|
||||
using namespace anm2ed::types;
|
||||
@@ -16,18 +16,18 @@ namespace anm2ed::anm2
|
||||
items.push_back(Animation(child));
|
||||
}
|
||||
|
||||
XMLElement* Animations::to_element(XMLDocument& document)
|
||||
XMLElement* Animations::to_element(XMLDocument& document, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement("Animations");
|
||||
element->SetAttribute("DefaultAnimation", defaultAnimation.c_str());
|
||||
for (auto& animation : items)
|
||||
animation.serialize(document, element);
|
||||
animation.serialize(document, element, flags);
|
||||
return element;
|
||||
}
|
||||
|
||||
void Animations::serialize(XMLDocument& document, XMLElement* parent)
|
||||
void Animations::serialize(XMLDocument& document, XMLElement* parent, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document));
|
||||
parent->InsertEndChild(to_element(document, flags));
|
||||
}
|
||||
|
||||
int Animations::length()
|
||||
@@ -40,4 +40,4 @@ namespace anm2ed::anm2
|
||||
return length;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "animation.h"
|
||||
#include "animation.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
@@ -13,8 +13,8 @@ namespace anm2ed::anm2
|
||||
|
||||
Animations() = default;
|
||||
Animations(tinyxml2::XMLElement*);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0);
|
||||
int length();
|
||||
};
|
||||
}
|
||||
}
|
||||
+237
-30
@@ -1,35 +1,171 @@
|
||||
#include "anm2.h"
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "filesystem_.h"
|
||||
#include "map_.h"
|
||||
#include "time_.h"
|
||||
#include "vector_.h"
|
||||
#include "xml_.h"
|
||||
#include "file_.hpp"
|
||||
#include "map_.hpp"
|
||||
#include "math_.hpp"
|
||||
#include "time_.hpp"
|
||||
#include "vector_.hpp"
|
||||
#include "working_directory.hpp"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace tinyxml2;
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace
|
||||
{
|
||||
int remap_id(const std::unordered_map<int, int>& table, int value)
|
||||
{
|
||||
if (value < 0) return value;
|
||||
if (auto it = table.find(value); it != table.end()) return it->second;
|
||||
return value;
|
||||
}
|
||||
|
||||
void region_frames_sync(anm2ed::anm2::Anm2& anm2, bool clearInvalid)
|
||||
{
|
||||
for (auto& animation : anm2.animations.items)
|
||||
{
|
||||
for (auto& [layerId, layerAnimation] : animation.layerAnimations)
|
||||
{
|
||||
if (!anm2.content.layers.contains(layerId)) continue;
|
||||
auto& layer = anm2.content.layers.at(layerId);
|
||||
auto spritesheet = anm2.spritesheet_get(layer.spritesheetID);
|
||||
if (!spritesheet) continue;
|
||||
|
||||
for (auto& frame : layerAnimation.frames)
|
||||
{
|
||||
if (frame.regionID == -1) continue;
|
||||
auto regionIt = spritesheet->regions.find(frame.regionID);
|
||||
if (regionIt == spritesheet->regions.end())
|
||||
{
|
||||
if (clearInvalid) frame.regionID = -1;
|
||||
continue;
|
||||
}
|
||||
frame.crop = regionIt->second.crop;
|
||||
frame.size = regionIt->second.size;
|
||||
frame.pivot = regionIt->second.pivot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
Frame Anm2::frame_effective(int layerId, const Frame& frame) const
|
||||
{
|
||||
auto resolved = frame;
|
||||
if (frame.regionID == -1) return resolved;
|
||||
if (!content.layers.contains(layerId)) return resolved;
|
||||
|
||||
auto spritesheet = const_cast<Anm2*>(this)->spritesheet_get(content.layers.at(layerId).spritesheetID);
|
||||
if (!spritesheet) return resolved;
|
||||
|
||||
auto regionIt = spritesheet->regions.find(frame.regionID);
|
||||
if (regionIt == spritesheet->regions.end()) return resolved;
|
||||
|
||||
resolved.crop = regionIt->second.crop;
|
||||
resolved.size = regionIt->second.size;
|
||||
resolved.pivot = regionIt->second.pivot;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
vec4 Anm2::animation_rect(Animation& animation, bool isRootTransform) const
|
||||
{
|
||||
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)animation.frameNum; t += 1.0f)
|
||||
{
|
||||
mat4 transform(1.0f);
|
||||
|
||||
if (isRootTransform)
|
||||
{
|
||||
auto root = animation.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] : animation.layerAnimations)
|
||||
{
|
||||
if (!layerAnimation.isVisible) continue;
|
||||
|
||||
auto frame = frame_effective(id, 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};
|
||||
}
|
||||
|
||||
void Anm2::bake_special_interpolated_frames(int interval, bool isRoundScale, bool isRoundRotation)
|
||||
{
|
||||
auto bake_item = [&](Item& item)
|
||||
{
|
||||
for (int i = (int)item.frames.size() - 1; i >= 0; --i)
|
||||
{
|
||||
auto interpolation = item.frames[i].interpolation;
|
||||
if (interpolation == Frame::Interpolation::NONE || interpolation == Frame::Interpolation::LINEAR) continue;
|
||||
item.frames_bake(i, interval, isRoundScale, isRoundRotation);
|
||||
}
|
||||
};
|
||||
|
||||
for (auto& animation : animations.items)
|
||||
{
|
||||
bake_item(animation.rootAnimation);
|
||||
for (auto& item : animation.layerAnimations | std::views::values)
|
||||
bake_item(item);
|
||||
for (auto& item : animation.nullAnimations | std::views::values)
|
||||
bake_item(item);
|
||||
}
|
||||
}
|
||||
|
||||
Anm2::Anm2(const std::filesystem::path& path, std::string* errorString)
|
||||
{
|
||||
XMLDocument document;
|
||||
|
||||
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
|
||||
File file(path, "rb");
|
||||
if (!file)
|
||||
{
|
||||
if (errorString) *errorString = document.ErrorStr();
|
||||
if (errorString) *errorString = localize.get(ERROR_FILE_NOT_FOUND);
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
filesystem::WorkingDirectory workingDirectory(path, true);
|
||||
if (document.LoadFile(file.get()) != XML_SUCCESS)
|
||||
{
|
||||
if (errorString) *errorString = document.ErrorStr();
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
WorkingDirectory workingDirectory(path, WorkingDirectory::FILE);
|
||||
|
||||
const XMLElement* element = document.RootElement();
|
||||
|
||||
@@ -37,26 +173,39 @@ namespace anm2ed::anm2
|
||||
if (auto contentElement = element->FirstChildElement("Content")) content = Content((XMLElement*)contentElement);
|
||||
if (auto animationsElement = element->FirstChildElement("Animations"))
|
||||
animations = Animations((XMLElement*)animationsElement);
|
||||
|
||||
region_frames_sync(*this, true);
|
||||
}
|
||||
|
||||
XMLElement* Anm2::to_element(XMLDocument& document)
|
||||
XMLElement* Anm2::to_element(XMLDocument& document, Flags flags)
|
||||
{
|
||||
auto normalized = normalized_for_serialize();
|
||||
region_frames_sync(normalized, true);
|
||||
auto element = document.NewElement("AnimatedActor");
|
||||
document.InsertFirstChild(element);
|
||||
|
||||
info.serialize(document, element);
|
||||
content.serialize(document, element);
|
||||
animations.serialize(document, element);
|
||||
normalized.info.serialize(document, element);
|
||||
normalized.content.serialize(document, element, flags);
|
||||
normalized.animations.serialize(document, element, flags);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
bool Anm2::serialize(const std::string& path, std::string* errorString)
|
||||
bool Anm2::serialize(const std::filesystem::path& path, std::string* errorString, Flags flags,
|
||||
bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation)
|
||||
{
|
||||
XMLDocument document;
|
||||
document.InsertFirstChild(to_element(document));
|
||||
auto serialized = *this;
|
||||
if (isBakeSpecialInterpolatedFramesOnSave) serialized.bake_special_interpolated_frames(1, isRoundScale, isRoundRotation);
|
||||
document.InsertFirstChild(serialized.to_element(document, flags));
|
||||
|
||||
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
|
||||
File file(path, "wb");
|
||||
if (!file)
|
||||
{
|
||||
if (errorString) *errorString = localize.get(ERROR_FILE_PERMISSIONS);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.SaveFile(file.get()) != XML_SUCCESS)
|
||||
{
|
||||
if (errorString) *errorString = document.ErrorStr();
|
||||
return false;
|
||||
@@ -64,15 +213,79 @@ namespace anm2ed::anm2
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Anm2::to_string()
|
||||
std::string Anm2::to_string(Flags flags)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document));
|
||||
document.InsertEndChild(to_element(document, flags));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
uint64_t Anm2::hash() { return std::hash<std::string>{}(to_string()); }
|
||||
|
||||
Anm2 Anm2::normalized_for_serialize() const
|
||||
{
|
||||
auto normalized = *this;
|
||||
auto sanitize_layer_order = [](Animation& animation)
|
||||
{
|
||||
std::vector<int> sanitized{};
|
||||
sanitized.reserve(animation.layerAnimations.size());
|
||||
std::set<int> seen{};
|
||||
|
||||
for (auto id : animation.layerOrder)
|
||||
{
|
||||
if (!animation.layerAnimations.contains(id)) continue;
|
||||
if (!seen.insert(id).second) continue;
|
||||
sanitized.push_back(id);
|
||||
}
|
||||
|
||||
std::vector<int> missing{};
|
||||
missing.reserve(animation.layerAnimations.size());
|
||||
for (auto& id : animation.layerAnimations | std::views::keys)
|
||||
if (!seen.contains(id)) missing.push_back(id);
|
||||
|
||||
std::sort(missing.begin(), missing.end());
|
||||
sanitized.insert(sanitized.end(), missing.begin(), missing.end());
|
||||
animation.layerOrder = std::move(sanitized);
|
||||
};
|
||||
std::unordered_map<int, int> layerRemap{};
|
||||
|
||||
int normalizedID = 0;
|
||||
for (auto& [layerID, layer] : content.layers)
|
||||
{
|
||||
layerRemap[layerID] = normalizedID;
|
||||
++normalizedID;
|
||||
}
|
||||
|
||||
normalized.content.layers.clear();
|
||||
for (auto& [layerID, layer] : content.layers)
|
||||
normalized.content.layers[remap_id(layerRemap, layerID)] = layer;
|
||||
|
||||
for (auto& animation : normalized.animations.items)
|
||||
{
|
||||
sanitize_layer_order(animation);
|
||||
std::unordered_map<int, Item> layerAnimations{};
|
||||
std::vector<int> layerOrder{};
|
||||
|
||||
for (auto layerID : animation.layerOrder)
|
||||
{
|
||||
auto mappedID = remap_id(layerRemap, layerID);
|
||||
if (mappedID >= 0) layerOrder.push_back(mappedID);
|
||||
}
|
||||
|
||||
for (auto& [layerID, item] : animation.layerAnimations)
|
||||
{
|
||||
auto mappedID = remap_id(layerRemap, layerID);
|
||||
if (mappedID >= 0) layerAnimations[mappedID] = item;
|
||||
}
|
||||
|
||||
animation.layerAnimations = std::move(layerAnimations);
|
||||
animation.layerOrder = std::move(layerOrder);
|
||||
sanitize_layer_order(animation);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
Frame* Anm2::frame_get(int animationIndex, Type itemType, int frameIndex, int itemID)
|
||||
{
|
||||
if (auto item = item_get(animationIndex, itemType, itemID); item)
|
||||
@@ -127,13 +340,6 @@ namespace anm2ed::anm2
|
||||
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{};
|
||||
@@ -219,7 +425,6 @@ namespace anm2ed::anm2
|
||||
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)
|
||||
@@ -236,7 +441,8 @@ namespace anm2ed::anm2
|
||||
{
|
||||
for (auto& frame : item.frames)
|
||||
{
|
||||
frame.soundID = remap_id(soundRemap, frame.soundID);
|
||||
for (auto& soundID : frame.soundIDs)
|
||||
soundID = remap_id(soundRemap, soundID);
|
||||
frame.eventID = remap_id(eventRemap, frame.eventID);
|
||||
}
|
||||
};
|
||||
@@ -326,7 +532,8 @@ namespace anm2ed::anm2
|
||||
animations.items.push_back(std::move(processed));
|
||||
}
|
||||
|
||||
if (animations.defaultAnimation.empty() && !source.animations.defaultAnimation.empty()) {
|
||||
if (animations.defaultAnimation.empty() && !source.animations.defaultAnimation.empty())
|
||||
{
|
||||
animations.defaultAnimation = source.animations.defaultAnimation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
#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 = {});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
#include "types.hpp"
|
||||
|
||||
#include "animations.hpp"
|
||||
#include "content.hpp"
|
||||
#include "info.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
struct Reference
|
||||
{
|
||||
int animationIndex{-1};
|
||||
Type itemType{NONE};
|
||||
int itemID{-1};
|
||||
int frameIndex{-1};
|
||||
|
||||
auto operator<=>(const Reference&) const = default;
|
||||
};
|
||||
|
||||
class Anm2
|
||||
{
|
||||
public:
|
||||
bool isValid{true};
|
||||
Info info{};
|
||||
Content content{};
|
||||
Animations animations{};
|
||||
|
||||
Anm2();
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
|
||||
bool serialize(const std::filesystem::path&, std::string* = nullptr, Flags = 0, bool = false, bool = true,
|
||||
bool = true);
|
||||
std::string to_string(Flags = 0);
|
||||
Anm2(const std::filesystem::path&, std::string* = nullptr);
|
||||
uint64_t hash();
|
||||
Anm2 normalized_for_serialize() const;
|
||||
|
||||
Spritesheet* spritesheet_get(int);
|
||||
bool spritesheet_add(const std::filesystem::path&, const std::filesystem::path&, int&);
|
||||
bool spritesheet_pack(int, int);
|
||||
bool regions_trim(int, const std::set<int>&);
|
||||
std::vector<std::string> spritesheet_labels_get();
|
||||
std::vector<int> spritesheet_ids_get();
|
||||
std::set<int> spritesheets_unused();
|
||||
bool spritesheets_merge(const std::set<int>&, SpritesheetMergeOrigin, bool, bool, origin::Type);
|
||||
bool spritesheets_deserialize(const std::string&, const std::filesystem::path&, types::merge::Type type,
|
||||
std::string*);
|
||||
std::vector<std::string> region_labels_get(Spritesheet&);
|
||||
std::vector<int> region_ids_get(Spritesheet&);
|
||||
std::set<int> regions_unused(Spritesheet&);
|
||||
void scan_and_set_regions();
|
||||
|
||||
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::vector<int> event_ids_get();
|
||||
std::set<int> events_unused();
|
||||
bool events_deserialize(const std::string&, types::merge::Type, std::string*);
|
||||
|
||||
bool sound_add(const std::filesystem::path& directory, const std::filesystem::path& path, int& id);
|
||||
std::vector<std::string> sound_labels_get();
|
||||
std::vector<int> sound_ids_get();
|
||||
std::set<int> sounds_unused();
|
||||
bool sounds_deserialize(const std::string&, const std::filesystem::path&, 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);
|
||||
Frame frame_effective(int, const Frame&) const;
|
||||
glm::vec4 animation_rect(Animation&, bool) const;
|
||||
void bake_special_interpolated_frames(int, bool, bool);
|
||||
|
||||
Item* item_get(int, Type, int = -1);
|
||||
Reference layer_animation_add(Reference = {}, int = -1, std::string = {}, int = 0,
|
||||
types::destination::Type = types::destination::ALL);
|
||||
Reference null_animation_add(Reference = {}, std::string = {}, bool = false,
|
||||
types::destination::Type = types::destination::ALL);
|
||||
|
||||
Frame* frame_get(int, Type, int, int = -1);
|
||||
void merge(const Anm2&, const std::filesystem::path&, const std::filesystem::path&);
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "anm2.h"
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include "vector_.h"
|
||||
#include "vector_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::types;
|
||||
@@ -37,11 +37,14 @@ namespace anm2ed::anm2
|
||||
element = element->NextSiblingElement("Animation"))
|
||||
{
|
||||
auto index = start + count;
|
||||
animations.items.insert(animations.items.begin() + start + count, Animation(element));
|
||||
auto& animation = *animations.items.insert(animations.items.begin() + start + count, Animation(element));
|
||||
|
||||
for (auto& trigger : animation.triggers.frames)
|
||||
if (!content.events.contains(trigger.eventID)) trigger.eventID = -1;
|
||||
|
||||
indices.insert(index);
|
||||
count++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
@@ -133,4 +136,4 @@ namespace anm2ed::anm2
|
||||
return finalIndex;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "anm2.h"
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
#include "map_.h"
|
||||
#include "map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
@@ -19,19 +19,28 @@ namespace anm2ed::anm2
|
||||
std::vector<std::string> Anm2::event_labels_get()
|
||||
{
|
||||
std::vector<std::string> labels{};
|
||||
labels.emplace_back("None");
|
||||
labels.emplace_back(localize.get(BASIC_NONE));
|
||||
for (auto& event : content.events | std::views::values)
|
||||
labels.emplace_back(event.name);
|
||||
return labels;
|
||||
}
|
||||
|
||||
std::vector<int> Anm2::event_ids_get()
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
ids.emplace_back(-1);
|
||||
for (auto& id : content.events | std::views::keys)
|
||||
ids.emplace_back(id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
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);
|
||||
if (frame.eventID != -1) used.insert(frame.eventID);
|
||||
|
||||
std::set<int> unused{};
|
||||
for (auto& id : content.events | std::views::keys)
|
||||
|
||||
+32
-16
@@ -1,8 +1,8 @@
|
||||
#include "anm2.h"
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include "map_.h"
|
||||
#include "types.h"
|
||||
#include "unordered_map_.h"
|
||||
#include "map_.hpp"
|
||||
#include "types.hpp"
|
||||
#include "unordered_map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
@@ -30,7 +30,8 @@ namespace anm2ed::anm2
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Reference Anm2::layer_animation_add(Reference reference, std::string name, int spritesheetID, locale::Type locale)
|
||||
Reference Anm2::layer_animation_add(Reference reference, int insertBeforeID, std::string name, int spritesheetID,
|
||||
destination::Type destination)
|
||||
{
|
||||
auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID;
|
||||
auto& layer = content.layers[id];
|
||||
@@ -38,46 +39,61 @@ namespace anm2ed::anm2
|
||||
layer.name = !name.empty() ? name : layer.name;
|
||||
layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0;
|
||||
|
||||
auto add = [&](Animation* animation, int id)
|
||||
auto add = [&](Animation* animation, int id, bool insertBeforeReference)
|
||||
{
|
||||
animation->layerAnimations[id] = Item();
|
||||
|
||||
if (insertBeforeReference && insertBeforeID != -1)
|
||||
{
|
||||
auto it = std::find(animation->layerOrder.begin(), animation->layerOrder.end(), insertBeforeID);
|
||||
if (it != animation->layerOrder.end())
|
||||
{
|
||||
animation->layerOrder.insert(it, id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
animation->layerOrder.push_back(id);
|
||||
};
|
||||
|
||||
if (locale == locale::GLOBAL)
|
||||
if (destination == destination::ALL)
|
||||
{
|
||||
for (auto& animation : animations.items)
|
||||
if (!animation.layerAnimations.contains(id)) add(&animation, id);
|
||||
for (size_t index = 0; index < animations.items.size(); ++index)
|
||||
{
|
||||
auto& animation = animations.items[index];
|
||||
if (!animation.layerAnimations.contains(id)) add(&animation, id, true);
|
||||
}
|
||||
}
|
||||
else if (locale == locale::LOCAL)
|
||||
else if (destination == destination::THIS)
|
||||
{
|
||||
if (auto animation = animation_get(reference.animationIndex))
|
||||
if (!animation->layerAnimations.contains(id)) add(animation, id);
|
||||
if (!animation->layerAnimations.contains(id)) add(animation, id, true);
|
||||
}
|
||||
|
||||
return {reference.animationIndex, LAYER, id};
|
||||
}
|
||||
|
||||
Reference Anm2::null_animation_add(Reference reference, std::string name, locale::Type locale)
|
||||
Reference Anm2::null_animation_add(Reference reference, std::string name, bool isShowRect, destination::Type destination)
|
||||
{
|
||||
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;
|
||||
null.isShowRect = isShowRect;
|
||||
|
||||
auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); };
|
||||
|
||||
if (locale == locale::GLOBAL)
|
||||
if (destination == destination::ALL)
|
||||
{
|
||||
for (auto& animation : animations.items)
|
||||
if (!animation.nullAnimations.contains(id)) add(&animation, id);
|
||||
}
|
||||
else if (locale == locale::LOCAL)
|
||||
else if (destination == destination::THIS)
|
||||
{
|
||||
if (auto animation = animation_get(reference.animationIndex))
|
||||
if (!animation->nullAnimations.contains(id)) add(animation, id);
|
||||
}
|
||||
|
||||
return {reference.animationIndex, LAYER, id};
|
||||
return {reference.animationIndex, NULL_, id};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "anm2.h"
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
#include "map_.h"
|
||||
#include "map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "anm2.h"
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
#include "map_.h"
|
||||
#include "map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "anm2.h"
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include "filesystem_.h"
|
||||
#include "map_.h"
|
||||
#include <format>
|
||||
|
||||
#include "map_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "working_directory.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
@@ -9,7 +12,7 @@ using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
bool Anm2::sound_add(const std::string& directory, const std::string& path, int& id)
|
||||
bool Anm2::sound_add(const std::filesystem::path& directory, const std::filesystem::path& path, int& id)
|
||||
{
|
||||
id = map::next_id_get(content.sounds);
|
||||
content.sounds[id] = Sound(directory, path);
|
||||
@@ -19,18 +22,31 @@ namespace anm2ed::anm2
|
||||
std::vector<std::string> Anm2::sound_labels_get()
|
||||
{
|
||||
std::vector<std::string> labels{};
|
||||
labels.emplace_back("None");
|
||||
labels.emplace_back(localize.get(BASIC_NONE));
|
||||
for (auto& [id, sound] : content.sounds)
|
||||
labels.emplace_back(sound.path.string());
|
||||
{
|
||||
auto pathString = path::to_utf8(sound.path);
|
||||
labels.emplace_back(std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(id, pathString)));
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
std::vector<int> Anm2::sound_ids_get()
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
ids.emplace_back(-1);
|
||||
for (auto& [id, sound] : content.sounds)
|
||||
ids.emplace_back(id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
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);
|
||||
for (auto& soundID : trigger.soundIDs)
|
||||
if (content.sounds.contains(soundID)) used.insert(soundID);
|
||||
|
||||
std::set<int> unused;
|
||||
for (auto& [id, sound] : content.sounds)
|
||||
@@ -39,7 +55,7 @@ namespace anm2ed::anm2
|
||||
return unused;
|
||||
}
|
||||
|
||||
bool Anm2::sounds_deserialize(const std::string& string, const std::string& directory, merge::Type type,
|
||||
bool Anm2::sounds_deserialize(const std::string& string, const std::filesystem::path& directory, merge::Type type,
|
||||
std::string* errorString)
|
||||
{
|
||||
XMLDocument document{};
|
||||
@@ -54,7 +70,7 @@ namespace anm2ed::anm2
|
||||
return false;
|
||||
}
|
||||
|
||||
filesystem::WorkingDirectory workingDirectory(directory);
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
|
||||
for (auto element = document.FirstChildElement("Sound"); element; element = element->NextSiblingElement("Sound"))
|
||||
{
|
||||
|
||||
+628
-10
@@ -1,9 +1,16 @@
|
||||
#include "anm2.h"
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <limits>
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "filesystem_.h"
|
||||
#include "map_.h"
|
||||
#include "map_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "working_directory.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
@@ -13,7 +20,7 @@ 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)
|
||||
bool Anm2::spritesheet_add(const std::filesystem::path& directory, const std::filesystem::path& path, int& id)
|
||||
{
|
||||
Spritesheet spritesheet(directory, path);
|
||||
if (!spritesheet.is_valid()) return false;
|
||||
@@ -22,6 +29,367 @@ namespace anm2ed::anm2
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Anm2::spritesheet_pack(int id, int padding)
|
||||
{
|
||||
const int packingPadding = std::max(0, padding);
|
||||
|
||||
struct RectI
|
||||
{
|
||||
int x{};
|
||||
int y{};
|
||||
int w{};
|
||||
int h{};
|
||||
};
|
||||
|
||||
struct PackItem
|
||||
{
|
||||
int regionID{-1};
|
||||
int srcX{};
|
||||
int srcY{};
|
||||
int width{};
|
||||
int height{};
|
||||
int packWidth{};
|
||||
int packHeight{};
|
||||
};
|
||||
|
||||
class MaxRectsPacker
|
||||
{
|
||||
int width{};
|
||||
int height{};
|
||||
std::vector<RectI> freeRects{};
|
||||
|
||||
static bool intersects(const RectI& a, const RectI& b)
|
||||
{
|
||||
return !(b.x >= a.x + a.w || b.x + b.w <= a.x || b.y >= a.y + a.h || b.y + b.h <= a.y);
|
||||
}
|
||||
|
||||
static bool contains(const RectI& a, const RectI& b)
|
||||
{
|
||||
return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
|
||||
}
|
||||
|
||||
void split_free_rects(const RectI& used)
|
||||
{
|
||||
std::vector<RectI> next{};
|
||||
next.reserve(freeRects.size() * 2);
|
||||
|
||||
for (auto& free : freeRects)
|
||||
{
|
||||
if (!intersects(free, used))
|
||||
{
|
||||
next.push_back(free);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (used.x > free.x) next.push_back({free.x, free.y, used.x - free.x, free.h});
|
||||
if (used.x + used.w < free.x + free.w)
|
||||
next.push_back({used.x + used.w, free.y, free.x + free.w - (used.x + used.w), free.h});
|
||||
if (used.y > free.y) next.push_back({free.x, free.y, free.w, used.y - free.y});
|
||||
if (used.y + used.h < free.y + free.h)
|
||||
next.push_back({free.x, used.y + used.h, free.w, free.y + free.h - (used.y + used.h)});
|
||||
}
|
||||
|
||||
freeRects = std::move(next);
|
||||
}
|
||||
|
||||
void prune_free_rects()
|
||||
{
|
||||
for (int i = 0; i < (int)freeRects.size(); i++)
|
||||
{
|
||||
if (freeRects[i].w <= 0 || freeRects[i].h <= 0)
|
||||
{
|
||||
freeRects.erase(freeRects.begin() + i--);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = i + 1; j < (int)freeRects.size();)
|
||||
{
|
||||
if (contains(freeRects[i], freeRects[j]))
|
||||
freeRects.erase(freeRects.begin() + j);
|
||||
else if (contains(freeRects[j], freeRects[i]))
|
||||
{
|
||||
freeRects.erase(freeRects.begin() + i);
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
else
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
MaxRectsPacker(int width, int height) : width(width), height(height), freeRects({{0, 0, width, height}}) {}
|
||||
|
||||
bool insert(int width, int height, RectI& result)
|
||||
{
|
||||
int bestShort = std::numeric_limits<int>::max();
|
||||
int bestLong = std::numeric_limits<int>::max();
|
||||
RectI best{};
|
||||
bool found{};
|
||||
|
||||
for (auto& free : freeRects)
|
||||
{
|
||||
if (width > free.w || height > free.h) continue;
|
||||
|
||||
int leftOverW = free.w - width;
|
||||
int leftOverH = free.h - height;
|
||||
int shortSide = std::min(leftOverW, leftOverH);
|
||||
int longSide = std::max(leftOverW, leftOverH);
|
||||
|
||||
if (shortSide < bestShort || (shortSide == bestShort && longSide < bestLong))
|
||||
{
|
||||
bestShort = shortSide;
|
||||
bestLong = longSide;
|
||||
best = {free.x, free.y, width, height};
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) return false;
|
||||
|
||||
result = best;
|
||||
split_free_rects(best);
|
||||
prune_free_rects();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
auto pack_regions = [&](const std::vector<PackItem>& items, int& packedWidth, int& packedHeight,
|
||||
std::unordered_map<int, RectI>& packedRects)
|
||||
{
|
||||
if (items.empty()) return false;
|
||||
|
||||
int maxWidth{};
|
||||
int maxHeight{};
|
||||
int sumWidth{};
|
||||
int sumHeight{};
|
||||
int64_t totalArea{};
|
||||
for (auto& item : items)
|
||||
{
|
||||
maxWidth = std::max(maxWidth, item.packWidth);
|
||||
maxHeight = std::max(maxHeight, item.packHeight);
|
||||
sumWidth += item.packWidth;
|
||||
sumHeight += item.packHeight;
|
||||
totalArea += (int64_t)item.packWidth * item.packHeight;
|
||||
}
|
||||
|
||||
if (maxWidth <= 0 || maxHeight <= 0) return false;
|
||||
|
||||
int bestSquareDelta = std::numeric_limits<int>::max();
|
||||
int bestArea = std::numeric_limits<int>::max();
|
||||
int bestWidth{};
|
||||
int bestHeight{};
|
||||
std::unordered_map<int, RectI> bestRects{};
|
||||
|
||||
int startWidth = maxWidth;
|
||||
int endWidth = std::max(startWidth, sumWidth);
|
||||
int step = std::max(1, (endWidth - startWidth) / 512);
|
||||
|
||||
for (int candidateWidth = startWidth; candidateWidth <= endWidth; candidateWidth += step)
|
||||
{
|
||||
int candidateHeightMin = std::max(maxHeight, (int)std::ceil((double)totalArea / candidateWidth));
|
||||
bool isValid{};
|
||||
int usedWidth{};
|
||||
int usedHeight{};
|
||||
std::unordered_map<int, RectI> candidateRects{};
|
||||
|
||||
// Grow candidate height until this width can actually fit all rectangles.
|
||||
for (int candidateHeight = candidateHeightMin; candidateHeight <= sumHeight; candidateHeight++)
|
||||
{
|
||||
MaxRectsPacker packer(candidateWidth, candidateHeight);
|
||||
candidateRects.clear();
|
||||
isValid = true;
|
||||
usedWidth = 0;
|
||||
usedHeight = 0;
|
||||
|
||||
for (auto& item : items)
|
||||
{
|
||||
RectI rect{};
|
||||
if (!packer.insert(item.packWidth, item.packHeight, rect))
|
||||
{
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
candidateRects[item.regionID] = rect;
|
||||
usedWidth = std::max(usedWidth, rect.x + rect.w);
|
||||
usedHeight = std::max(usedHeight, rect.y + rect.h);
|
||||
}
|
||||
|
||||
if (isValid) break;
|
||||
}
|
||||
|
||||
if (!isValid) continue;
|
||||
|
||||
int area = usedWidth * usedHeight;
|
||||
int squareDelta = std::abs(usedWidth - usedHeight);
|
||||
if (squareDelta < bestSquareDelta || (squareDelta == bestSquareDelta && area < bestArea))
|
||||
{
|
||||
bestSquareDelta = squareDelta;
|
||||
bestArea = area;
|
||||
bestWidth = usedWidth;
|
||||
bestHeight = usedHeight;
|
||||
bestRects = std::move(candidateRects);
|
||||
if (bestArea == totalArea && bestSquareDelta == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestArea == std::numeric_limits<int>::max()) return false;
|
||||
|
||||
packedWidth = bestWidth;
|
||||
packedHeight = bestHeight;
|
||||
packedRects = std::move(bestRects);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!content.spritesheets.contains(id)) return false;
|
||||
auto& spritesheet = content.spritesheets.at(id);
|
||||
if (!spritesheet.texture.is_valid() || spritesheet.texture.pixels.empty()) return false;
|
||||
if (spritesheet.regions.empty()) return false;
|
||||
|
||||
std::vector<PackItem> items{};
|
||||
items.reserve(spritesheet.regions.size());
|
||||
|
||||
for (auto& [regionID, region] : spritesheet.regions)
|
||||
{
|
||||
auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size));
|
||||
auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size));
|
||||
auto size = glm::max(maxPoint - minPoint, glm::ivec2(1));
|
||||
int packWidth = size.x + packingPadding * 2;
|
||||
int packHeight = size.y + packingPadding * 2;
|
||||
items.push_back({regionID, minPoint.x, minPoint.y, size.x, size.y, packWidth, packHeight});
|
||||
}
|
||||
|
||||
std::sort(items.begin(), items.end(), [](const PackItem& a, const PackItem& b)
|
||||
{
|
||||
int areaA = a.width * a.height;
|
||||
int areaB = b.width * b.height;
|
||||
if (areaA != areaB) return areaA > areaB;
|
||||
return a.regionID < b.regionID;
|
||||
});
|
||||
|
||||
int packedWidth{};
|
||||
int packedHeight{};
|
||||
std::unordered_map<int, RectI> packedRects{};
|
||||
if (!pack_regions(items, packedWidth, packedHeight, packedRects)) return false;
|
||||
if (packedWidth <= 0 || packedHeight <= 0) return false;
|
||||
|
||||
auto textureSize = spritesheet.texture.size;
|
||||
auto& sourcePixels = spritesheet.texture.pixels;
|
||||
std::vector<uint8_t> packedPixels((size_t)packedWidth * packedHeight * resource::texture::CHANNELS, 0);
|
||||
|
||||
for (auto& item : items)
|
||||
{
|
||||
if (!packedRects.contains(item.regionID)) continue;
|
||||
auto destinationRect = packedRects.at(item.regionID);
|
||||
|
||||
for (int y = 0; y < item.height; y++)
|
||||
{
|
||||
for (int x = 0; x < item.width; x++)
|
||||
{
|
||||
int sourceX = item.srcX + x;
|
||||
int sourceY = item.srcY + y;
|
||||
int destinationX = destinationRect.x + packingPadding + x;
|
||||
int destinationY = destinationRect.y + packingPadding + y;
|
||||
|
||||
if (sourceX < 0 || sourceY < 0 || sourceX >= textureSize.x || sourceY >= textureSize.y) continue;
|
||||
if (destinationX < 0 || destinationY < 0 || destinationX >= packedWidth || destinationY >= packedHeight)
|
||||
continue;
|
||||
|
||||
auto sourceIndex = ((size_t)sourceY * textureSize.x + sourceX) * resource::texture::CHANNELS;
|
||||
auto destinationIndex =
|
||||
((size_t)destinationY * packedWidth + destinationX) * resource::texture::CHANNELS;
|
||||
std::copy_n(sourcePixels.data() + sourceIndex, resource::texture::CHANNELS,
|
||||
packedPixels.data() + destinationIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spritesheet.texture = resource::Texture(packedPixels.data(), {packedWidth, packedHeight});
|
||||
|
||||
for (auto& [regionID, region] : spritesheet.regions)
|
||||
if (packedRects.contains(regionID))
|
||||
{
|
||||
auto& rect = packedRects.at(regionID);
|
||||
region.crop = {rect.x + packingPadding, rect.y + packingPadding};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Anm2::regions_trim(int spritesheetID, const std::set<int>& ids)
|
||||
{
|
||||
auto spritesheet = spritesheet_get(spritesheetID);
|
||||
if (!spritesheet || !spritesheet->texture.is_valid() || spritesheet->texture.pixels.empty() || ids.empty())
|
||||
return false;
|
||||
|
||||
auto& texture = spritesheet->texture;
|
||||
bool changed{};
|
||||
|
||||
for (auto id : ids)
|
||||
{
|
||||
if (!spritesheet->regions.contains(id)) continue;
|
||||
auto& region = spritesheet->regions.at(id);
|
||||
|
||||
auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size));
|
||||
auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size));
|
||||
|
||||
int minX = std::max(0, minPoint.x);
|
||||
int minY = std::max(0, minPoint.y);
|
||||
int maxX = std::min(texture.size.x, maxPoint.x);
|
||||
int maxY = std::min(texture.size.y, maxPoint.y);
|
||||
|
||||
if (minX >= maxX || minY >= maxY) continue;
|
||||
|
||||
int contentMinX = std::numeric_limits<int>::max();
|
||||
int contentMinY = std::numeric_limits<int>::max();
|
||||
int contentMaxX = std::numeric_limits<int>::min();
|
||||
int contentMaxY = std::numeric_limits<int>::min();
|
||||
|
||||
for (int y = minY; y < maxY; y++)
|
||||
{
|
||||
for (int x = minX; x < maxX; x++)
|
||||
{
|
||||
auto index = ((size_t)y * texture.size.x + x) * resource::texture::CHANNELS;
|
||||
if (index + resource::texture::CHANNELS > texture.pixels.size()) continue;
|
||||
|
||||
auto r = texture.pixels[index + 0];
|
||||
auto g = texture.pixels[index + 1];
|
||||
auto b = texture.pixels[index + 2];
|
||||
auto a = texture.pixels[index + 3];
|
||||
if (r == 0 && g == 0 && b == 0 && a == 0) continue;
|
||||
|
||||
contentMinX = std::min(contentMinX, x);
|
||||
contentMinY = std::min(contentMinY, y);
|
||||
contentMaxX = std::max(contentMaxX, x);
|
||||
contentMaxY = std::max(contentMaxY, y);
|
||||
}
|
||||
}
|
||||
|
||||
if (contentMinX == std::numeric_limits<int>::max()) continue;
|
||||
|
||||
auto newCrop = glm::vec2(contentMinX, contentMinY);
|
||||
auto newSize = glm::vec2(contentMaxX - contentMinX + 1, contentMaxY - contentMinY + 1);
|
||||
if (region.crop != newCrop || region.size != newSize)
|
||||
{
|
||||
auto previousCrop = region.crop;
|
||||
region.crop = newCrop;
|
||||
region.size = newSize;
|
||||
if (region.origin == Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = {static_cast<int>(region.size.x / 2.0f), static_cast<int>(region.size.y / 2.0f)};
|
||||
else
|
||||
// Preserve the same texture-space pivot location when trimming shifts region crop.
|
||||
region.pivot -= (region.crop - previousCrop);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
std::set<int> Anm2::spritesheets_unused()
|
||||
{
|
||||
std::set<int> used{};
|
||||
@@ -35,20 +403,270 @@ namespace anm2ed::anm2
|
||||
return unused;
|
||||
}
|
||||
|
||||
bool Anm2::spritesheets_merge(const std::set<int>& ids, SpritesheetMergeOrigin mergeOrigin, bool isMakeRegions,
|
||||
bool isMakePrimaryRegion, origin::Type regionOrigin)
|
||||
{
|
||||
if (ids.size() < 2) return false;
|
||||
|
||||
auto baseId = *ids.begin();
|
||||
if (!content.spritesheets.contains(baseId)) return false;
|
||||
for (auto id : ids)
|
||||
if (!content.spritesheets.contains(id)) return false;
|
||||
|
||||
auto& base = content.spritesheets.at(baseId);
|
||||
if (!base.texture.is_valid()) return false;
|
||||
|
||||
std::unordered_map<int, glm::ivec2> offsets{};
|
||||
offsets[baseId] = {};
|
||||
|
||||
auto baseTextureSize = base.texture.size;
|
||||
auto mergedTexture = base.texture;
|
||||
for (auto id : ids)
|
||||
{
|
||||
if (id == baseId) continue;
|
||||
|
||||
auto& spritesheet = content.spritesheets.at(id);
|
||||
if (!spritesheet.texture.is_valid()) return false;
|
||||
|
||||
offsets[id] = mergeOrigin == APPEND_RIGHT ? glm::ivec2(mergedTexture.size.x, 0)
|
||||
: glm::ivec2(0, mergedTexture.size.y);
|
||||
mergedTexture = resource::Texture::merge_append(mergedTexture, spritesheet.texture,
|
||||
mergeOrigin == APPEND_RIGHT);
|
||||
}
|
||||
base.texture = std::move(mergedTexture);
|
||||
|
||||
std::unordered_map<int, std::unordered_map<int, int>> regionIdMap{};
|
||||
|
||||
if (isMakeRegions)
|
||||
{
|
||||
if (base.regionOrder.size() != base.regions.size())
|
||||
{
|
||||
base.regionOrder.clear();
|
||||
base.regionOrder.reserve(base.regions.size());
|
||||
for (auto id : base.regions | std::views::keys)
|
||||
base.regionOrder.push_back(id);
|
||||
}
|
||||
|
||||
if (isMakePrimaryRegion)
|
||||
{
|
||||
auto baseLocationRegionID = map::next_id_get(base.regions);
|
||||
auto baseFilename = path::to_utf8(base.path.stem());
|
||||
auto baseLocationRegionName = baseFilename.empty() ? std::format("#{}", baseId) : baseFilename;
|
||||
auto baseLocationRegionPivot =
|
||||
regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(baseTextureSize) * 0.5f : glm::vec2();
|
||||
base.regions[baseLocationRegionID] = {
|
||||
.name = baseLocationRegionName,
|
||||
.crop = {},
|
||||
.pivot = glm::ivec2(baseLocationRegionPivot),
|
||||
.size = baseTextureSize,
|
||||
.origin = regionOrigin,
|
||||
};
|
||||
base.regionOrder.push_back(baseLocationRegionID);
|
||||
}
|
||||
|
||||
for (auto id : ids)
|
||||
{
|
||||
if (id == baseId) continue;
|
||||
|
||||
auto& source = content.spritesheets.at(id);
|
||||
auto sheetOffset = offsets.at(id);
|
||||
|
||||
auto locationRegionID = map::next_id_get(base.regions);
|
||||
auto sourceFilename = path::to_utf8(source.path.stem());
|
||||
auto locationRegionName = sourceFilename.empty() ? std::format("#{}", id) : sourceFilename;
|
||||
auto locationRegionPivot =
|
||||
regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(source.texture.size) * 0.5f : glm::vec2();
|
||||
base.regions[locationRegionID] = {
|
||||
.name = locationRegionName,
|
||||
.crop = sheetOffset,
|
||||
.pivot = glm::ivec2(locationRegionPivot),
|
||||
.size = source.texture.size,
|
||||
.origin = regionOrigin,
|
||||
};
|
||||
base.regionOrder.push_back(locationRegionID);
|
||||
|
||||
for (auto& [sourceRegionID, sourceRegion] : source.regions)
|
||||
{
|
||||
auto destinationRegionID = map::next_id_get(base.regions);
|
||||
auto destinationRegion = sourceRegion;
|
||||
destinationRegion.crop += sheetOffset;
|
||||
base.regions[destinationRegionID] = destinationRegion;
|
||||
base.regionOrder.push_back(destinationRegionID);
|
||||
regionIdMap[id][sourceRegionID] = destinationRegionID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<int, int> layerSpritesheetBefore{};
|
||||
for (auto& [layerID, layer] : content.layers)
|
||||
{
|
||||
if (!ids.contains(layer.spritesheetID)) continue;
|
||||
layerSpritesheetBefore[layerID] = layer.spritesheetID;
|
||||
layer.spritesheetID = baseId;
|
||||
}
|
||||
|
||||
for (auto& animation : animations.items)
|
||||
{
|
||||
for (auto& [layerID, item] : animation.layerAnimations)
|
||||
{
|
||||
if (!layerSpritesheetBefore.contains(layerID)) continue;
|
||||
auto sourceSpritesheetID = layerSpritesheetBefore.at(layerID);
|
||||
if (sourceSpritesheetID == baseId) continue;
|
||||
|
||||
for (auto& frame : item.frames)
|
||||
{
|
||||
if (frame.regionID == -1) continue;
|
||||
|
||||
if (isMakeRegions && regionIdMap.contains(sourceSpritesheetID) &&
|
||||
regionIdMap.at(sourceSpritesheetID).contains(frame.regionID))
|
||||
frame.regionID = regionIdMap.at(sourceSpritesheetID).at(frame.regionID);
|
||||
else
|
||||
frame.regionID = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto id : ids)
|
||||
if (id != baseId) content.spritesheets.erase(id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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)));
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
labels.emplace_back(std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
bool Anm2::spritesheets_deserialize(const std::string& string, const std::string& directory, merge::Type type,
|
||||
std::string* errorString)
|
||||
std::vector<int> Anm2::spritesheet_ids_get()
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
for (auto& [id, spritesheet] : content.spritesheets)
|
||||
ids.emplace_back(id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::vector<std::string> Anm2::region_labels_get(Spritesheet& spritesheet)
|
||||
{
|
||||
auto rebuild_order = [&]()
|
||||
{
|
||||
spritesheet.regionOrder.clear();
|
||||
spritesheet.regionOrder.reserve(spritesheet.regions.size());
|
||||
for (auto id : spritesheet.regions | std::views::keys)
|
||||
spritesheet.regionOrder.push_back(id);
|
||||
};
|
||||
if (spritesheet.regionOrder.size() != spritesheet.regions.size())
|
||||
rebuild_order();
|
||||
else
|
||||
{
|
||||
bool isOrderValid = true;
|
||||
for (auto id : spritesheet.regionOrder)
|
||||
if (!spritesheet.regions.contains(id))
|
||||
{
|
||||
isOrderValid = false;
|
||||
break;
|
||||
}
|
||||
if (!isOrderValid) rebuild_order();
|
||||
}
|
||||
|
||||
std::vector<std::string> labels{};
|
||||
labels.emplace_back(localize.get(BASIC_NONE));
|
||||
for (auto id : spritesheet.regionOrder)
|
||||
labels.emplace_back(spritesheet.regions.at(id).name);
|
||||
return labels;
|
||||
}
|
||||
|
||||
std::vector<int> Anm2::region_ids_get(Spritesheet& spritesheet)
|
||||
{
|
||||
auto rebuild_order = [&]()
|
||||
{
|
||||
spritesheet.regionOrder.clear();
|
||||
spritesheet.regionOrder.reserve(spritesheet.regions.size());
|
||||
for (auto id : spritesheet.regions | std::views::keys)
|
||||
spritesheet.regionOrder.push_back(id);
|
||||
};
|
||||
if (spritesheet.regionOrder.size() != spritesheet.regions.size())
|
||||
rebuild_order();
|
||||
else
|
||||
{
|
||||
bool isOrderValid = true;
|
||||
for (auto id : spritesheet.regionOrder)
|
||||
if (!spritesheet.regions.contains(id))
|
||||
{
|
||||
isOrderValid = false;
|
||||
break;
|
||||
}
|
||||
if (!isOrderValid) rebuild_order();
|
||||
}
|
||||
|
||||
std::vector<int> ids{};
|
||||
ids.emplace_back(-1);
|
||||
for (auto id : spritesheet.regionOrder)
|
||||
ids.emplace_back(id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::set<int> Anm2::regions_unused(Spritesheet& spritesheet)
|
||||
{
|
||||
std::set<int> used{};
|
||||
|
||||
for (auto& animation : animations.items)
|
||||
{
|
||||
for (auto& layerAnimation : animation.layerAnimations | std::views::values)
|
||||
{
|
||||
for (auto& frame : layerAnimation.frames)
|
||||
if (frame.regionID != -1) used.insert(frame.regionID);
|
||||
}
|
||||
}
|
||||
|
||||
std::set<int> unused{};
|
||||
for (auto& id : spritesheet.regions | std::views::keys)
|
||||
if (!used.contains(id)) unused.insert(id);
|
||||
|
||||
return unused;
|
||||
}
|
||||
|
||||
void Anm2::scan_and_set_regions()
|
||||
{
|
||||
for (auto& animation : animations.items)
|
||||
{
|
||||
for (auto& [layerID, item] : animation.layerAnimations)
|
||||
{
|
||||
auto layer = map::find(content.layers, layerID);
|
||||
if (!layer) continue;
|
||||
|
||||
auto spritesheet = spritesheet_get(layer->spritesheetID);
|
||||
if (!spritesheet || spritesheet->regions.empty()) continue;
|
||||
|
||||
for (auto& frame : item.frames)
|
||||
{
|
||||
if (frame.regionID != -1) continue;
|
||||
|
||||
auto frameCrop = glm::ivec2(frame.crop);
|
||||
auto frameSize = glm::ivec2(frame.size);
|
||||
auto framePivot = glm::ivec2(frame.pivot);
|
||||
|
||||
for (auto& [regionID, region] : spritesheet->regions)
|
||||
{
|
||||
if (glm::ivec2(region.crop) == frameCrop && glm::ivec2(region.size) == frameSize &&
|
||||
glm::ivec2(region.pivot) == framePivot)
|
||||
{
|
||||
frame.regionID = regionID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Anm2::spritesheets_deserialize(const std::string& string, const std::filesystem::path& directory,
|
||||
merge::Type type, std::string* errorString)
|
||||
{
|
||||
XMLDocument document{};
|
||||
|
||||
@@ -62,7 +680,7 @@ namespace anm2ed::anm2
|
||||
return false;
|
||||
}
|
||||
|
||||
filesystem::WorkingDirectory workingDirectory(directory);
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
|
||||
for (auto element = document.FirstChildElement("Spritesheet"); element;
|
||||
element = element->NextSiblingElement("Spritesheet"))
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "icon.h"
|
||||
#include "strings.h"
|
||||
#include "icon.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
#include <glm/glm/vec2.hpp>
|
||||
#include <glm/glm/vec3.hpp>
|
||||
#include <glm/glm/vec4.hpp>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
@@ -86,4 +87,35 @@ namespace anm2ed::anm2
|
||||
MULTIPLY,
|
||||
DIVIDE
|
||||
};
|
||||
}
|
||||
|
||||
enum Compatibility
|
||||
{
|
||||
ISAAC,
|
||||
ANM2ED,
|
||||
ANM2ED_LIMITED,
|
||||
COUNT
|
||||
};
|
||||
|
||||
enum SpritesheetMergeOrigin
|
||||
{
|
||||
APPEND_RIGHT,
|
||||
APPEND_BOTTOM
|
||||
};
|
||||
|
||||
enum Flag
|
||||
{
|
||||
NO_SOUNDS = 1 << 0,
|
||||
NO_REGIONS = 1 << 1,
|
||||
FRAME_NO_REGION_VALUES = 1 << 2,
|
||||
INTERPOLATION_BOOL_ONLY = 1 << 3
|
||||
};
|
||||
|
||||
typedef int Flags;
|
||||
|
||||
inline bool has_flag(Flags flags, Flag flag) { return (flags & flag) != 0; }
|
||||
|
||||
inline const std::unordered_map<Compatibility, Flags> COMPATIBILITY_FLAGS = {
|
||||
{ISAAC, NO_SOUNDS | NO_REGIONS | FRAME_NO_REGION_VALUES | INTERPOLATION_BOOL_ONLY},
|
||||
{ANM2ED, 0},
|
||||
{ANM2ED_LIMITED, FRAME_NO_REGION_VALUES}};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "content.h"
|
||||
#include "content.hpp"
|
||||
|
||||
using namespace tinyxml2;
|
||||
|
||||
@@ -29,13 +29,13 @@ namespace anm2ed::anm2
|
||||
sounds.emplace(id, Sound(child, id));
|
||||
}
|
||||
|
||||
void Content::serialize(XMLDocument& document, XMLElement* parent)
|
||||
void Content::serialize(XMLDocument& document, XMLElement* parent, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement("Content");
|
||||
|
||||
auto spritesheetsElement = document.NewElement("Spritesheets");
|
||||
for (auto& [id, spritesheet] : spritesheets)
|
||||
spritesheet.serialize(document, spritesheetsElement, id);
|
||||
spritesheet.serialize(document, spritesheetsElement, id, flags);
|
||||
element->InsertEndChild(spritesheetsElement);
|
||||
|
||||
auto layersElement = document.NewElement("Layers");
|
||||
@@ -53,7 +53,7 @@ namespace anm2ed::anm2
|
||||
event.serialize(document, eventsElement, id);
|
||||
element->InsertEndChild(eventsElement);
|
||||
|
||||
if (!sounds.empty())
|
||||
if (!has_flag(flags, NO_SOUNDS) && !sounds.empty())
|
||||
{
|
||||
auto soundsElement = document.NewElement("Sounds");
|
||||
for (auto& [id, sound] : sounds)
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "event.h"
|
||||
#include "layer.h"
|
||||
#include "null.h"
|
||||
#include "sound.h"
|
||||
#include "spritesheet.h"
|
||||
#include "event.hpp"
|
||||
#include "layer.hpp"
|
||||
#include "null.hpp"
|
||||
#include "sound.hpp"
|
||||
#include "spritesheet.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
@@ -20,7 +20,7 @@ namespace anm2ed::anm2
|
||||
|
||||
Content() = default;
|
||||
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0);
|
||||
Content(tinyxml2::XMLElement*);
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
#include "event.h"
|
||||
#include "event.hpp"
|
||||
|
||||
#include "xml_.h"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace anm2ed::anm2
|
||||
{
|
||||
public:
|
||||
std::string name{};
|
||||
int soundID{-1};
|
||||
|
||||
Event() = default;
|
||||
Event(tinyxml2::XMLElement*, int&);
|
||||
+137
-45
@@ -1,51 +1,118 @@
|
||||
#include "frame.h"
|
||||
#include "frame.hpp"
|
||||
|
||||
#include "math_.h"
|
||||
#include "xml_.h"
|
||||
#include <cstring>
|
||||
|
||||
#include "math_.hpp"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
Frame::Interpolation interpolation_from_xml(const char* value, bool fallback)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
if (std::strcmp(value, "EaseIn") == 0) return Frame::Interpolation::EASE_IN;
|
||||
if (std::strcmp(value, "EaseOut") == 0) return Frame::Interpolation::EASE_OUT;
|
||||
if (std::strcmp(value, "EaseInOut") == 0) return Frame::Interpolation::EASE_IN_OUT;
|
||||
}
|
||||
return fallback ? Frame::Interpolation::LINEAR : Frame::Interpolation::NONE;
|
||||
}
|
||||
|
||||
const char* interpolation_to_xml(Frame::Interpolation interpolation)
|
||||
{
|
||||
switch (interpolation)
|
||||
{
|
||||
case Frame::Interpolation::EASE_IN:
|
||||
return "EaseIn";
|
||||
case Frame::Interpolation::EASE_OUT:
|
||||
return "EaseOut";
|
||||
case Frame::Interpolation::EASE_IN_OUT:
|
||||
return "EaseInOut";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Frame::Frame(XMLElement* element, Type type)
|
||||
{
|
||||
if (type != TRIGGER)
|
||||
bool isInterpolatedBool{};
|
||||
const char* interpolationValue = element->Attribute("Interpolated");
|
||||
|
||||
switch (type)
|
||||
{
|
||||
element->QueryFloatAttribute("XPosition", &position.x);
|
||||
element->QueryFloatAttribute("YPosition", &position.y);
|
||||
if (type == LAYER)
|
||||
{
|
||||
case ROOT:
|
||||
case NULL_:
|
||||
element->QueryFloatAttribute("XPosition", &position.x);
|
||||
element->QueryFloatAttribute("YPosition", &position.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", &isInterpolatedBool);
|
||||
if (interpolationValue) interpolation = interpolation_from_xml(interpolationValue, isInterpolatedBool);
|
||||
break;
|
||||
case LAYER:
|
||||
element->QueryIntAttribute("RegionId", ®ionID);
|
||||
element->QueryFloatAttribute("XPosition", &position.x);
|
||||
element->QueryFloatAttribute("YPosition", &position.y);
|
||||
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", &isInterpolatedBool);
|
||||
if (interpolationValue) interpolation = interpolation_from_xml(interpolationValue, isInterpolatedBool);
|
||||
break;
|
||||
case TRIGGER:
|
||||
{
|
||||
element->QueryIntAttribute("EventId", &eventID);
|
||||
|
||||
int soundID{};
|
||||
// Backwards compatibility with old formats
|
||||
if (element->QueryIntAttribute("SoundId", &soundID) == XML_SUCCESS) soundIDs.push_back(soundID);
|
||||
|
||||
for (auto child = element->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
|
||||
{
|
||||
child->QueryIntAttribute("Id", &soundID);
|
||||
soundIDs.push_back(soundID);
|
||||
}
|
||||
|
||||
element->QueryIntAttribute("AtFrame", &atFrame);
|
||||
break;
|
||||
}
|
||||
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);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XMLElement* Frame::to_element(XMLDocument& document, Type type)
|
||||
XMLElement* Frame::to_element(XMLDocument& document, Type type, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement(type == TRIGGER ? "Trigger" : "Frame");
|
||||
|
||||
@@ -67,17 +134,31 @@ namespace anm2ed::anm2
|
||||
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);
|
||||
if (has_flag(flags, INTERPOLATION_BOOL_ONLY) || interpolation == Interpolation::NONE ||
|
||||
interpolation == Interpolation::LINEAR)
|
||||
element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR);
|
||||
else if (const char* interpolationValue = interpolation_to_xml(interpolation))
|
||||
element->SetAttribute("Interpolated", interpolationValue);
|
||||
break;
|
||||
case LAYER:
|
||||
{
|
||||
bool noRegions = has_flag(flags, NO_REGIONS);
|
||||
bool frameNoRegionValues = has_flag(flags, FRAME_NO_REGION_VALUES);
|
||||
bool hasValidRegion = !noRegions && regionID != -1;
|
||||
bool writeRegionValues = !frameNoRegionValues || !hasValidRegion;
|
||||
|
||||
if (hasValidRegion) element->SetAttribute("RegionId", regionID);
|
||||
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);
|
||||
if (writeRegionValues)
|
||||
{
|
||||
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);
|
||||
@@ -90,11 +171,24 @@ namespace anm2ed::anm2
|
||||
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);
|
||||
if (has_flag(flags, INTERPOLATION_BOOL_ONLY) || interpolation == Interpolation::NONE ||
|
||||
interpolation == Interpolation::LINEAR)
|
||||
element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR);
|
||||
else if (const char* interpolationValue = interpolation_to_xml(interpolation))
|
||||
element->SetAttribute("Interpolated", interpolationValue);
|
||||
break;
|
||||
}
|
||||
case TRIGGER:
|
||||
element->SetAttribute("EventId", eventID);
|
||||
element->SetAttribute("SoundId", soundID);
|
||||
if (eventID != -1) element->SetAttribute("EventId", eventID);
|
||||
|
||||
if (!has_flag(flags, NO_SOUNDS))
|
||||
for (auto& id : soundIDs)
|
||||
{
|
||||
if (id == -1) continue;
|
||||
auto soundChild = element->InsertNewChildElement("Sound");
|
||||
soundChild->SetAttribute("Id", id);
|
||||
}
|
||||
|
||||
element->SetAttribute("AtFrame", atFrame);
|
||||
break;
|
||||
default:
|
||||
@@ -104,20 +198,18 @@ namespace anm2ed::anm2
|
||||
return element;
|
||||
}
|
||||
|
||||
void Frame::serialize(XMLDocument& document, XMLElement* parent, Type type)
|
||||
void Frame::serialize(XMLDocument& document, XMLElement* parent, Type type, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, type));
|
||||
parent->InsertEndChild(to_element(document, type, flags));
|
||||
}
|
||||
|
||||
std::string Frame::to_string(Type type)
|
||||
std::string Frame::to_string(Type type, Flags flags)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document, type));
|
||||
document.InsertEndChild(to_element(document, type, flags));
|
||||
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); }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
#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
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
#include "anm2_type.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
constexpr auto FRAME_DURATION_MIN = 1;
|
||||
constexpr auto FRAME_DURATION_MAX = 1000000;
|
||||
|
||||
class Frame
|
||||
{
|
||||
public:
|
||||
enum Interpolation
|
||||
{
|
||||
NONE,
|
||||
LINEAR,
|
||||
EASE_IN,
|
||||
EASE_OUT,
|
||||
EASE_IN_OUT
|
||||
};
|
||||
|
||||
bool isVisible{true};
|
||||
Interpolation interpolation{NONE};
|
||||
float rotation{};
|
||||
int duration{FRAME_DURATION_MIN};
|
||||
int atFrame{-1};
|
||||
int eventID{-1};
|
||||
int regionID{-1};
|
||||
std::vector<int> soundIDs{};
|
||||
glm::vec2 pivot{};
|
||||
glm::vec2 crop{};
|
||||
glm::vec2 position{};
|
||||
glm::vec2 size{};
|
||||
glm::vec2 scale{100, 100};
|
||||
glm::vec3 colorOffset{};
|
||||
glm::vec4 tint{types::color::WHITE};
|
||||
|
||||
Frame() = default;
|
||||
Frame(tinyxml2::XMLElement*, Type);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, Flags = 0);
|
||||
std::string to_string(Type type, Flags = 0);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, Flags = 0);
|
||||
void shorten();
|
||||
void extend();
|
||||
};
|
||||
|
||||
struct FrameChange
|
||||
{
|
||||
std::optional<bool> isVisible{};
|
||||
std::optional<Frame::Interpolation> interpolation{};
|
||||
std::optional<float> rotation{};
|
||||
std::optional<int> duration{};
|
||||
std::optional<int> regionID{};
|
||||
std::optional<float> pivotX{};
|
||||
std::optional<float> pivotY{};
|
||||
std::optional<float> cropX{};
|
||||
std::optional<float> cropY{};
|
||||
std::optional<float> positionX{};
|
||||
std::optional<float> positionY{};
|
||||
std::optional<float> sizeX{};
|
||||
std::optional<float> sizeY{};
|
||||
std::optional<float> scaleX{};
|
||||
std::optional<float> scaleY{};
|
||||
std::optional<float> colorOffsetR{};
|
||||
std::optional<float> colorOffsetG{};
|
||||
std::optional<float> colorOffsetB{};
|
||||
std::optional<float> tintR{};
|
||||
std::optional<float> tintG{};
|
||||
std::optional<float> tintB{};
|
||||
std::optional<float> tintA{};
|
||||
std::optional<bool> isFlipX{};
|
||||
std::optional<bool> isFlipY{};
|
||||
};
|
||||
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
#include "info.h"
|
||||
#include "info.hpp"
|
||||
|
||||
#include "xml_.h"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
+104
-77
@@ -1,9 +1,10 @@
|
||||
#include "item.h"
|
||||
#include "item.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ranges>
|
||||
|
||||
#include "vector_.h"
|
||||
#include "xml_.h"
|
||||
#include "vector_.hpp"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
@@ -11,6 +12,29 @@ using namespace glm;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
float interpolation_factor(Frame::Interpolation interpolation, float value)
|
||||
{
|
||||
value = glm::clamp(value, 0.0f, 1.0f);
|
||||
|
||||
switch (interpolation)
|
||||
{
|
||||
case Frame::Interpolation::LINEAR:
|
||||
return value;
|
||||
case Frame::Interpolation::EASE_IN:
|
||||
return value * value;
|
||||
case Frame::Interpolation::EASE_OUT:
|
||||
return 1.0f - ((1.0f - value) * (1.0f - value));
|
||||
case Frame::Interpolation::EASE_IN_OUT:
|
||||
return value < 0.5f ? (2.0f * value * value) : (1.0f - std::pow(-2.0f * value + 2.0f, 2.0f) * 0.5f);
|
||||
case Frame::Interpolation::NONE:
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item::Item(XMLElement* element, Type type, int* id)
|
||||
{
|
||||
if (type == LAYER && id) element->QueryIntAttribute("LayerId", id);
|
||||
@@ -23,7 +47,7 @@ namespace anm2ed::anm2
|
||||
frames.push_back(Frame(child, type));
|
||||
}
|
||||
|
||||
XMLElement* Item::to_element(XMLDocument& document, Type type, int id)
|
||||
XMLElement* Item::to_element(XMLDocument& document, Type type, int id, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement(TYPE_ITEM_STRINGS[type]);
|
||||
|
||||
@@ -34,20 +58,20 @@ namespace anm2ed::anm2
|
||||
if (type == TRIGGER) frames_sort_by_at_frame();
|
||||
|
||||
for (auto& frame : frames)
|
||||
frame.serialize(document, element, type);
|
||||
frame.serialize(document, element, type, flags);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
void Item::serialize(XMLDocument& document, XMLElement* parent, Type type, int id)
|
||||
void Item::serialize(XMLDocument& document, XMLElement* parent, Type type, int id, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, type, id));
|
||||
parent->InsertEndChild(to_element(document, type, id, flags));
|
||||
}
|
||||
|
||||
std::string Item::to_string(Type type, int id)
|
||||
std::string Item::to_string(Type type, int id, Flags flags)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document, type, id));
|
||||
document.InsertEndChild(to_element(document, type, id, flags));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
@@ -112,9 +136,10 @@ namespace anm2ed::anm2
|
||||
}
|
||||
}
|
||||
|
||||
if (type != TRIGGER && frame.isInterpolated && frameNext && frame.duration > 1)
|
||||
if (type != TRIGGER && frame.interpolation != Frame::Interpolation::NONE && frameNext && frame.duration > 1)
|
||||
{
|
||||
auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
|
||||
auto interpolation =
|
||||
interpolation_factor(frame.interpolation, (time - durationCurrent) / (durationNext - durationCurrent));
|
||||
|
||||
frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation);
|
||||
frame.position = glm::mix(frame.position, frameNext->position, interpolation);
|
||||
@@ -126,82 +151,84 @@ namespace anm2ed::anm2
|
||||
return frame;
|
||||
}
|
||||
|
||||
void Item::frames_change(FrameChange& change, ChangeType type, int start, int numberFrames)
|
||||
void Item::frames_change(FrameChange change, anm2::Type itemType, ChangeType changeType, std::set<int>& selection)
|
||||
{
|
||||
auto useStart = numberFrames > -1 ? start : 0;
|
||||
auto end = numberFrames > -1 ? start + numberFrames : (int)frames.size();
|
||||
end = glm::clamp(end, start, (int)frames.size());
|
||||
const auto clamp_identity = [](auto value) { return value; };
|
||||
const auto clamp01 = [](auto value) { return glm::clamp(value, 0.0f, 1.0f); };
|
||||
const auto clamp_duration = [](int value) { return std::max(FRAME_DURATION_MIN, value); };
|
||||
|
||||
for (int i = useStart; i < end; i++)
|
||||
if (selection.empty()) return;
|
||||
|
||||
auto apply_scalar_with_clamp = [&](auto& target, const auto& optionalValue, auto clampFunc)
|
||||
{
|
||||
if (!optionalValue) return;
|
||||
auto value = *optionalValue;
|
||||
|
||||
switch (changeType)
|
||||
{
|
||||
case ADJUST:
|
||||
target = clampFunc(value);
|
||||
break;
|
||||
case ADD:
|
||||
target = clampFunc(target + value);
|
||||
break;
|
||||
case SUBTRACT:
|
||||
target = clampFunc(target - value);
|
||||
break;
|
||||
case MULTIPLY:
|
||||
target = clampFunc(target * value);
|
||||
break;
|
||||
case DIVIDE:
|
||||
if (value == decltype(value){}) return;
|
||||
target = clampFunc(target / value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
auto apply_scalar = [&](auto& target, const auto& optionalValue)
|
||||
{ apply_scalar_with_clamp(target, optionalValue, clamp_identity); };
|
||||
|
||||
for (auto i : selection)
|
||||
{
|
||||
if (!vector::in_bounds(frames, i)) continue;
|
||||
Frame& frame = frames[i];
|
||||
|
||||
if (change.isVisible) frame.isVisible = *change.isVisible;
|
||||
if (change.isInterpolated) frame.isInterpolated = *change.isInterpolated;
|
||||
if (change.interpolation) frame.interpolation = *change.interpolation;
|
||||
if (change.isFlipX) frame.scale.x = -frame.scale.x;
|
||||
if (change.isFlipY) frame.scale.y = -frame.scale.y;
|
||||
|
||||
switch (type)
|
||||
apply_scalar(frame.rotation, change.rotation);
|
||||
apply_scalar_with_clamp(frame.duration, change.duration, clamp_duration);
|
||||
|
||||
if (itemType == LAYER)
|
||||
{
|
||||
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;
|
||||
apply_scalar(frame.crop.x, change.cropX);
|
||||
apply_scalar(frame.crop.y, change.cropY);
|
||||
|
||||
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;
|
||||
apply_scalar(frame.pivot.x, change.pivotX);
|
||||
apply_scalar(frame.pivot.y, change.pivotY);
|
||||
|
||||
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;
|
||||
apply_scalar(frame.size.x, change.sizeX);
|
||||
apply_scalar(frame.size.y, change.sizeY);
|
||||
|
||||
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;
|
||||
if (change.regionID) frame.regionID = *change.regionID;
|
||||
}
|
||||
|
||||
apply_scalar(frame.position.x, change.positionX);
|
||||
apply_scalar(frame.position.y, change.positionY);
|
||||
|
||||
apply_scalar(frame.scale.x, change.scaleX);
|
||||
apply_scalar(frame.scale.y, change.scaleY);
|
||||
|
||||
apply_scalar_with_clamp(frame.colorOffset.x, change.colorOffsetR, clamp01);
|
||||
apply_scalar_with_clamp(frame.colorOffset.y, change.colorOffsetG, clamp01);
|
||||
apply_scalar_with_clamp(frame.colorOffset.z, change.colorOffsetB, clamp01);
|
||||
|
||||
apply_scalar_with_clamp(frame.tint.x, change.tintR, clamp01);
|
||||
apply_scalar_with_clamp(frame.tint.y, change.tintG, clamp01);
|
||||
apply_scalar_with_clamp(frame.tint.z, change.tintB, clamp01);
|
||||
apply_scalar_with_clamp(frame.tint.w, change.tintA, clamp01);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,9 +305,9 @@ namespace anm2ed::anm2
|
||||
while (duration < original.duration)
|
||||
{
|
||||
Frame baked = original;
|
||||
float interpolation = (float)duration / original.duration;
|
||||
float interpolation = interpolation_factor(original.interpolation, (float)duration / original.duration);
|
||||
baked.duration = std::min(interval, original.duration - duration);
|
||||
baked.isInterpolated = (i == index) ? original.isInterpolated : false;
|
||||
baked.interpolation = Frame::Interpolation::NONE;
|
||||
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);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "frame.h"
|
||||
#include "frame.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
@@ -15,12 +15,12 @@ namespace anm2ed::anm2
|
||||
|
||||
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);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, int, Flags = 0);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1, Flags = 0);
|
||||
std::string to_string(Type, int = -1, Flags = 0);
|
||||
int length(Type);
|
||||
Frame frame_generate(float, Type);
|
||||
void frames_change(FrameChange&, ChangeType, int, int = 0);
|
||||
void frames_change(FrameChange, anm2::Type, ChangeType, std::set<int>&);
|
||||
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);
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
#include "layer.h"
|
||||
#include "layer.hpp"
|
||||
|
||||
#include "xml_.h"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
#include "null.h"
|
||||
#include "null.hpp"
|
||||
|
||||
#include "xml_.h"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
+12
-30
@@ -1,7 +1,8 @@
|
||||
#include "sound.h"
|
||||
#include "sound.hpp"
|
||||
|
||||
#include "filesystem_.h"
|
||||
#include "xml_.h"
|
||||
#include "path_.hpp"
|
||||
#include "working_directory.hpp"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
@@ -9,41 +10,24 @@ using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Sound::Sound(const Sound& other) : path(other.path) { audio = path.empty() ? Audio() : Audio(path); }
|
||||
Sound::Sound(const Sound& other) : path(other.path), audio(other.audio) {}
|
||||
|
||||
Sound& Sound::operator=(const Sound& other)
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
path = other.path;
|
||||
audio = path.empty() ? Audio() : Audio(path);
|
||||
audio = other.audio;
|
||||
}
|
||||
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())
|
||||
{
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
||||
this->path = path::lower_case_backslash_handle(this->path);
|
||||
audio = Audio(this->path);
|
||||
}
|
||||
|
||||
Sound::Sound(XMLElement* element, int& id)
|
||||
@@ -51,7 +35,7 @@ namespace anm2ed::anm2
|
||||
if (!element) return;
|
||||
element->QueryIntAttribute("Id", &id);
|
||||
xml::query_path_attribute(element, "Path", &path);
|
||||
path = filesystem::path_lower_case_backslash_handle(path);
|
||||
path = path::lower_case_backslash_handle(path);
|
||||
audio = Audio(path);
|
||||
}
|
||||
|
||||
@@ -59,7 +43,7 @@ namespace anm2ed::anm2
|
||||
{
|
||||
auto element = document.NewElement("Sound");
|
||||
element->SetAttribute("Id", id);
|
||||
auto pathString = path.generic_string();
|
||||
auto pathString = path::to_utf8(path);
|
||||
element->SetAttribute("Path", pathString.c_str());
|
||||
return element;
|
||||
}
|
||||
@@ -77,8 +61,6 @@ namespace anm2ed::anm2
|
||||
}
|
||||
|
||||
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(); }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <filesystem>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
#include "audio.h"
|
||||
#include "audio.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
@@ -20,7 +20,6 @@ namespace anm2ed::anm2
|
||||
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);
|
||||
+222
-31
@@ -1,14 +1,48 @@
|
||||
#include "spritesheet.h"
|
||||
#include "spritesheet.hpp"
|
||||
|
||||
#include "filesystem_.h"
|
||||
#include "xml_.h"
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <ranges>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "map_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "working_directory.hpp"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::types;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const char* origin_to_string(Spritesheet::Region::Origin origin)
|
||||
{
|
||||
switch (origin)
|
||||
{
|
||||
case Spritesheet::Region::TOP_LEFT:
|
||||
return "TopLeft";
|
||||
case Spritesheet::Region::ORIGIN_CENTER:
|
||||
return "Center";
|
||||
case Spritesheet::Region::CUSTOM:
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Spritesheet::Region::Origin origin_from_string(const char* originString)
|
||||
{
|
||||
if (!originString) return Spritesheet::Region::CUSTOM;
|
||||
if (std::string(originString) == "TopLeft") return Spritesheet::Region::TOP_LEFT;
|
||||
if (std::string(originString) == "Center") return Spritesheet::Region::ORIGIN_CENTER;
|
||||
return Spritesheet::Region::CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
Spritesheet::Spritesheet(XMLElement* element, int& id)
|
||||
{
|
||||
if (!element) return;
|
||||
@@ -17,47 +51,96 @@ namespace anm2ed::anm2
|
||||
// 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);
|
||||
path = path::lower_case_backslash_handle(path);
|
||||
texture = Texture(path);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
std::filesystem::path make_relative_or_keep(const std::filesystem::path& input)
|
||||
regionOrder.clear();
|
||||
for (auto child = element->FirstChildElement("Region"); child; child = child->NextSiblingElement("Region"))
|
||||
{
|
||||
if (input.empty()) return input;
|
||||
std::error_code ec{};
|
||||
auto relative = std::filesystem::relative(input, ec);
|
||||
if (!ec) return relative;
|
||||
return input;
|
||||
Region region{};
|
||||
int id{};
|
||||
child->QueryIntAttribute("Id", &id);
|
||||
xml::query_string_attribute(child, "Name", ®ion.name);
|
||||
child->QueryFloatAttribute("XCrop", ®ion.crop.x);
|
||||
child->QueryFloatAttribute("YCrop", ®ion.crop.y);
|
||||
child->QueryFloatAttribute("Width", ®ion.size.x);
|
||||
child->QueryFloatAttribute("Height", ®ion.size.y);
|
||||
region.origin = origin_from_string(child->Attribute("Origin"));
|
||||
if (region.origin == Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)};
|
||||
else
|
||||
{
|
||||
child->QueryFloatAttribute("XPivot", ®ion.pivot.x);
|
||||
child->QueryFloatAttribute("YPivot", ®ion.pivot.y);
|
||||
}
|
||||
regions.emplace(id, std::move(region));
|
||||
regionOrder.push_back(id);
|
||||
}
|
||||
|
||||
if (regionOrder.size() != regions.size())
|
||||
{
|
||||
regionOrder.clear();
|
||||
regionOrder.reserve(regions.size());
|
||||
for (auto id : regions | std::views::keys)
|
||||
regionOrder.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
auto loadPath = !path.empty() ? path::lower_case_backslash_handle(path) : this->path;
|
||||
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
||||
this->path = path::lower_case_backslash_handle(this->path);
|
||||
texture = Texture(!loadPath.empty() ? loadPath : this->path);
|
||||
}
|
||||
|
||||
XMLElement* Spritesheet::to_element(XMLDocument& document, int id)
|
||||
XMLElement* Spritesheet::to_element(XMLDocument& document, int id, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement("Spritesheet");
|
||||
element->SetAttribute("Id", id);
|
||||
auto pathString = path.generic_string();
|
||||
auto pathString = path::to_utf8(path);
|
||||
element->SetAttribute("Path", pathString.c_str());
|
||||
|
||||
if (!has_flag(flags, NO_REGIONS))
|
||||
{
|
||||
if (regionOrder.size() != regions.size())
|
||||
{
|
||||
regionOrder.clear();
|
||||
regionOrder.reserve(regions.size());
|
||||
for (auto id : regions | std::views::keys)
|
||||
regionOrder.push_back(id);
|
||||
}
|
||||
|
||||
for (auto id : regionOrder)
|
||||
{
|
||||
if (!regions.contains(id)) continue;
|
||||
auto& region = regions.at(id);
|
||||
auto regionElement = element->InsertNewChildElement("Region");
|
||||
regionElement->SetAttribute("Id", id);
|
||||
regionElement->SetAttribute("Name", region.name.c_str());
|
||||
regionElement->SetAttribute("XCrop", region.crop.x);
|
||||
regionElement->SetAttribute("YCrop", region.crop.y);
|
||||
regionElement->SetAttribute("Width", region.size.x);
|
||||
regionElement->SetAttribute("Height", region.size.y);
|
||||
if (auto originString = origin_to_string(region.origin); originString)
|
||||
regionElement->SetAttribute("Origin", originString);
|
||||
else
|
||||
{
|
||||
regionElement->SetAttribute("XPivot", region.pivot.x);
|
||||
regionElement->SetAttribute("YPivot", region.pivot.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
void Spritesheet::serialize(XMLDocument& document, XMLElement* parent, int id)
|
||||
void Spritesheet::serialize(XMLDocument& document, XMLElement* parent, int id, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, id));
|
||||
parent->InsertEndChild(to_element(document, id, flags));
|
||||
}
|
||||
|
||||
std::string Spritesheet::to_string(int id)
|
||||
@@ -67,15 +150,123 @@ namespace anm2ed::anm2
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
bool Spritesheet::save(const std::string& directory, const std::string& path)
|
||||
std::string Spritesheet::region_to_string(int id)
|
||||
{
|
||||
filesystem::WorkingDirectory workingDirectory(directory);
|
||||
this->path = !path.empty() ? make_relative_or_keep(path) : this->path;
|
||||
if (!regions.contains(id)) return {};
|
||||
|
||||
XMLDocument document{};
|
||||
auto element = document.NewElement("Region");
|
||||
auto& region = regions.at(id);
|
||||
element->SetAttribute("Id", id);
|
||||
element->SetAttribute("Name", region.name.c_str());
|
||||
element->SetAttribute("XCrop", region.crop.x);
|
||||
element->SetAttribute("YCrop", region.crop.y);
|
||||
element->SetAttribute("Width", region.size.x);
|
||||
element->SetAttribute("Height", region.size.y);
|
||||
if (auto originString = origin_to_string(region.origin); originString)
|
||||
element->SetAttribute("Origin", originString);
|
||||
else
|
||||
{
|
||||
element->SetAttribute("XPivot", region.pivot.x);
|
||||
element->SetAttribute("YPivot", region.pivot.y);
|
||||
}
|
||||
document.InsertEndChild(element);
|
||||
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
bool Spritesheet::regions_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("Region"))
|
||||
{
|
||||
if (errorString) *errorString = "No valid region(s).";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto element = document.FirstChildElement("Region"); element;
|
||||
element = element->NextSiblingElement("Region"))
|
||||
{
|
||||
Region region{};
|
||||
element->QueryIntAttribute("Id", &id);
|
||||
xml::query_string_attribute(element, "Name", ®ion.name);
|
||||
element->QueryFloatAttribute("XCrop", ®ion.crop.x);
|
||||
element->QueryFloatAttribute("YCrop", ®ion.crop.y);
|
||||
element->QueryFloatAttribute("Width", ®ion.size.x);
|
||||
element->QueryFloatAttribute("Height", ®ion.size.y);
|
||||
region.origin = origin_from_string(element->Attribute("Origin"));
|
||||
if (region.origin == Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = glm::ivec2(region.size / 2.0f);
|
||||
else
|
||||
{
|
||||
element->QueryFloatAttribute("XPivot", ®ion.pivot.x);
|
||||
element->QueryFloatAttribute("YPivot", ®ion.pivot.y);
|
||||
}
|
||||
|
||||
if (type == merge::APPEND) id = map::next_id_get(regions);
|
||||
regions[id] = std::move(region);
|
||||
if (std::find(regionOrder.begin(), regionOrder.end(), id) == regionOrder.end()) regionOrder.push_back(id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
*errorString = document.ErrorStr();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Spritesheet::save(const std::filesystem::path& directory, const std::filesystem::path& path)
|
||||
{
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
||||
if (this->path.empty()) return false;
|
||||
path::ensure_directory(this->path.parent_path());
|
||||
return texture.write_png(this->path);
|
||||
}
|
||||
|
||||
void Spritesheet::reload(const std::filesystem::path& directory) { *this = Spritesheet(directory, this->path); }
|
||||
|
||||
void Spritesheet::reload(const std::filesystem::path& directory, const std::filesystem::path& path)
|
||||
{
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
auto loadPath = !path.empty() ? path::lower_case_backslash_handle(path) : this->path;
|
||||
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
||||
this->path = path::lower_case_backslash_handle(this->path);
|
||||
texture = Texture(!loadPath.empty() ? loadPath : this->path);
|
||||
}
|
||||
bool Spritesheet::is_valid() { return texture.is_valid(); }
|
||||
|
||||
uint64_t Spritesheet::hash() const
|
||||
{
|
||||
auto hash_combine = [](std::size_t& seed, std::size_t value)
|
||||
{
|
||||
seed ^= value + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2);
|
||||
};
|
||||
|
||||
std::size_t seed{};
|
||||
hash_combine(seed, std::hash<int>{}(texture.size.x));
|
||||
hash_combine(seed, std::hash<int>{}(texture.size.y));
|
||||
hash_combine(seed, std::hash<int>{}(texture.channels));
|
||||
hash_combine(seed, std::hash<int>{}(texture.filter));
|
||||
hash_combine(seed, std::hash<std::string>{}(path::to_utf8(path)));
|
||||
|
||||
if (!texture.pixels.empty())
|
||||
{
|
||||
std::string_view bytes(reinterpret_cast<const char*>(texture.pixels.data()), texture.pixels.size());
|
||||
hash_combine(seed, std::hash<std::string_view>{}(bytes));
|
||||
}
|
||||
else
|
||||
{
|
||||
hash_combine(seed, 0);
|
||||
}
|
||||
|
||||
return static_cast<uint64_t>(seed);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
#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();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
#include "texture.hpp"
|
||||
#include "anm2_type.hpp"
|
||||
#include "types.hpp"
|
||||
#include "origin.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
class Spritesheet
|
||||
{
|
||||
public:
|
||||
struct Region
|
||||
{
|
||||
using Origin = origin::Type;
|
||||
static constexpr Origin TOP_LEFT = origin::TOP_LEFT;
|
||||
static constexpr Origin ORIGIN_CENTER = origin::ORIGIN_CENTER;
|
||||
static constexpr Origin CUSTOM = origin::CUSTOM;
|
||||
|
||||
std::string name{};
|
||||
glm::vec2 crop{};
|
||||
glm::vec2 pivot{};
|
||||
glm::vec2 size{};
|
||||
Origin origin{CUSTOM};
|
||||
};
|
||||
|
||||
std::filesystem::path path{};
|
||||
resource::Texture texture;
|
||||
|
||||
std::map<int, Region> regions{};
|
||||
std::vector<int> regionOrder{};
|
||||
|
||||
Spritesheet() = default;
|
||||
Spritesheet(tinyxml2::XMLElement*, int&);
|
||||
Spritesheet(const std::filesystem::path&, const std::filesystem::path& = {});
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int, Flags = 0);
|
||||
std::string to_string(int id);
|
||||
std::string region_to_string(int id);
|
||||
bool regions_deserialize(const std::string&, types::merge::Type, std::string* = nullptr);
|
||||
bool save(const std::filesystem::path&, const std::filesystem::path& = {});
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int, Flags = 0);
|
||||
void reload(const std::filesystem::path&, const std::filesystem::path& = {});
|
||||
bool is_valid();
|
||||
uint64_t hash() const;
|
||||
};
|
||||
}
|
||||
+18
-8
@@ -1,4 +1,4 @@
|
||||
#include "canvas.h"
|
||||
#include "canvas.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <glm/ext/matrix_clip_space.hpp>
|
||||
@@ -6,20 +6,14 @@
|
||||
#include <glm/gtc/matrix_inverse.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#include "math_.h"
|
||||
#include "math_.hpp"
|
||||
|
||||
using namespace glm;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::canvas;
|
||||
|
||||
namespace anm2ed
|
||||
{
|
||||
constexpr float AXIS_VERTICES[] = {-1.0f, 0.0f, 1.0f, 0.0f};
|
||||
constexpr float GRID_VERTICES[] = {-1.f, -1.f, 0.f, 0.f, 3.f, -1.f, 2.f, 0.f, -1.f, 3.f, 0.f, 2.f};
|
||||
constexpr float RECT_VERTICES[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
|
||||
constexpr GLuint TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0};
|
||||
|
||||
Canvas::Canvas() = default;
|
||||
|
||||
Canvas::Canvas(vec2 size)
|
||||
@@ -218,6 +212,22 @@ namespace anm2ed
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
void Canvas::rect_fill_render(Shader& shader, const mat4& transform, const mat4& model, vec4 color) const
|
||||
{
|
||||
glUseProgram(shader.id);
|
||||
|
||||
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
|
||||
if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_MODEL); location != -1)
|
||||
glUniformMatrix4fv(location, 1, GL_FALSE, value_ptr(model));
|
||||
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), 1, value_ptr(color));
|
||||
|
||||
glBindVertexArray(rectVAO);
|
||||
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
|
||||
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
void Canvas::zoom_set(float& zoom, vec2& pan, vec2 focus, float step) const
|
||||
{
|
||||
auto zoomFactor = math::percent_to_unit(zoom);
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <glad/glad.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "framebuffer.h"
|
||||
#include "shader.h"
|
||||
|
||||
namespace anm2ed::canvas
|
||||
{
|
||||
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};
|
||||
|
||||
constexpr auto PIVOT_SIZE = glm::vec2(8, 8);
|
||||
constexpr auto ZOOM_MIN = 1.0f;
|
||||
constexpr auto ZOOM_MAX = 2000.0f;
|
||||
|
||||
constexpr auto DASH_LENGTH = 4.0f;
|
||||
constexpr auto DASH_GAP = 1.0f;
|
||||
constexpr auto DASH_OFFSET = 1.0f;
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
class Canvas : public Framebuffer
|
||||
{
|
||||
public:
|
||||
GLuint axisVAO{};
|
||||
GLuint axisVBO{};
|
||||
GLuint rectVAO{};
|
||||
GLuint rectVBO{};
|
||||
GLuint gridVAO{};
|
||||
GLuint gridVBO{};
|
||||
GLuint textureVAO{};
|
||||
GLuint textureVBO{};
|
||||
GLuint textureEBO{};
|
||||
|
||||
Canvas();
|
||||
Canvas(glm::vec2);
|
||||
~Canvas();
|
||||
glm::mat4 transform_get(float = 100.0f, glm::vec2 = {}) const;
|
||||
void axes_render(resource::Shader&, float, glm::vec2, glm::vec4 = glm::vec4(1.0f)) const;
|
||||
void grid_render(resource::Shader&, float, glm::vec2, glm::ivec2 = glm::ivec2(32, 32), glm::ivec2 = {},
|
||||
glm::vec4 = glm::vec4(1.0f)) const;
|
||||
void texture_render(resource::Shader&, GLuint&, glm::mat4 = {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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <glad/glad.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "framebuffer.hpp"
|
||||
#include "shader.hpp"
|
||||
|
||||
#include "types.hpp"
|
||||
|
||||
namespace anm2ed
|
||||
{
|
||||
class Canvas : public Framebuffer
|
||||
{
|
||||
public:
|
||||
static 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};
|
||||
static constexpr auto PIVOT_SIZE = glm::vec2(8, 8);
|
||||
static constexpr auto ZOOM_MIN = 1.0f;
|
||||
static constexpr auto ZOOM_MAX = 2000.0f;
|
||||
static constexpr auto DASH_LENGTH = 4.0f;
|
||||
static constexpr auto DASH_GAP = 1.0f;
|
||||
static constexpr auto DASH_OFFSET = 1.0f;
|
||||
static constexpr auto STEP = 1.0f;
|
||||
static constexpr auto STEP_FAST = 5.0f;
|
||||
static constexpr auto GRID_SIZE_MIN = 1;
|
||||
static constexpr auto GRID_SIZE_MAX = 10000;
|
||||
static constexpr auto GRID_OFFSET_MIN = 0;
|
||||
static constexpr auto GRID_OFFSET_MAX = 10000;
|
||||
static constexpr auto CHECKER_SIZE = 32.0f;
|
||||
static constexpr float AXIS_VERTICES[] = {-1.0f, 0.0f, 1.0f, 0.0f};
|
||||
static 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};
|
||||
static constexpr float RECT_VERTICES[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
|
||||
static constexpr GLuint TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0};
|
||||
static constexpr auto BORDER_DASH_LENGTH = 1.0f;
|
||||
static constexpr auto BORDER_DASH_GAP = 0.5f;
|
||||
static constexpr auto BORDER_DASH_OFFSET = 0.0f;
|
||||
static constexpr auto PIVOT_COLOR = types::color::PINK;
|
||||
|
||||
GLuint axisVAO{};
|
||||
GLuint axisVBO{};
|
||||
GLuint rectVAO{};
|
||||
GLuint rectVBO{};
|
||||
GLuint gridVAO{};
|
||||
GLuint gridVBO{};
|
||||
GLuint textureVAO{};
|
||||
GLuint textureVBO{};
|
||||
GLuint textureEBO{};
|
||||
|
||||
Canvas();
|
||||
Canvas(glm::vec2);
|
||||
~Canvas();
|
||||
glm::mat4 transform_get(float = 100.0f, glm::vec2 = {}) const;
|
||||
void axes_render(resource::Shader&, float, glm::vec2, glm::vec4 = glm::vec4(1.0f)) const;
|
||||
void grid_render(resource::Shader&, float, glm::vec2, glm::ivec2 = glm::ivec2(32, 32), glm::ivec2 = {},
|
||||
glm::vec4 = glm::vec4(1.0f)) const;
|
||||
void texture_render(resource::Shader&, GLuint&, glm::mat4 = {1.0f}, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {},
|
||||
float* = (float*)TEXTURE_VERTICES) const;
|
||||
void rect_fill_render(resource::Shader&, const glm::mat4&, const glm::mat4&,
|
||||
glm::vec4 = glm::vec4(1.0f)) const;
|
||||
void rect_render(resource::Shader&, const glm::mat4&, const glm::mat4&, glm::vec4 = glm::vec4(1.0f),
|
||||
float dashLength = DASH_LENGTH, float dashGap = DASH_GAP, float dashOffset = 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;
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#include "clipboard.h"
|
||||
#include "clipboard.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
|
||||
+27
-26
@@ -1,17 +1,23 @@
|
||||
#include "dialog.h"
|
||||
#include "dialog.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif __unix__
|
||||
#else
|
||||
#include "log.h"
|
||||
#include "strings.h"
|
||||
#include "toast.h"
|
||||
#include "log.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
|
||||
namespace anm2ed::dialog
|
||||
#include "path_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
|
||||
namespace anm2ed
|
||||
{
|
||||
static void callback(void* userData, const char* const* filelist, int filter)
|
||||
{
|
||||
@@ -19,18 +25,15 @@ namespace anm2ed::dialog
|
||||
|
||||
if (filelist && filelist[0] && strlen(filelist[0]) > 0)
|
||||
{
|
||||
self->path = filelist[0];
|
||||
self->path = path::from_utf8(filelist[0]);
|
||||
self->selectedFilter = filter;
|
||||
}
|
||||
else
|
||||
{
|
||||
self->selectedFilter = -1;
|
||||
self->path.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using namespace anm2ed::dialog;
|
||||
|
||||
namespace anm2ed
|
||||
{
|
||||
|
||||
Dialog::Dialog(SDL_Window* window)
|
||||
{
|
||||
@@ -38,32 +41,37 @@ namespace anm2ed
|
||||
this->window = window;
|
||||
}
|
||||
|
||||
void Dialog::file_open(dialog::Type type)
|
||||
void Dialog::file_open(Type type)
|
||||
{
|
||||
if (type == Dialog::NONE) return;
|
||||
SDL_ShowOpenFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]),
|
||||
nullptr, false);
|
||||
this->type = type;
|
||||
}
|
||||
|
||||
void Dialog::file_save(dialog::Type type)
|
||||
void Dialog::file_save(Type type)
|
||||
{
|
||||
if (type == Dialog::NONE) return;
|
||||
SDL_ShowSaveFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]),
|
||||
nullptr);
|
||||
this->type = type;
|
||||
}
|
||||
|
||||
void Dialog::folder_open(dialog::Type type)
|
||||
void Dialog::folder_open(Type type)
|
||||
{
|
||||
if (type == Dialog::NONE) return;
|
||||
SDL_ShowOpenFolderDialog(callback, this, window, nullptr, false);
|
||||
this->type = type;
|
||||
}
|
||||
|
||||
void Dialog::file_explorer_open(const std::string& path)
|
||||
void Dialog::file_explorer_open(const std::filesystem::path& path)
|
||||
{
|
||||
if (path.empty()) return;
|
||||
#ifdef _WIN32
|
||||
ShellExecuteA(NULL, "open", path.c_str(), NULL, NULL, SW_SHOWNORMAL);
|
||||
ShellExecuteW(nullptr, L"open", path.native().c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
#elif __unix__
|
||||
system(std::format("xdg-open \"{}\" &", path).c_str());
|
||||
auto pathUtf8 = path::to_utf8(path);
|
||||
system(std::format("xdg-open \"{}\" &", pathUtf8).c_str());
|
||||
#else
|
||||
toasts.push(localize.get(TOAST_NOT_SUPPORTED));
|
||||
logger.warning(localize.get(TOAST_NOT_SUPPORTED, anm2ed::ENGLISH));
|
||||
@@ -72,13 +80,6 @@ namespace anm2ed
|
||||
|
||||
void Dialog::reset() { *this = Dialog(this->window); }
|
||||
|
||||
bool Dialog::is_selected(dialog::Type type) const { return this->type == type && !path.empty(); }
|
||||
bool Dialog::is_selected(Type type) const { return this->type == type && !path.empty(); }
|
||||
|
||||
void Dialog::set_string_to_selected_path(std::string& string, dialog::Type type)
|
||||
{
|
||||
if (type == NONE) return;
|
||||
if (!is_selected(type)) return;
|
||||
string = path;
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace anm2ed::dialog
|
||||
namespace anm2ed
|
||||
{
|
||||
class Dialog
|
||||
{
|
||||
public:
|
||||
#if defined(_WIN32)
|
||||
#define EXECUTABLE_FILTER {"Executable", "exe"}
|
||||
#else
|
||||
@@ -15,25 +18,25 @@ namespace anm2ed::dialog
|
||||
#define FILTER_LIST \
|
||||
X(NO_FILTER, {}) \
|
||||
X(ANM2, {"Anm2 file", "anm2;xml"}) \
|
||||
X(PNG, {"PNG image", "png"}) \
|
||||
X(PNG, {"PNG image", "png;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)
|
||||
|
||||
enum Filter
|
||||
{
|
||||
enum Filter
|
||||
{
|
||||
#define X(symbol, ...) symbol,
|
||||
FILTER_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
constexpr SDL_DialogFileFilter FILTERS[][1] = {
|
||||
#define X(symbol, ...) {__VA_ARGS__},
|
||||
FILTER_LIST
|
||||
#undef X
|
||||
};
|
||||
};
|
||||
|
||||
static constexpr SDL_DialogFileFilter FILTERS[][1] = {
|
||||
#define X(symbol, ...) {__VA_ARGS__},
|
||||
FILTER_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
#undef FILTER_LIST
|
||||
|
||||
@@ -43,8 +46,10 @@ namespace anm2ed::dialog
|
||||
X(ANM2_OPEN, ANM2) \
|
||||
X(ANM2_SAVE, ANM2) \
|
||||
X(SOUND_OPEN, SOUND) \
|
||||
X(SOUND_REPLACE, SOUND) \
|
||||
X(SPRITESHEET_OPEN, PNG) \
|
||||
X(SPRITESHEET_REPLACE, PNG) \
|
||||
X(SPRITESHEET_PATH_SET, PNG) \
|
||||
X(FFMPEG_PATH_SET, EXECUTABLE) \
|
||||
X(PNG_DIRECTORY_SET, NO_FILTER) \
|
||||
X(PNG_PATH_SET, PNG) \
|
||||
@@ -52,41 +57,33 @@ namespace anm2ed::dialog
|
||||
X(WEBM_PATH_SET, WEBM) \
|
||||
X(MP4_PATH_SET, MP4)
|
||||
|
||||
enum Type
|
||||
{
|
||||
enum Type
|
||||
{
|
||||
#define X(symbol, filter) symbol,
|
||||
DIALOG_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
constexpr Filter TYPE_FILTERS[] = {
|
||||
#define X(symbol, filter) filter,
|
||||
DIALOG_LIST
|
||||
#undef X
|
||||
};
|
||||
};
|
||||
|
||||
static constexpr Filter TYPE_FILTERS[] = {
|
||||
#define X(symbol, filter) filter,
|
||||
DIALOG_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
#undef DIALOG_LIST
|
||||
}
|
||||
|
||||
namespace anm2ed
|
||||
{
|
||||
|
||||
class Dialog
|
||||
{
|
||||
public:
|
||||
SDL_Window* window{};
|
||||
std::string path{};
|
||||
dialog::Type type{dialog::NONE};
|
||||
std::filesystem::path path{};
|
||||
Type type{NONE};
|
||||
int selectedFilter{-1};
|
||||
|
||||
Dialog() = default;
|
||||
Dialog(SDL_Window*);
|
||||
void file_open(dialog::Type type);
|
||||
void file_save(dialog::Type type);
|
||||
void folder_open(dialog::Type type);
|
||||
bool is_selected(dialog::Type type) const;
|
||||
void file_open(Type type);
|
||||
void file_save(Type type);
|
||||
void folder_open(Type type);
|
||||
bool is_selected(Type type) const;
|
||||
void reset();
|
||||
void file_explorer_open(const std::string&);
|
||||
void set_string_to_selected_path(std::string& set, dialog::Type type);
|
||||
void file_explorer_open(const std::filesystem::path&);
|
||||
};
|
||||
}
|
||||
+176
-70
@@ -1,57 +1,84 @@
|
||||
#include "document.h"
|
||||
#include "document.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "log.h"
|
||||
#include "strings.h"
|
||||
#include "toast.h"
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::anm2;
|
||||
using namespace anm2ed::imgui;
|
||||
using namespace anm2ed::types;
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed
|
||||
{
|
||||
Document::Document(Anm2& anm2, const std::string& path)
|
||||
namespace
|
||||
{
|
||||
anm2::Flags serialize_flags_get(anm2::Compatibility compatibility)
|
||||
{
|
||||
if (auto it = anm2::COMPATIBILITY_FLAGS.find(compatibility); it != anm2::COMPATIBILITY_FLAGS.end())
|
||||
return it->second;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Document::Document(Anm2& anm2, const std::filesystem::path& path)
|
||||
{
|
||||
this->anm2 = std::move(anm2);
|
||||
this->path = path;
|
||||
isValid = this->anm2.isValid;
|
||||
if (!isValid) return;
|
||||
clean();
|
||||
change(Document::ALL);
|
||||
}
|
||||
|
||||
Document::Document(const std::string& path, bool isNew, std::string* errorString)
|
||||
Document::Document(const std::filesystem::path& path, bool isNew, std::string* errorString)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
anm2 = anm2::Anm2();
|
||||
if (!save(path)) return;
|
||||
if (!save(path, errorString))
|
||||
{
|
||||
isValid = false;
|
||||
this->path.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
anm2 = Anm2(path, errorString);
|
||||
if (errorString && !errorString->empty()) return;
|
||||
if (!anm2.isValid)
|
||||
{
|
||||
isValid = false;
|
||||
this->path.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->path = path;
|
||||
isValid = anm2.isValid;
|
||||
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),
|
||||
playback(current.playback), animation(current.animation), event(current.event), frames(current.frames),
|
||||
items(current.items), layer(current.layer), merge(current.merge), null(current.null), region(current.region),
|
||||
sound(current.sound), spritesheet(current.spritesheet), anm2(current.anm2), reference(current.reference),
|
||||
frameTime(current.frameTime), message(current.message), regionBySpritesheet(std::move(other.regionBySpritesheet)),
|
||||
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)
|
||||
autosaveHash(other.autosaveHash), lastAutosaveTime(other.lastAutosaveTime), isValid(other.isValid),
|
||||
isOpen(other.isOpen), isForceDirty(other.isForceDirty),
|
||||
spritesheetHashes(std::move(other.spritesheetHashes)),
|
||||
spritesheetSaveHashes(std::move(other.spritesheetSaveHashes)),
|
||||
isAnimationPreviewSet(other.isAnimationPreviewSet), isSpritesheetEditorSet(other.isSpritesheetEditorSet)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -66,37 +93,44 @@ namespace anm2ed
|
||||
editorPan = other.editorPan;
|
||||
editorZoom = other.editorZoom;
|
||||
overlayIndex = other.overlayIndex;
|
||||
regionBySpritesheet = std::move(other.regionBySpritesheet);
|
||||
hash = other.hash;
|
||||
saveHash = other.saveHash;
|
||||
autosaveHash = other.autosaveHash;
|
||||
lastAutosaveTime = other.lastAutosaveTime;
|
||||
isValid = other.isValid;
|
||||
isOpen = other.isOpen;
|
||||
isForceDirty = other.isForceDirty;
|
||||
spritesheetHashes = std::move(other.spritesheetHashes);
|
||||
spritesheetSaveHashes = std::move(other.spritesheetSaveHashes);
|
||||
isAnimationPreviewSet = other.isAnimationPreviewSet;
|
||||
isSpritesheetEditorSet = other.isSpritesheetEditorSet;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool Document::save(const std::string& path, std::string* errorString)
|
||||
bool Document::save(const std::filesystem::path& path, std::string* errorString, anm2::Compatibility compatibility,
|
||||
bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation)
|
||||
{
|
||||
this->path = !path.empty() ? path : this->path.string();
|
||||
this->path = !path.empty() ? path : this->path;
|
||||
|
||||
auto absolutePath = this->path.string();
|
||||
if (anm2.serialize(absolutePath, errorString))
|
||||
auto absolutePath = this->path;
|
||||
auto absolutePathUtf8 = path::to_utf8(absolutePath);
|
||||
if (anm2.serialize(absolutePath, errorString, serialize_flags_get(compatibility),
|
||||
isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation))
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT), std::make_format_args(absolutePath)));
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT), std::make_format_args(absolutePathUtf8)));
|
||||
logger.info(
|
||||
std::vformat(localize.get(TOAST_SAVE_DOCUMENT, anm2ed::ENGLISH), std::make_format_args(absolutePath)));
|
||||
std::vformat(localize.get(TOAST_SAVE_DOCUMENT, anm2ed::ENGLISH), std::make_format_args(absolutePathUtf8)));
|
||||
clean();
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
{
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_SAVE_DOCUMENT_FAILED), std::make_format_args(absolutePath, *errorString)));
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT_FAILED),
|
||||
std::make_format_args(absolutePathUtf8, *errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SAVE_DOCUMENT_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(absolutePath, *errorString)));
|
||||
std::make_format_args(absolutePathUtf8, *errorString)));
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -104,39 +138,43 @@ namespace anm2ed
|
||||
|
||||
std::filesystem::path Document::autosave_path_get()
|
||||
{
|
||||
return directory_get() / std::string("." + filename_get().string() + ".autosave");
|
||||
auto fileNameUtf8 = path::to_utf8(filename_get());
|
||||
auto autosaveNameUtf8 = "." + fileNameUtf8 + ".autosave";
|
||||
return directory_get() / path::from_utf8(autosaveNameUtf8);
|
||||
}
|
||||
|
||||
std::filesystem::path Document::path_from_autosave_get(std::filesystem::path& path)
|
||||
{
|
||||
auto fileName = path.filename().string();
|
||||
auto fileName = path::to_utf8(path.filename());
|
||||
if (!fileName.empty() && fileName.front() == '.') fileName.erase(fileName.begin());
|
||||
|
||||
auto restorePath = path.parent_path() / fileName;
|
||||
auto restorePath = path.parent_path() / std::filesystem::path(std::u8string(fileName.begin(), fileName.end()));
|
||||
restorePath.replace_extension("");
|
||||
|
||||
return path;
|
||||
return restorePath;
|
||||
}
|
||||
|
||||
bool Document::autosave(std::string* errorString)
|
||||
bool Document::autosave(std::string* errorString, anm2::Compatibility compatibility,
|
||||
bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation)
|
||||
{
|
||||
auto autosavePath = autosave_path_get();
|
||||
auto autosavePathString = autosavePath.string();
|
||||
if (anm2.serialize(autosavePathString, errorString))
|
||||
auto autosavePathUtf8 = path::to_utf8(autosavePath);
|
||||
if (anm2.serialize(autosavePath, errorString, serialize_flags_get(compatibility),
|
||||
isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation))
|
||||
{
|
||||
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()));
|
||||
logger.info(std::format("Autosaved document to: {}", autosavePathUtf8));
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
{
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_AUTOSAVE_FAILED), std::make_format_args(autosavePathString, *errorString)));
|
||||
std::vformat(localize.get(TOAST_AUTOSAVE_FAILED), std::make_format_args(autosavePathUtf8, *errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_AUTOSAVE_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(autosavePathString, *errorString)));
|
||||
std::make_format_args(autosavePathUtf8, *errorString)));
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -152,65 +190,99 @@ namespace anm2ed
|
||||
isForceDirty = false;
|
||||
}
|
||||
|
||||
void Document::spritesheet_hashes_reset()
|
||||
{
|
||||
spritesheetHashes.clear();
|
||||
spritesheetSaveHashes.clear();
|
||||
for (auto& [id, spritesheet] : anm2.content.spritesheets)
|
||||
{
|
||||
auto currentHash = spritesheet.hash();
|
||||
spritesheetHashes[id] = currentHash;
|
||||
spritesheetSaveHashes[id] = currentHash;
|
||||
}
|
||||
}
|
||||
|
||||
void Document::spritesheet_hashes_sync()
|
||||
{
|
||||
for (auto it = spritesheetHashes.begin(); it != spritesheetHashes.end();)
|
||||
{
|
||||
if (!anm2.content.spritesheets.contains(it->first))
|
||||
it = spritesheetHashes.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
|
||||
for (auto it = spritesheetSaveHashes.begin(); it != spritesheetSaveHashes.end();)
|
||||
{
|
||||
if (!anm2.content.spritesheets.contains(it->first))
|
||||
it = spritesheetSaveHashes.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
|
||||
for (auto& [id, spritesheet] : anm2.content.spritesheets)
|
||||
{
|
||||
auto currentHash = spritesheet.hash();
|
||||
spritesheetHashes[id] = currentHash;
|
||||
if (!spritesheetSaveHashes.contains(id)) spritesheetSaveHashes[id] = currentHash;
|
||||
}
|
||||
}
|
||||
|
||||
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 events_set = [&]() { event.labels_set(anm2.event_labels_get(), anm2.event_ids_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());
|
||||
spritesheet.labels_set(anm2.spritesheet_labels_get(), anm2.spritesheet_ids_get());
|
||||
spritesheet_hashes_sync();
|
||||
};
|
||||
|
||||
auto sounds_set = [&]()
|
||||
{
|
||||
sound.unused = anm2.sounds_unused();
|
||||
sound.labels_set(anm2.sound_labels_get());
|
||||
auto sounds_set = [&]() { sound.labels_set(anm2.sound_labels_get(), anm2.sound_ids_get()); };
|
||||
|
||||
for (auto& animation : anm2.animations.items)
|
||||
for (auto& trigger : animation.triggers.frames)
|
||||
if (!anm2.content.sounds.contains(trigger.soundID)) trigger.soundID = -1;
|
||||
auto regions_set = [&]()
|
||||
{
|
||||
regionBySpritesheet.clear();
|
||||
for (auto& [id, spritesheet] : anm2.content.spritesheets)
|
||||
{
|
||||
Storage storage{};
|
||||
storage.labels_set(anm2.region_labels_get(spritesheet), anm2.region_ids_get(spritesheet));
|
||||
regionBySpritesheet.emplace(id, std::move(storage));
|
||||
}
|
||||
};
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case LAYERS:
|
||||
layers_set();
|
||||
break;
|
||||
case NULLS:
|
||||
nulls_set();
|
||||
break;
|
||||
case EVENTS:
|
||||
events_set();
|
||||
break;
|
||||
case SPRITESHEETS:
|
||||
spritesheets_set();
|
||||
regions_set();
|
||||
break;
|
||||
case SOUNDS:
|
||||
sounds_set();
|
||||
break;
|
||||
case ANIMATIONS:
|
||||
animations_set();
|
||||
break;
|
||||
case FRAMES:
|
||||
events_set();
|
||||
sounds_set();
|
||||
break;
|
||||
case ITEMS:
|
||||
spritesheets_set();
|
||||
break;
|
||||
case ANIMATIONS:
|
||||
case ALL:
|
||||
layers_set();
|
||||
nulls_set();
|
||||
events_set();
|
||||
spritesheets_set();
|
||||
regions_set();
|
||||
animations_set();
|
||||
sounds_set();
|
||||
break;
|
||||
@@ -221,9 +293,40 @@ namespace anm2ed
|
||||
|
||||
bool Document::is_dirty() const { return hash != saveHash; }
|
||||
bool Document::is_autosave_dirty() const { return hash != autosaveHash; }
|
||||
void Document::spritesheet_hash_update(int id)
|
||||
{
|
||||
if (!anm2.content.spritesheets.contains(id)) return;
|
||||
spritesheetHashes[id] = anm2.content.spritesheets.at(id).hash();
|
||||
}
|
||||
|
||||
void Document::spritesheet_hash_set_saved(int id)
|
||||
{
|
||||
if (!anm2.content.spritesheets.contains(id)) return;
|
||||
auto currentHash = anm2.content.spritesheets.at(id).hash();
|
||||
spritesheetHashes[id] = currentHash;
|
||||
spritesheetSaveHashes[id] = currentHash;
|
||||
}
|
||||
|
||||
bool Document::spritesheet_is_dirty(int id)
|
||||
{
|
||||
if (!anm2.content.spritesheets.contains(id)) return false;
|
||||
if (!spritesheetHashes.contains(id)) spritesheet_hash_update(id);
|
||||
auto saveIt = spritesheetSaveHashes.find(id);
|
||||
if (saveIt == spritesheetSaveHashes.end()) return false;
|
||||
return spritesheetHashes.at(id) != saveIt->second;
|
||||
}
|
||||
|
||||
bool Document::spritesheet_any_dirty()
|
||||
{
|
||||
for (auto& [id, spritesheet] : anm2.content.spritesheets)
|
||||
{
|
||||
if (spritesheet_is_dirty(id)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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(); }
|
||||
bool Document::is_valid() const { return isValid && !path.empty(); }
|
||||
|
||||
anm2::Frame* Document::frame_get()
|
||||
{
|
||||
@@ -237,43 +340,45 @@ namespace anm2ed
|
||||
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)
|
||||
void Document::spritesheet_add(const std::filesystem::path& path)
|
||||
{
|
||||
auto add = [&]()
|
||||
{
|
||||
int id{};
|
||||
auto pathCopy = path;
|
||||
if (anm2.spritesheet_add(directory_get().string(), path, id))
|
||||
if (anm2.spritesheet_add(directory_get(), path, id))
|
||||
{
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
auto path = spritesheet.path.string();
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
this->spritesheet.selection = {id};
|
||||
this->spritesheet.reference = id;
|
||||
toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED), std::make_format_args(id, path)));
|
||||
spritesheet_hash_set_saved(id);
|
||||
toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, path)));
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED), std::make_format_args(pathCopy)));
|
||||
auto pathUtf8 = path::to_utf8(pathCopy);
|
||||
toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED), std::make_format_args(pathUtf8)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(pathCopy)));
|
||||
std::make_format_args(pathUtf8)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT_PTR(this, localize.get(EDIT_ADD_SPRITESHEET), Document::SPRITESHEETS, add());
|
||||
}
|
||||
|
||||
void Document::sound_add(const std::string& path)
|
||||
void Document::sound_add(const std::filesystem::path& path)
|
||||
{
|
||||
auto add = [&]()
|
||||
{
|
||||
int id{};
|
||||
auto pathCopy = path;
|
||||
if (anm2.sound_add(directory_get().string(), path, id))
|
||||
if (anm2.sound_add(directory_get(), path, id))
|
||||
{
|
||||
auto& soundInfo = anm2.content.sounds[id];
|
||||
auto soundPath = soundInfo.path.string();
|
||||
auto soundPath = path::to_utf8(soundInfo.path);
|
||||
sound.selection = {id};
|
||||
sound.reference = id;
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, soundPath)));
|
||||
@@ -282,9 +387,10 @@ namespace anm2ed
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED), std::make_format_args(pathCopy)));
|
||||
auto pathUtf8 = path::to_utf8(pathCopy);
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED), std::make_format_args(pathUtf8)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(pathCopy)));
|
||||
std::make_format_args(pathUtf8)));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "snapshots.h"
|
||||
#include "snapshots.hpp"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
@@ -33,20 +35,22 @@ namespace anm2ed
|
||||
Snapshots snapshots{};
|
||||
Snapshot& current = snapshots.current;
|
||||
|
||||
Playback& playback = current.playback;
|
||||
Storage& animation = current.animation;
|
||||
Storage& event = current.event;
|
||||
Storage& frames = current.frames;
|
||||
Storage& items = current.items;
|
||||
Storage& layer = current.layer;
|
||||
Storage& merge = current.merge;
|
||||
Storage& null = current.null;
|
||||
Storage& region = current.region;
|
||||
Storage& sound = current.sound;
|
||||
Storage& spritesheet = current.spritesheet;
|
||||
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;
|
||||
std::map<int, Storage> regionBySpritesheet{};
|
||||
|
||||
float previewZoom{200};
|
||||
glm::vec2 previewPan{};
|
||||
@@ -58,18 +62,22 @@ namespace anm2ed
|
||||
uint64_t saveHash{};
|
||||
uint64_t autosaveHash{};
|
||||
double lastAutosaveTime{};
|
||||
bool isValid{true};
|
||||
bool isOpen{true};
|
||||
bool isForceDirty{false};
|
||||
std::unordered_map<int, uint64_t> spritesheetHashes{};
|
||||
std::unordered_map<int, uint64_t> spritesheetSaveHashes{};
|
||||
bool isAnimationPreviewSet{false};
|
||||
bool isSpritesheetEditorSet{false};
|
||||
|
||||
Document(anm2::Anm2& anm2, const std::string&);
|
||||
Document(const std::string&, bool = false, std::string* = nullptr);
|
||||
Document(anm2::Anm2& anm2, const std::filesystem::path&);
|
||||
Document(const std::filesystem::path&, 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);
|
||||
bool save(const std::filesystem::path& = {}, std::string* = nullptr, anm2::Compatibility = anm2::ANM2ED,
|
||||
bool = false, bool = true, bool = true);
|
||||
void hash_set();
|
||||
void clean();
|
||||
void change(ChangeType);
|
||||
@@ -78,16 +86,23 @@ namespace anm2ed
|
||||
std::filesystem::path directory_get() const;
|
||||
std::filesystem::path filename_get() const;
|
||||
bool is_valid() const;
|
||||
void spritesheet_hash_update(int);
|
||||
void spritesheet_hash_set_saved(int);
|
||||
bool spritesheet_is_dirty(int);
|
||||
bool spritesheet_any_dirty();
|
||||
void spritesheet_hashes_reset();
|
||||
void spritesheet_hashes_sync();
|
||||
|
||||
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&);
|
||||
void spritesheet_add(const std::filesystem::path&);
|
||||
void sound_add(const std::filesystem::path&);
|
||||
|
||||
bool autosave(std::string* = nullptr);
|
||||
bool autosave(std::string* = nullptr, anm2::Compatibility = anm2::ANM2ED, bool = false, bool = true,
|
||||
bool = true);
|
||||
std::filesystem::path autosave_path_get();
|
||||
std::filesystem::path path_from_autosave_get(std::filesystem::path&);
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
#include "framebuffer.h"
|
||||
#include "framebuffer.hpp"
|
||||
|
||||
#include "texture.h"
|
||||
#include "texture.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace glm;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "dockspace.h"
|
||||
#include "dockspace.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -14,6 +14,7 @@ namespace anm2ed::imgui
|
||||
|
||||
auto viewport = ImGui::GetMainViewport();
|
||||
auto windowHeight = viewport->Size.y - taskbar.height - documents.height;
|
||||
if (windowHeight < 1.0f) windowHeight = 1.0f;
|
||||
|
||||
ImGui::SetNextWindowViewport(viewport->ID);
|
||||
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height + documents.height));
|
||||
@@ -30,6 +31,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
if (settings.windowIsAnimationPreview) animationPreview.update(manager, settings, resources);
|
||||
if (settings.windowIsAnimations) animations.update(manager, settings, resources, clipboard);
|
||||
if (settings.windowIsRegions) regions.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);
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
#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"
|
||||
#include "documents.hpp"
|
||||
#include "taskbar.hpp"
|
||||
#include "window/animation_preview.hpp"
|
||||
#include "window/animations.hpp"
|
||||
#include "window/regions.hpp"
|
||||
#include "window/events.hpp"
|
||||
#include "window/frame_properties.hpp"
|
||||
#include "window/layers.hpp"
|
||||
#include "window/nulls.hpp"
|
||||
#include "window/onionskin.hpp"
|
||||
#include "window/sounds.hpp"
|
||||
#include "window/spritesheet_editor.hpp"
|
||||
#include "window/spritesheets.hpp"
|
||||
#include "window/timeline.hpp"
|
||||
#include "window/tools.hpp"
|
||||
#include "window/welcome.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -22,6 +23,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
AnimationPreview animationPreview;
|
||||
Animations animations;
|
||||
Regions regions;
|
||||
Events events;
|
||||
FrameProperties frameProperties;
|
||||
Layers layers;
|
||||
+57
-14
@@ -1,10 +1,13 @@
|
||||
#include "documents.h"
|
||||
#include "documents.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <vector>
|
||||
|
||||
#include "strings.h"
|
||||
#include "time_.h"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "time_.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::types;
|
||||
@@ -34,7 +37,10 @@ namespace anm2ed::imgui
|
||||
if (isDirty)
|
||||
{
|
||||
document.lastAutosaveTime += ImGui::GetIO().DeltaTime;
|
||||
if (document.lastAutosaveTime > settings.fileAutosaveTime * time::SECOND_M) manager.autosave(document);
|
||||
if (document.lastAutosaveTime > time::SECOND_M)
|
||||
manager.autosave(document, (anm2::Compatibility)settings.fileCompatibility,
|
||||
settings.fileBakeSpecialInterpolatedFramesOnSave, settings.bakeIsRoundScale,
|
||||
settings.bakeIsRoundRotation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +65,9 @@ namespace anm2ed::imgui
|
||||
for (int i = 0; i < documentsCount; ++i)
|
||||
{
|
||||
auto& document = manager.documents[i];
|
||||
auto isDirty = document.is_dirty() || document.isForceDirty;
|
||||
auto isDocumentDirty = document.is_dirty() || document.isForceDirty;
|
||||
auto isSpritesheetDirty = document.spritesheet_any_dirty();
|
||||
auto isDirty = isDocumentDirty || isSpritesheetDirty;
|
||||
|
||||
if (!closePopup.is_open())
|
||||
{
|
||||
@@ -85,13 +93,13 @@ namespace anm2ed::imgui
|
||||
}
|
||||
|
||||
auto isRequested = i == manager.pendingSelected;
|
||||
auto font = isDirty ? font::ITALICS : font::REGULAR;
|
||||
auto filename = document.filename_get().string();
|
||||
auto font = isDocumentDirty ? font::ITALICS : font::REGULAR;
|
||||
auto filename = path::to_utf8(document.filename_get());
|
||||
auto string =
|
||||
isDirty ? std::vformat(localize.get(FORMAT_NOT_SAVED), std::make_format_args(filename)) : filename;
|
||||
isDocumentDirty ? 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;
|
||||
auto flags = isDocumentDirty ? ImGuiTabItemFlags_UnsavedDocument : 0;
|
||||
if (isRequested) flags |= ImGuiTabItemFlags_SetSelected;
|
||||
|
||||
ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
|
||||
@@ -103,7 +111,8 @@ namespace anm2ed::imgui
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", document.path.string().c_str());
|
||||
auto pathUtf8 = path::to_utf8(document.path);
|
||||
ImGui::SetItemTooltip("%s", pathUtf8.c_str());
|
||||
|
||||
ImGui::PopFont();
|
||||
}
|
||||
@@ -125,8 +134,14 @@ namespace anm2ed::imgui
|
||||
{
|
||||
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));
|
||||
auto filename = path::to_utf8(closeDocument.filename_get());
|
||||
auto isDocumentDirty = closeDocument.is_dirty() || closeDocument.isForceDirty;
|
||||
auto isSpritesheetDirty = closeDocument.spritesheet_any_dirty();
|
||||
auto promptLabel = isDocumentDirty && isSpritesheetDirty
|
||||
? LABEL_DOCUMENT_AND_SPRITESHEETS_MODIFIED_PROMPT
|
||||
: (isDocumentDirty ? LABEL_DOCUMENT_MODIFIED_PROMPT
|
||||
: LABEL_SPRITESHEETS_MODIFIED_PROMPT);
|
||||
auto prompt = std::vformat(localize.get(promptLabel), std::make_format_args(filename));
|
||||
ImGui::TextUnformatted(prompt.c_str());
|
||||
|
||||
auto widgetSize = imgui::widget_size_with_row_get(3);
|
||||
@@ -137,9 +152,36 @@ namespace anm2ed::imgui
|
||||
closePopup.close();
|
||||
};
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
|
||||
{
|
||||
manager.save(closeDocumentIndex);
|
||||
if (isDocumentDirty)
|
||||
manager.save(closeDocumentIndex, {}, (anm2::Compatibility)settings.fileCompatibility,
|
||||
settings.fileBakeSpecialInterpolatedFramesOnSave, settings.bakeIsRoundScale,
|
||||
settings.bakeIsRoundRotation);
|
||||
|
||||
if (isSpritesheetDirty)
|
||||
{
|
||||
for (auto& [id, spritesheet] : closeDocument.anm2.content.spritesheets)
|
||||
{
|
||||
if (!closeDocument.spritesheet_is_dirty(id)) continue;
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
if (spritesheet.save(closeDocument.directory_get()))
|
||||
{
|
||||
closeDocument.spritesheet_hash_set_saved(id);
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED), std::make_format_args(id, pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
}
|
||||
}
|
||||
manager.close(closeDocumentIndex);
|
||||
close();
|
||||
}
|
||||
@@ -154,6 +196,7 @@ namespace anm2ed::imgui
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize))
|
||||
{
|
||||
isQuitting = false;
|
||||
@@ -210,7 +253,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
for (auto& path : manager.anm2DragDropPaths)
|
||||
{
|
||||
anm2::Anm2 source(path.string());
|
||||
anm2::Anm2 source(path);
|
||||
document->anm2.merge(source, document->directory_get(), path.parent_path());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include "strings.h"
|
||||
#include "taskbar.h"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "taskbar.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
+313
-10
@@ -1,20 +1,127 @@
|
||||
#include "imgui_.h"
|
||||
#include "strings.h"
|
||||
|
||||
#include <imgui/imgui_internal.h>
|
||||
|
||||
#include <format>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
static auto isRenaming = false;
|
||||
|
||||
namespace
|
||||
{
|
||||
const std::vector<std::pair<ImGuiKey, const char*>> CANONICAL_KEY_NAMES = {
|
||||
{ImGuiKey_A, "A"},
|
||||
{ImGuiKey_B, "B"},
|
||||
{ImGuiKey_C, "C"},
|
||||
{ImGuiKey_D, "D"},
|
||||
{ImGuiKey_E, "E"},
|
||||
{ImGuiKey_F, "F"},
|
||||
{ImGuiKey_G, "G"},
|
||||
{ImGuiKey_H, "H"},
|
||||
{ImGuiKey_I, "I"},
|
||||
{ImGuiKey_J, "J"},
|
||||
{ImGuiKey_K, "K"},
|
||||
{ImGuiKey_L, "L"},
|
||||
{ImGuiKey_M, "M"},
|
||||
{ImGuiKey_N, "N"},
|
||||
{ImGuiKey_O, "O"},
|
||||
{ImGuiKey_P, "P"},
|
||||
{ImGuiKey_Q, "Q"},
|
||||
{ImGuiKey_R, "R"},
|
||||
{ImGuiKey_S, "S"},
|
||||
{ImGuiKey_T, "T"},
|
||||
{ImGuiKey_U, "U"},
|
||||
{ImGuiKey_V, "V"},
|
||||
{ImGuiKey_W, "W"},
|
||||
{ImGuiKey_X, "X"},
|
||||
{ImGuiKey_Y, "Y"},
|
||||
{ImGuiKey_Z, "Z"},
|
||||
{ImGuiKey_0, "0"},
|
||||
{ImGuiKey_1, "1"},
|
||||
{ImGuiKey_2, "2"},
|
||||
{ImGuiKey_3, "3"},
|
||||
{ImGuiKey_4, "4"},
|
||||
{ImGuiKey_5, "5"},
|
||||
{ImGuiKey_6, "6"},
|
||||
{ImGuiKey_7, "7"},
|
||||
{ImGuiKey_8, "8"},
|
||||
{ImGuiKey_9, "9"},
|
||||
{ImGuiKey_Keypad0, "Num0"},
|
||||
{ImGuiKey_Keypad1, "Num1"},
|
||||
{ImGuiKey_Keypad2, "Num2"},
|
||||
{ImGuiKey_Keypad3, "Num3"},
|
||||
{ImGuiKey_Keypad4, "Num4"},
|
||||
{ImGuiKey_Keypad5, "Num5"},
|
||||
{ImGuiKey_Keypad6, "Num6"},
|
||||
{ImGuiKey_Keypad7, "Num7"},
|
||||
{ImGuiKey_Keypad8, "Num8"},
|
||||
{ImGuiKey_Keypad9, "Num9"},
|
||||
{ImGuiKey_KeypadAdd, "NumAdd"},
|
||||
{ImGuiKey_KeypadSubtract, "NumSubtract"},
|
||||
{ImGuiKey_KeypadMultiply, "NumMultiply"},
|
||||
{ImGuiKey_KeypadDivide, "NumDivide"},
|
||||
{ImGuiKey_KeypadEnter, "NumEnter"},
|
||||
{ImGuiKey_KeypadDecimal, "NumDecimal"},
|
||||
{ImGuiKey_KeypadEqual, "NumEqual"},
|
||||
{ImGuiKey_F1, "F1"},
|
||||
{ImGuiKey_F2, "F2"},
|
||||
{ImGuiKey_F3, "F3"},
|
||||
{ImGuiKey_F4, "F4"},
|
||||
{ImGuiKey_F5, "F5"},
|
||||
{ImGuiKey_F6, "F6"},
|
||||
{ImGuiKey_F7, "F7"},
|
||||
{ImGuiKey_F8, "F8"},
|
||||
{ImGuiKey_F9, "F9"},
|
||||
{ImGuiKey_F10, "F10"},
|
||||
{ImGuiKey_F11, "F11"},
|
||||
{ImGuiKey_F12, "F12"},
|
||||
{ImGuiKey_UpArrow, "Up"},
|
||||
{ImGuiKey_DownArrow, "Down"},
|
||||
{ImGuiKey_LeftArrow, "Left"},
|
||||
{ImGuiKey_RightArrow, "Right"},
|
||||
{ImGuiKey_Space, "Space"},
|
||||
{ImGuiKey_Enter, "Enter"},
|
||||
{ImGuiKey_Escape, "Escape"},
|
||||
{ImGuiKey_Tab, "Tab"},
|
||||
{ImGuiKey_Backspace, "Backspace"},
|
||||
{ImGuiKey_Delete, "Delete"},
|
||||
{ImGuiKey_Insert, "Insert"},
|
||||
{ImGuiKey_Home, "Home"},
|
||||
{ImGuiKey_End, "End"},
|
||||
{ImGuiKey_PageUp, "PageUp"},
|
||||
{ImGuiKey_PageDown, "PageDown"},
|
||||
{ImGuiKey_Minus, "Minus"},
|
||||
{ImGuiKey_Equal, "Equal"},
|
||||
{ImGuiKey_LeftBracket, "LeftBracket"},
|
||||
{ImGuiKey_RightBracket, "RightBracket"},
|
||||
{ImGuiKey_Semicolon, "Semicolon"},
|
||||
{ImGuiKey_Apostrophe, "Apostrophe"},
|
||||
{ImGuiKey_Comma, "Comma"},
|
||||
{ImGuiKey_Period, "Period"},
|
||||
{ImGuiKey_Slash, "Slash"},
|
||||
{ImGuiKey_Backslash, "Backslash"},
|
||||
{ImGuiKey_GraveAccent, "GraveAccent"},
|
||||
};
|
||||
|
||||
const char* canonical_key_name(ImGuiKey key)
|
||||
{
|
||||
for (const auto& [mappedKey, name] : CANONICAL_KEY_NAMES)
|
||||
if (mappedKey == key) return name;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
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};
|
||||
@@ -84,6 +191,17 @@ namespace anm2ed::imgui
|
||||
return ImGui::InputText(label, string->data(), string->capacity() + 1, flags, input_text_callback, string);
|
||||
}
|
||||
|
||||
bool input_text_path(const char* label, std::filesystem::path* path, ImGuiInputTextFlags flags)
|
||||
{
|
||||
if (!path) return false;
|
||||
|
||||
auto pathUtf8 = path::to_utf8(*path);
|
||||
auto edited = input_text_string(label, &pathUtf8, flags);
|
||||
if (edited) *path = path::from_utf8(pathUtf8);
|
||||
|
||||
return edited;
|
||||
}
|
||||
|
||||
bool combo_negative_one_indexed(const std::string& label, int* index, std::vector<const char*>& strings)
|
||||
{
|
||||
*index += 1;
|
||||
@@ -93,6 +211,182 @@ namespace anm2ed::imgui
|
||||
return isActivated;
|
||||
}
|
||||
|
||||
bool combo_id_mapped(const std::string& label, int* id, const std::vector<int>& ids, std::vector<const char*>& labels)
|
||||
{
|
||||
if (!id) return false;
|
||||
|
||||
int index = -1;
|
||||
if (!ids.empty())
|
||||
{
|
||||
auto it = std::find(ids.begin(), ids.end(), *id);
|
||||
if (it != ids.end()) index = (int)std::distance(ids.begin(), it);
|
||||
}
|
||||
|
||||
bool isActivated = ImGui::Combo(label.c_str(), &index, labels.data(), (int)labels.size());
|
||||
if (isActivated)
|
||||
{
|
||||
if (index >= 0 && index < (int)ids.size())
|
||||
*id = ids[index];
|
||||
else
|
||||
*id = -1;
|
||||
}
|
||||
|
||||
return isActivated;
|
||||
}
|
||||
|
||||
edit::Type drag_int_persistent(const char* label, int* value, float speed, int min, int max, const char* format,
|
||||
ImGuiSliderFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static int start{INT_MAX};
|
||||
auto persistent = value ? *value : 0;
|
||||
|
||||
ImGui::DragInt(label, &persistent, speed, min, max, format, flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = INT_MAX;
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
edit::Type drag_float_persistent(const char* label, float* value, float speed, float min, float max,
|
||||
const char* format, ImGuiSliderFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static float start{NAN};
|
||||
auto persistent = value ? *value : 0;
|
||||
|
||||
ImGui::DragFloat(label, &persistent, speed, min, max, format, flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = NAN;
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
edit::Type drag_float2_persistent(const char* label, vec2* value, float speed, float min, float max,
|
||||
const char* format, ImGuiSliderFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static vec2 start{NAN};
|
||||
auto persistent = value ? *value : vec2();
|
||||
|
||||
ImGui::DragFloat2(label, value_ptr(persistent), speed, min, max, format, flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = vec2{NAN};
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
edit::Type color_edit3_persistent(const char* label, vec3* value, ImGuiColorEditFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static vec3 start{NAN};
|
||||
auto persistent = value ? *value : vec4();
|
||||
|
||||
ImGui::ColorEdit3(label, value_ptr(persistent), flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = vec4{NAN};
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
edit::Type color_edit4_persistent(const char* label, vec4* value, ImGuiColorEditFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static vec4 start{NAN};
|
||||
auto persistent = value ? *value : vec4();
|
||||
|
||||
ImGui::ColorEdit4(label, value_ptr(persistent), flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = vec4{NAN};
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
bool input_int_range(const char* label, int& value, int min, int max, int step, int stepFast,
|
||||
ImGuiInputTextFlags flags)
|
||||
{
|
||||
@@ -116,10 +410,16 @@ namespace anm2ed::imgui
|
||||
return isActivated;
|
||||
}
|
||||
|
||||
std::string& selectable_input_text_id()
|
||||
{
|
||||
static std::string editID{};
|
||||
return editID;
|
||||
}
|
||||
|
||||
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& editID = selectable_input_text_id();
|
||||
auto isRename = editID == id;
|
||||
bool isActivated{};
|
||||
|
||||
@@ -148,8 +448,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
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)))
|
||||
if (state == RENAME_FORCE_EDIT || (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)))
|
||||
{
|
||||
state = RENAME_BEGIN;
|
||||
editID = id;
|
||||
@@ -232,10 +531,10 @@ namespace anm2ed::imgui
|
||||
|
||||
if (auto key = (ImGuiKey)(chord & ~ImGuiMod_Mask_); key != ImGuiKey_None)
|
||||
{
|
||||
if (const char* name = ImGui::GetKeyName(key); name && *name)
|
||||
if (const char* name = canonical_key_name(key); name && *name)
|
||||
result += name;
|
||||
else
|
||||
result += "Unknown";
|
||||
result += ImGui::GetKeyName(key);
|
||||
}
|
||||
|
||||
if (!result.empty() && result.back() == '+') result.pop_back();
|
||||
@@ -304,7 +603,11 @@ namespace anm2ed::imgui
|
||||
|
||||
bool shortcut(ImGuiKeyChord chord, shortcut::Type type)
|
||||
{
|
||||
if (ImGui::GetTopMostPopupModal() != nullptr) return false;
|
||||
if (chord == ImGuiKey_None) return false;
|
||||
|
||||
if (ImGui::GetTopMostPopupModal() != nullptr &&
|
||||
(type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET))
|
||||
return false;
|
||||
|
||||
int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal
|
||||
: ImGuiInputFlags_RouteFocused;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <glm/glm.hpp>
|
||||
#include <imgui/imgui.h>
|
||||
#include <set>
|
||||
@@ -7,12 +8,13 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "strings.h"
|
||||
#include "types.h"
|
||||
#include "strings.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
constexpr auto DRAG_SPEED = 1.0f;
|
||||
constexpr auto DRAG_SPEED = 0.25f;
|
||||
constexpr auto DRAG_SPEED_FAST = 1.00f;
|
||||
constexpr auto STEP = 1.0f;
|
||||
constexpr auto STEP_FAST = 5.0f;
|
||||
|
||||
@@ -113,6 +115,7 @@ namespace anm2ed::imgui
|
||||
{"NumDivide", ImGuiKey_KeypadDivide},
|
||||
{"NumEnter", ImGuiKey_KeypadEnter},
|
||||
{"NumDecimal", ImGuiKey_KeypadDecimal},
|
||||
{"NumEqual", ImGuiKey_KeypadEqual},
|
||||
|
||||
{"F1", ImGuiKey_F1},
|
||||
{"F2", ImGuiKey_F2},
|
||||
@@ -155,6 +158,29 @@ namespace anm2ed::imgui
|
||||
{"Slash", ImGuiKey_Slash},
|
||||
{"Backslash", ImGuiKey_Backslash},
|
||||
{"GraveAccent", ImGuiKey_GraveAccent},
|
||||
|
||||
// Legacy aliases for older saved shortcut strings.
|
||||
{"Keypad0", ImGuiKey_Keypad0},
|
||||
{"Keypad1", ImGuiKey_Keypad1},
|
||||
{"Keypad2", ImGuiKey_Keypad2},
|
||||
{"Keypad3", ImGuiKey_Keypad3},
|
||||
{"Keypad4", ImGuiKey_Keypad4},
|
||||
{"Keypad5", ImGuiKey_Keypad5},
|
||||
{"Keypad6", ImGuiKey_Keypad6},
|
||||
{"Keypad7", ImGuiKey_Keypad7},
|
||||
{"Keypad8", ImGuiKey_Keypad8},
|
||||
{"Keypad9", ImGuiKey_Keypad9},
|
||||
{"KeypadAdd", ImGuiKey_KeypadAdd},
|
||||
{"KeypadSubtract", ImGuiKey_KeypadSubtract},
|
||||
{"KeypadMultiply", ImGuiKey_KeypadMultiply},
|
||||
{"KeypadDivide", ImGuiKey_KeypadDivide},
|
||||
{"KeypadEnter", ImGuiKey_KeypadEnter},
|
||||
{"KeypadDecimal", ImGuiKey_KeypadDecimal},
|
||||
{"KeypadEqual", ImGuiKey_KeypadEqual},
|
||||
{"UpArrow", ImGuiKey_UpArrow},
|
||||
{"DownArrow", ImGuiKey_DownArrow},
|
||||
{"LeftArrow", ImGuiKey_LeftArrow},
|
||||
{"RightArrow", ImGuiKey_RightArrow},
|
||||
};
|
||||
|
||||
const std::unordered_map<std::string, ImGuiKey> MOD_MAP = {
|
||||
@@ -175,11 +201,22 @@ namespace anm2ed::imgui
|
||||
ImVec2 child_size_get(int = 1);
|
||||
int input_text_callback(ImGuiInputTextCallbackData*);
|
||||
bool input_text_string(const char*, std::string*, ImGuiInputTextFlags = 0);
|
||||
bool input_text_path(const char*, std::filesystem::path*, 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);
|
||||
types::edit::Type drag_int_persistent(const char*, int*, float = DRAG_SPEED, int = {}, int = {}, const char* = "%d",
|
||||
ImGuiSliderFlags = 0);
|
||||
types::edit::Type drag_float_persistent(const char*, float*, float = DRAG_SPEED, float = {}, float = {},
|
||||
const char* = "%.3f", ImGuiSliderFlags = 0);
|
||||
types::edit::Type drag_float2_persistent(const char*, glm::vec2*, float = DRAG_SPEED, float = {}, float = {},
|
||||
const char* = "%.3f", ImGuiSliderFlags = 0);
|
||||
types::edit::Type color_edit3_persistent(const char*, glm::vec3*, ImGuiColorEditFlags = 0);
|
||||
types::edit::Type color_edit4_persistent(const char*, glm::vec4*, ImGuiColorEditFlags = 0);
|
||||
bool combo_negative_one_indexed(const std::string&, int*, std::vector<const char*>&);
|
||||
bool combo_id_mapped(const std::string&, int*, const std::vector<int>&, std::vector<const char*>&);
|
||||
std::string& selectable_input_text_id();
|
||||
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& = {});
|
||||
@@ -205,9 +242,8 @@ namespace anm2ed::imgui
|
||||
using std::set<int>::erase;
|
||||
|
||||
MultiSelectStorage();
|
||||
void start(size_t, ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_BoxSelect2d |
|
||||
ImGuiMultiSelectFlags_ClearOnEscape |
|
||||
ImGuiMultiSelectFlags_ScopeWindow);
|
||||
void start(size_t,
|
||||
ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ScopeWindow);
|
||||
void apply();
|
||||
void finish();
|
||||
void set_index_map(std::vector<int>*);
|
||||
+94
-809
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
||||
#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&);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "canvas.hpp"
|
||||
#include "dialog.hpp"
|
||||
#include "imgui_.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
#include "wizard/about.hpp"
|
||||
#include "wizard/change_all_frame_properties.hpp"
|
||||
#include "wizard/configure.hpp"
|
||||
#include "wizard/generate_animation_from_grid.hpp"
|
||||
#include "wizard/render_animation.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Taskbar
|
||||
{
|
||||
wizard::ChangeAllFrameProperties changeAllFrameProperties{};
|
||||
wizard::About about{};
|
||||
wizard::Configure configure{};
|
||||
wizard::GenerateAnimationFromGrid generateAnimationFromGrid{};
|
||||
wizard::RenderAnimation renderAnimation{};
|
||||
|
||||
Canvas generate;
|
||||
PopupHelper generatePopup{PopupHelper(LABEL_TASKBAR_GENERATE_ANIMATION_FROM_GRID)};
|
||||
PopupHelper changePopup{PopupHelper(LABEL_CHANGE_ALL_FRAME_PROPERTIES, imgui::POPUP_NORMAL_NO_HEIGHT)};
|
||||
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{};
|
||||
bool isQuittingMode{};
|
||||
|
||||
public:
|
||||
float height{};
|
||||
|
||||
void update(Manager&, Settings&, Resources&, Dialog&, bool&);
|
||||
};
|
||||
};
|
||||
+5
-3
@@ -1,9 +1,11 @@
|
||||
#include "toast.h"
|
||||
#include "toast.hpp"
|
||||
|
||||
#include "log.h"
|
||||
#include <format>
|
||||
|
||||
#include "log.hpp"
|
||||
#include <imgui/imgui.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "types.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
#include "animation_preview.h"
|
||||
#include "animation_preview.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <system_error>
|
||||
|
||||
#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"
|
||||
#include "imgui_.hpp"
|
||||
#include "log.hpp"
|
||||
#include "math_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "tool.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
using namespace anm2ed::canvas;
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::resource;
|
||||
@@ -28,9 +32,124 @@ 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);
|
||||
constexpr auto PLAYBACK_TICK_RATE = 30.0f;
|
||||
|
||||
namespace
|
||||
{
|
||||
std::filesystem::path render_destination_directory(const std::filesystem::path& path, int type)
|
||||
{
|
||||
if (type == render::PNGS) return path;
|
||||
auto directory = path.parent_path();
|
||||
if (directory.empty()) directory = std::filesystem::current_path();
|
||||
return directory;
|
||||
}
|
||||
|
||||
std::filesystem::path render_frame_filename(const std::filesystem::path& format, int index)
|
||||
{
|
||||
auto formatString = path::to_utf8(format);
|
||||
try
|
||||
{
|
||||
auto name = std::vformat(formatString, std::make_format_args(index));
|
||||
auto filename = path::from_utf8(name).filename();
|
||||
if (filename.empty()) return path::from_utf8(std::format("frame_{:06}.png", index));
|
||||
if (filename.extension().empty()) filename.replace_extension(render::EXTENSIONS[render::SPRITESHEET]);
|
||||
return filename;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return path::from_utf8(std::format("frame_{:06}.png", index));
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path render_temp_directory_create(const std::filesystem::path& directory)
|
||||
{
|
||||
auto timestamp = (uint64_t)std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
for (int suffix = 0; suffix < 1000; ++suffix)
|
||||
{
|
||||
auto tempDirectory = directory / path::from_utf8(std::format(".anm2ed_render_tmp_{}_{}", timestamp, suffix));
|
||||
std::error_code ec;
|
||||
if (std::filesystem::create_directories(tempDirectory, ec)) return tempDirectory;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void render_temp_cleanup(std::filesystem::path& directory, std::vector<std::filesystem::path>& frames)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (!directory.empty()) std::filesystem::remove_all(directory, ec);
|
||||
directory.clear();
|
||||
frames.clear();
|
||||
}
|
||||
|
||||
void pixels_unpremultiply_alpha(std::vector<uint8_t>& pixels)
|
||||
{
|
||||
for (size_t index = 0; index + 3 < pixels.size(); index += 4)
|
||||
{
|
||||
auto alpha = pixels[index + 3];
|
||||
if (alpha == 0)
|
||||
{
|
||||
pixels[index + 0] = 0;
|
||||
pixels[index + 1] = 0;
|
||||
pixels[index + 2] = 0;
|
||||
continue;
|
||||
}
|
||||
if (alpha == 255) continue;
|
||||
|
||||
float alphaUnit = (float)alpha / 255.0f;
|
||||
pixels[index + 0] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 0] / alphaUnit), 0.0f, 255.0f);
|
||||
pixels[index + 1] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 1] / alphaUnit), 0.0f, 255.0f);
|
||||
pixels[index + 2] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 2] / alphaUnit), 0.0f, 255.0f);
|
||||
}
|
||||
}
|
||||
bool render_audio_stream_generate(AudioStream& audioStream, std::map<int, anm2::Sound>& sounds,
|
||||
const std::vector<int>& frameSoundIDs, int fps)
|
||||
{
|
||||
audioStream.stream.clear();
|
||||
if (frameSoundIDs.empty() || fps <= 0) return true;
|
||||
|
||||
SDL_AudioSpec mixSpec = audioStream.spec;
|
||||
mixSpec.format = SDL_AUDIO_F32;
|
||||
auto* mixer = MIX_CreateMixer(&mixSpec);
|
||||
if (!mixer) return false;
|
||||
|
||||
auto channels = std::max(mixSpec.channels, 1);
|
||||
auto sampleRate = std::max(mixSpec.freq, 1);
|
||||
auto framesPerStep = (double)sampleRate / (double)fps;
|
||||
auto sampleFrameAccumulator = 0.0;
|
||||
auto frameBuffer = std::vector<float>{};
|
||||
|
||||
for (auto soundID : frameSoundIDs)
|
||||
{
|
||||
if (soundID != -1 && sounds.contains(soundID)) sounds.at(soundID).audio.play(false, mixer);
|
||||
|
||||
sampleFrameAccumulator += framesPerStep;
|
||||
auto sampleFramesToGenerate = (int)std::floor(sampleFrameAccumulator);
|
||||
sampleFramesToGenerate = std::max(sampleFramesToGenerate, 1);
|
||||
sampleFrameAccumulator -= (double)sampleFramesToGenerate;
|
||||
|
||||
frameBuffer.resize((std::size_t)sampleFramesToGenerate * (std::size_t)channels);
|
||||
if (!MIX_Generate(mixer, frameBuffer.data(), (int)(frameBuffer.size() * sizeof(float))))
|
||||
{
|
||||
for (auto& [_, sound] : sounds)
|
||||
sound.audio.track_detach(mixer);
|
||||
MIX_DestroyMixer(mixer);
|
||||
audioStream.stream.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
audioStream.stream.insert(audioStream.stream.end(), frameBuffer.begin(), frameBuffer.end());
|
||||
}
|
||||
|
||||
for (auto& [_, sound] : sounds)
|
||||
sound.audio.track_detach(mixer);
|
||||
MIX_DestroyMixer(mixer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
|
||||
|
||||
@@ -45,43 +164,37 @@ namespace anm2ed::imgui
|
||||
auto& overlayIndex = document.overlayIndex;
|
||||
auto& pan = document.previewPan;
|
||||
|
||||
auto stop_all_sounds = [&]()
|
||||
{
|
||||
for (auto& sound : anm2.content.sounds | std::views::values)
|
||||
sound.audio.stop(mixer);
|
||||
};
|
||||
|
||||
if (manager.isRecording)
|
||||
{
|
||||
auto& ffmpegPath = settings.renderFFmpegPath;
|
||||
auto& path = settings.renderPath;
|
||||
auto pathString = path::to_utf8(path);
|
||||
auto& type = settings.renderType;
|
||||
|
||||
if (playback.time > end || playback.isFinished)
|
||||
{
|
||||
if (settings.timelineIsSound) audioStream.capture_end(mixer);
|
||||
|
||||
if (type == render::PNGS)
|
||||
{
|
||||
auto& format = settings.renderFormat;
|
||||
bool isSuccess{true};
|
||||
for (auto [i, frame] : std::views::enumerate(renderFrames))
|
||||
if (!renderTempFrames.empty())
|
||||
{
|
||||
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)));
|
||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES), std::make_format_args(pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES, anm2ed::ENGLISH),
|
||||
std::make_format_args(path)));
|
||||
std::make_format_args(pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED),
|
||||
std::make_format_args(path)));
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED), std::make_format_args(pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(path)));
|
||||
std::make_format_args(pathString)));
|
||||
}
|
||||
}
|
||||
else if (type == render::SPRITESHEET)
|
||||
@@ -89,14 +202,14 @@ namespace anm2ed::imgui
|
||||
auto& rows = settings.renderRows;
|
||||
auto& columns = settings.renderColumns;
|
||||
|
||||
if (renderFrames.empty())
|
||||
if (renderTempFrames.empty())
|
||||
{
|
||||
toasts.push(localize.get(TOAST_SPRITESHEET_NO_FRAMES));
|
||||
logger.warning(localize.get(TOAST_SPRITESHEET_NO_FRAMES, anm2ed::ENGLISH));
|
||||
}
|
||||
else
|
||||
{
|
||||
auto& firstFrame = renderFrames.front();
|
||||
auto firstFrame = Texture(renderTempFrames.front());
|
||||
if (firstFrame.size.x <= 0 || firstFrame.size.y <= 0 || firstFrame.pixels.empty())
|
||||
{
|
||||
toasts.push(localize.get(TOAST_SPRITESHEET_EMPTY));
|
||||
@@ -110,9 +223,9 @@ namespace anm2ed::imgui
|
||||
|
||||
std::vector<uint8_t> spritesheet((size_t)(spritesheetSize.x) * spritesheetSize.y * CHANNELS);
|
||||
|
||||
for (std::size_t index = 0; index < renderFrames.size(); ++index)
|
||||
for (std::size_t index = 0; index < renderTempFrames.size(); ++index)
|
||||
{
|
||||
const auto& frame = renderFrames[index];
|
||||
auto frame = Texture(renderTempFrames[index]);
|
||||
auto row = (int)(index / columns);
|
||||
auto column = (int)(index % columns);
|
||||
if (row >= rows || column >= columns) break;
|
||||
@@ -131,42 +244,66 @@ namespace anm2ed::imgui
|
||||
Texture spritesheetTexture(spritesheet.data(), spritesheetSize);
|
||||
if (spritesheetTexture.write_png(path))
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET), std::make_format_args(path)));
|
||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET), std::make_format_args(pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(path)));
|
||||
std::make_format_args(pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED),
|
||||
std::make_format_args(path)));
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED), std::make_format_args(pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(path)));
|
||||
std::make_format_args(pathString)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (animation_render(ffmpegPath, path, renderFrames, audioStream, (render::Type)type, size, anm2.info.fps))
|
||||
if (settings.timelineIsSound && type != render::GIF)
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(path)));
|
||||
if (!render_audio_stream_generate(audioStream, anm2.content.sounds, renderFrameSoundIDs, anm2.info.fps))
|
||||
{
|
||||
toasts.push(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED));
|
||||
logger.error("Failed to generate deterministic render audio stream; exporting without audio.");
|
||||
audioStream.stream.clear();
|
||||
}
|
||||
}
|
||||
else
|
||||
audioStream.stream.clear();
|
||||
|
||||
if (animation_render(ffmpegPath, path, renderTempFrames, renderTempFrameDurations, audioStream,
|
||||
(render::Type)type, anm2.info.fps))
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION, anm2ed::ENGLISH),
|
||||
std::make_format_args(path)));
|
||||
std::make_format_args(pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED),
|
||||
std::make_format_args(path)));
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(path)));
|
||||
std::make_format_args(pathString)));
|
||||
}
|
||||
}
|
||||
|
||||
renderFrames.clear();
|
||||
if (type == render::PNGS)
|
||||
{
|
||||
renderTempDirectory.clear();
|
||||
renderTempFrames.clear();
|
||||
renderTempFrameDurations.clear();
|
||||
renderFrameSoundIDs.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
||||
renderTempFrameDurations.clear();
|
||||
renderFrameSoundIDs.clear();
|
||||
}
|
||||
|
||||
if (settings.renderIsRawAnimation)
|
||||
{
|
||||
|
||||
settings = savedSettings;
|
||||
|
||||
pan = savedPan;
|
||||
@@ -177,8 +314,6 @@ namespace anm2ed::imgui
|
||||
isCheckerPanInitialized = false;
|
||||
}
|
||||
|
||||
if (settings.timelineIsSound) audioStream.capture_end(mixer);
|
||||
|
||||
playback.isPlaying = false;
|
||||
playback.isFinished = false;
|
||||
manager.isRecording = false;
|
||||
@@ -186,10 +321,70 @@ namespace anm2ed::imgui
|
||||
}
|
||||
else
|
||||
{
|
||||
if (settings.timelineIsSound && renderTempFrames.empty()) audioStream.capture_begin(mixer);
|
||||
auto frameSoundID = -1;
|
||||
if (settings.timelineIsSound && !anm2.content.sounds.empty())
|
||||
{
|
||||
if (auto animation = document.animation_get();
|
||||
animation && animation->triggers.isVisible && (!settings.timelineIsOnlyShowLayers || manager.isRecording))
|
||||
{
|
||||
if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible)
|
||||
{
|
||||
if (!trigger.soundIDs.empty())
|
||||
{
|
||||
auto soundIndex = trigger.soundIDs.size() > 1
|
||||
? (size_t)math::random_in_range(0.0f, (float)trigger.soundIDs.size())
|
||||
: (size_t)0;
|
||||
soundIndex = std::min(soundIndex, trigger.soundIDs.size() - 1);
|
||||
auto soundID = trigger.soundIDs[soundIndex];
|
||||
if (anm2.content.sounds.contains(soundID)) frameSoundID = soundID;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
renderFrameSoundIDs.push_back(frameSoundID);
|
||||
|
||||
bind();
|
||||
auto pixels = pixels_get();
|
||||
renderFrames.push_back(Texture(pixels.data(), size));
|
||||
if (settings.renderIsRawAnimation) pixels_unpremultiply_alpha(pixels);
|
||||
auto frameIndex = (int)renderTempFrames.size();
|
||||
auto framePath = renderTempDirectory / render_frame_filename(settings.renderFormat, frameIndex);
|
||||
if (Texture::write_pixels_png(framePath, size, pixels.data()))
|
||||
{
|
||||
renderTempFrames.push_back(framePath);
|
||||
auto nowCounter = SDL_GetPerformanceCounter();
|
||||
auto counterFrequency = SDL_GetPerformanceFrequency();
|
||||
auto fallbackDuration = 1.0 / (double)std::max(anm2.info.fps, 1);
|
||||
|
||||
if (renderTempFrames.size() == 1)
|
||||
{
|
||||
renderCaptureCounterPrev = nowCounter;
|
||||
renderTempFrameDurations.push_back(fallbackDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto elapsedCounter = nowCounter - renderCaptureCounterPrev;
|
||||
auto frameDuration = counterFrequency > 0 ? (double)elapsedCounter / (double)counterFrequency : 0.0;
|
||||
frameDuration = std::max(frameDuration, 1.0 / 1000.0);
|
||||
renderTempFrameDurations.back() = frameDuration;
|
||||
renderTempFrameDurations.push_back(frameDuration);
|
||||
renderCaptureCounterPrev = nowCounter;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(pathString)));
|
||||
if (type != render::PNGS) render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
||||
renderTempFrameDurations.clear();
|
||||
renderFrameSoundIDs.clear();
|
||||
playback.isPlaying = false;
|
||||
playback.isFinished = false;
|
||||
manager.isRecording = false;
|
||||
manager.progressPopup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,22 +394,37 @@ namespace anm2ed::imgui
|
||||
auto& isSound = settings.timelineIsSound;
|
||||
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
|
||||
|
||||
if (!anm2.content.sounds.empty() && isSound)
|
||||
if (!manager.isRecording && !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);
|
||||
{
|
||||
if (!trigger.soundIDs.empty())
|
||||
{
|
||||
auto soundIndex = trigger.soundIDs.size() > 1
|
||||
? (size_t)math::random_in_range(0.0f, (float)trigger.soundIDs.size())
|
||||
: (size_t)0;
|
||||
soundIndex = std::min(soundIndex, trigger.soundIDs.size() - 1);
|
||||
auto soundID = trigger.soundIDs[soundIndex];
|
||||
|
||||
if (anm2.content.sounds.contains(soundID)) anm2.content.sounds[soundID].audio.play(false, mixer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playback.tick(anm2.info.fps, animation->frameNum,
|
||||
(animation->isLoop || settings.playbackIsLoop) && !manager.isRecording);
|
||||
auto fps = std::max(anm2.info.fps, 1);
|
||||
auto deltaSeconds = manager.isRecording ? (1.0f / (float)fps) : (1.0f / PLAYBACK_TICK_RATE);
|
||||
playback.tick(fps, animation->frameNum, (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording,
|
||||
deltaSeconds);
|
||||
|
||||
frameTime = playback.time;
|
||||
}
|
||||
|
||||
if (wasPlaybackPlaying && !playback.isPlaying) stop_all_sounds();
|
||||
wasPlaybackPlaying = playback.isPlaying;
|
||||
}
|
||||
|
||||
void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources)
|
||||
@@ -247,6 +457,7 @@ namespace anm2ed::imgui
|
||||
auto& shaderAxes = resources.shaders[shader::AXIS];
|
||||
auto& shaderGrid = resources.shaders[shader::GRID];
|
||||
auto& shaderTexture = resources.shaders[shader::TEXTURE];
|
||||
auto& frames = document.frames.selection;
|
||||
|
||||
auto reset_checker_pan = [&]()
|
||||
{
|
||||
@@ -277,8 +488,28 @@ namespace anm2ed::imgui
|
||||
|
||||
auto center_view = [&]() { pan = vec2(); };
|
||||
|
||||
auto fit_view = [&]()
|
||||
{
|
||||
if (animation) set_to_rect(zoom, pan, anm2.animation_rect(*animation, isRootTransform));
|
||||
};
|
||||
|
||||
auto zoom_adjust = [&](float delta)
|
||||
{
|
||||
auto focus = position_translate(zoom, pan, size * 0.5f);
|
||||
auto previousZoom = zoom;
|
||||
zoom_set(zoom, pan, focus, delta);
|
||||
if (zoom != previousZoom) hasPendingZoomPanAdjust = true;
|
||||
};
|
||||
|
||||
auto zoom_in = [&]() { zoom_adjust(zoomStep); };
|
||||
auto zoom_out = [&]() { zoom_adjust(-zoomStep); };
|
||||
|
||||
manager.isAbleToRecord = false;
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_ANIMATION_PREVIEW_WINDOW), &settings.windowIsAnimationPreview))
|
||||
{
|
||||
manager.isAbleToRecord = true;
|
||||
|
||||
auto childSize = ImVec2(row_widget_width_get(4),
|
||||
(ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2));
|
||||
|
||||
@@ -308,14 +539,13 @@ namespace anm2ed::imgui
|
||||
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();
|
||||
if (ImGui::Button(localize.get(LABEL_CENTER_VIEW), widgetSize)) center_view();
|
||||
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));
|
||||
if (ImGui::Button(localize.get(LABEL_FIT), widgetSize)) fit_view();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_FIT), settings.shortcutFit);
|
||||
|
||||
auto mousePosInt = ivec2(mousePos);
|
||||
@@ -342,7 +572,7 @@ namespace anm2ed::imgui
|
||||
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::DragFloat(localize.get(BASIC_ALPHA), &overlayTransparency, DRAG_SPEED, 0, 255, "%.0f");
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERLAY_ALPHA));
|
||||
}
|
||||
ImGui::EndChild();
|
||||
@@ -383,13 +613,12 @@ namespace anm2ed::imgui
|
||||
{
|
||||
savedSettings = settings;
|
||||
|
||||
if (settings.timelineIsSound) audioStream.capture_begin(mixer);
|
||||
|
||||
if (settings.renderIsRawAnimation)
|
||||
{
|
||||
settings.previewBackgroundColor = vec4();
|
||||
settings.previewIsGrid = false;
|
||||
settings.previewIsAxes = false;
|
||||
settings.previewIsPivots = false;
|
||||
settings.previewIsBorder = false;
|
||||
settings.timelineIsOnlyShowLayers = true;
|
||||
settings.onionskinIsEnabled = false;
|
||||
@@ -398,7 +627,7 @@ namespace anm2ed::imgui
|
||||
savedZoom = zoom;
|
||||
savedPan = pan;
|
||||
|
||||
if (auto rect = document.animation_get()->rect(isRootTransform); rect != vec4(-1.0f))
|
||||
if (auto rect = anm2.animation_rect(*document.animation_get(), isRootTransform); rect != vec4(-1.0f))
|
||||
{
|
||||
size_set(vec2(rect.z, rect.w) * settings.renderScale);
|
||||
set_to_rect(zoom, pan, rect);
|
||||
@@ -409,7 +638,38 @@ namespace anm2ed::imgui
|
||||
|
||||
manager.isRecordingStart = false;
|
||||
manager.isRecording = true;
|
||||
renderTempFrames.clear();
|
||||
renderTempFrameDurations.clear();
|
||||
renderFrameSoundIDs.clear();
|
||||
renderCaptureCounterPrev = 0;
|
||||
if (settings.renderType == render::PNGS)
|
||||
{
|
||||
renderTempDirectory = settings.renderPath;
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(renderTempDirectory, ec);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto destinationDirectory = render_destination_directory(settings.renderPath, settings.renderType);
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(destinationDirectory, ec);
|
||||
renderTempDirectory = render_temp_directory_create(destinationDirectory);
|
||||
}
|
||||
if (renderTempDirectory.empty())
|
||||
{
|
||||
auto pathString = path::to_utf8(settings.renderPath);
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(pathString)));
|
||||
manager.isRecording = false;
|
||||
manager.progressPopup.close();
|
||||
playback.isPlaying = false;
|
||||
playback.isFinished = false;
|
||||
return;
|
||||
}
|
||||
playback.isPlaying = true;
|
||||
playback.timing_reset();
|
||||
playback.time = manager.recordingStart;
|
||||
}
|
||||
|
||||
@@ -417,7 +677,7 @@ namespace anm2ed::imgui
|
||||
|
||||
bind();
|
||||
viewport_set();
|
||||
clear(manager.isRecording && settings.renderIsRawAnimation ? vec4() : vec4(backgroundColor, 1.0f));
|
||||
clear(manager.isRecording && settings.renderIsRawAnimation ? vec4(0) : vec4(backgroundColor, 1.0f));
|
||||
|
||||
if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor);
|
||||
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
|
||||
@@ -519,9 +779,11 @@ namespace anm2ed::imgui
|
||||
|
||||
for (auto& id : animation->layerOrder)
|
||||
{
|
||||
if (!animation->layerAnimations.contains(id)) continue;
|
||||
auto& layerAnimation = animation->layerAnimations[id];
|
||||
if (!layerAnimation.isVisible) continue;
|
||||
|
||||
if (!anm2.content.layers.contains(id)) continue;
|
||||
auto& layer = anm2.content.layers.at(id);
|
||||
|
||||
auto spritesheet = anm2.spritesheet_get(layer.spritesheetID);
|
||||
@@ -534,22 +796,33 @@ namespace anm2ed::imgui
|
||||
{
|
||||
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;
|
||||
frame = anm2.frame_effective(id, frame);
|
||||
auto crop = frame.crop;
|
||||
auto size = frame.size;
|
||||
auto pivot = frame.pivot;
|
||||
|
||||
auto layerModel =
|
||||
math::quad_model_get(size, frame.position, pivot, math::percent_to_unit(frame.scale), frame.rotation);
|
||||
auto layerTransform = sampleTransform * layerModel;
|
||||
|
||||
auto uvMin = crop / texSize;
|
||||
auto uvMax = (crop + size) / texSize;
|
||||
|
||||
vec3 frameColorOffset = frame.colorOffset + colorOffset + sampleColor;
|
||||
vec4 frameTint = frame.tint;
|
||||
|
||||
if (isRootTransform)
|
||||
{
|
||||
auto rootFrame = animation->rootAnimation.frame_generate(sampleTime, anm2::ROOT);
|
||||
frameColorOffset += rootFrame.colorOffset;
|
||||
frameTint *= rootFrame.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());
|
||||
@@ -606,8 +879,8 @@ namespace anm2ed::imgui
|
||||
|
||||
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 rectModel =
|
||||
math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
|
||||
auto rectTransform = sampleTransform * rectModel;
|
||||
|
||||
rect_render(shaderLine, rectTransform, rectModel, color);
|
||||
@@ -640,8 +913,11 @@ namespace anm2ed::imgui
|
||||
|
||||
unbind();
|
||||
|
||||
sync_checker_pan();
|
||||
render_checker_background(ImGui::GetWindowDrawList(), min, max, -size - checkerPan, CHECKER_SIZE);
|
||||
if (manager.isRecording && settings.renderIsRawAnimation)
|
||||
{
|
||||
sync_checker_pan();
|
||||
render_checker_background(ImGui::GetWindowDrawList(), min, max, -size - checkerPan, CHECKER_SIZE);
|
||||
}
|
||||
ImGui::Image(texture, to_imvec2(size));
|
||||
|
||||
isPreviewHovered = ImGui::IsItemHovered();
|
||||
@@ -659,8 +935,9 @@ namespace anm2ed::imgui
|
||||
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());
|
||||
if (anm2.content.events.contains(trigger.eventID))
|
||||
drawList->AddText(textPos, ImGui::GetColorU32(triggerTextColor),
|
||||
anm2.content.events.at(trigger.eventID).name.c_str());
|
||||
ImGui::PopFont();
|
||||
drawList->PopClipRect();
|
||||
}
|
||||
@@ -707,17 +984,25 @@ namespace anm2ed::imgui
|
||||
auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift);
|
||||
|
||||
auto frame = document.frame_get();
|
||||
auto item = document.item_get();
|
||||
auto useTool = tool;
|
||||
auto step = isMod ? canvas::STEP_FAST : canvas::STEP;
|
||||
auto step = isMod ? STEP_FAST : 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;
|
||||
auto frame_change_apply = [&](anm2::FrameChange frameChange, anm2::ChangeType changeType = anm2::ADJUST)
|
||||
{ item->frames_change(frameChange, reference.itemType, changeType, frames); };
|
||||
|
||||
auto& toolInfo = tool::INFO[useTool];
|
||||
auto& areaType = toolInfo.areaType;
|
||||
bool isAreaAllowed = areaType == tool::ALL || areaType == tool::ANIMATION_PREVIEW;
|
||||
bool isFrameRequired =
|
||||
!(useTool == tool::PAN || useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER);
|
||||
bool isFrameAvailable = !isFrameRequired || frame;
|
||||
auto cursor = (isAreaAllowed && isFrameAvailable) ? toolInfo.cursor : ImGuiMouseCursor_NotAllowed;
|
||||
ImGui::SetMouseCursor(cursor);
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
if (useTool != tool::MOVE) isMoveDragging = false;
|
||||
@@ -727,7 +1012,7 @@ namespace anm2ed::imgui
|
||||
if (isMouseDown || isMouseMiddleDown) pan += vec2(mouseDelta.x, mouseDelta.y);
|
||||
break;
|
||||
case tool::MOVE:
|
||||
if (!frame) break;
|
||||
if (!item || frames.empty()) break;
|
||||
if (isBegin)
|
||||
{
|
||||
document.snapshot(localize.get(EDIT_FRAME_POSITION));
|
||||
@@ -737,11 +1022,15 @@ namespace anm2ed::imgui
|
||||
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 (isMouseDown && isMoveDragging)
|
||||
frame_change_apply(
|
||||
{.positionX = (int)(mousePos.x - moveOffset.x), .positionY = (int)(mousePos.y - moveOffset.y)});
|
||||
|
||||
if (isLeftPressed) frame_change_apply({.positionX = step}, anm2::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.positionX = step}, anm2::ADD);
|
||||
if (isUpPressed) frame_change_apply({.positionY = step}, anm2::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.positionY = step}, anm2::ADD);
|
||||
|
||||
if (isMouseReleased) isMoveDragging = false;
|
||||
if (isEnd) document.change(Document::FRAMES);
|
||||
if (isDuring)
|
||||
@@ -756,17 +1045,19 @@ namespace anm2ed::imgui
|
||||
}
|
||||
break;
|
||||
case tool::SCALE:
|
||||
if (!frame) break;
|
||||
if (!item || frames.empty()) 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};
|
||||
frame_change_apply({.scaleX = (int)frame->scale.x, .scaleY = (int)frame->scale.y});
|
||||
}
|
||||
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 (isLeftPressed) frame_change_apply({.scaleX = step}, anm2::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.scaleX = step}, anm2::ADD);
|
||||
if (isUpPressed) frame_change_apply({.scaleY = step}, anm2::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.scaleY = step}, anm2::ADD);
|
||||
|
||||
if (isDuring)
|
||||
{
|
||||
@@ -782,11 +1073,11 @@ namespace anm2ed::imgui
|
||||
if (isEnd) document.change(Document::FRAMES);
|
||||
break;
|
||||
case tool::ROTATE:
|
||||
if (!frame) break;
|
||||
if (!item || frames.empty()) 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 (isMouseDown) frame_change_apply({.rotation = (int)mouseDelta.x}, anm2::ADD);
|
||||
if (isLeftPressed || isDownPressed) frame_change_apply({.rotation = step}, anm2::SUBTRACT);
|
||||
if (isUpPressed || isRightPressed) frame_change_apply({.rotation = step}, anm2::ADD);
|
||||
|
||||
if (isDuring)
|
||||
{
|
||||
@@ -804,6 +1095,26 @@ namespace anm2ed::imgui
|
||||
break;
|
||||
}
|
||||
|
||||
if ((isMouseDown || isKeyDown) && useTool != tool::PAN)
|
||||
{
|
||||
if (!isAreaAllowed && areaType == tool::SPRITESHEET_EDITOR)
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
ImGui::TextUnformatted(localize.get(TEXT_TOOL_SPRITESHEET_EDITOR));
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
else if (isFrameRequired && !isFrameAvailable)
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
ImGui::TextUnformatted(localize.get(TEXT_SELECT_FRAME));
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseWheel != 0 || isZoomIn || isZoomOut)
|
||||
{
|
||||
auto previousZoom = zoom;
|
||||
@@ -812,14 +1123,34 @@ namespace anm2ed::imgui
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (tool == tool::PAN && ImGui::BeginPopupContextWindow("##Animation Preview Context Menu", ImGuiMouseButton_Right))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_CENTER_VIEW), settings.shortcutCenterView.c_str())) center_view();
|
||||
if (ImGui::MenuItem(localize.get(LABEL_FIT), settings.shortcutFit.c_str(), false, animation)) fit_view();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_IN), settings.shortcutZoomIn.c_str())) zoom_in();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_OUT), settings.shortcutZoomOut.c_str())) zoom_out();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -828,9 +1159,20 @@ namespace anm2ed::imgui
|
||||
|
||||
ImGui::TextUnformatted(localize.get(TEXT_RECORDING_PROGRESS));
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
||||
{
|
||||
renderFrames.clear();
|
||||
if (settings.renderType == render::PNGS)
|
||||
{
|
||||
renderTempDirectory.clear();
|
||||
renderTempFrames.clear();
|
||||
renderTempFrameDurations.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
||||
renderTempFrameDurations.clear();
|
||||
}
|
||||
|
||||
pan = savedPan;
|
||||
zoom = savedZoom;
|
||||
@@ -860,5 +1202,6 @@ namespace anm2ed::imgui
|
||||
}
|
||||
|
||||
settings.previewStartZoom = zoom;
|
||||
ImGui::End();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "audio_stream.h"
|
||||
#include "canvas.h"
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include <filesystem>
|
||||
|
||||
#include "audio_stream.hpp"
|
||||
#include "canvas.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -12,6 +14,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
|
||||
AudioStream audioStream = AudioStream(mixer);
|
||||
bool wasPlaybackPlaying{};
|
||||
bool isPreviewHovered{};
|
||||
bool isSizeTrySet{true};
|
||||
Settings savedSettings{};
|
||||
@@ -26,7 +29,11 @@ namespace anm2ed::imgui
|
||||
bool hasPendingZoomPanAdjust{};
|
||||
bool isMoveDragging{};
|
||||
glm::vec2 moveOffset{};
|
||||
std::vector<resource::Texture> renderFrames{};
|
||||
std::filesystem::path renderTempDirectory{};
|
||||
std::vector<std::filesystem::path> renderTempFrames{};
|
||||
std::vector<double> renderTempFrameDurations{};
|
||||
std::vector<int> renderFrameSoundIDs{};
|
||||
Uint64 renderCaptureCounterPrev{};
|
||||
|
||||
public:
|
||||
AnimationPreview();
|
||||
+277
-212
@@ -1,13 +1,12 @@
|
||||
#include "animations.h"
|
||||
#include "animations.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "log.h"
|
||||
#include "strings.h"
|
||||
#include "toast.h"
|
||||
#include "vector_.h"
|
||||
#include "log.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "vector_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::resource;
|
||||
@@ -20,27 +19,193 @@ namespace anm2ed::imgui
|
||||
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 rename_format_get = [&](int index)
|
||||
{ return std::format("###Document #{} Animation #{}", manager.selected, index); };
|
||||
|
||||
auto animations_remove = [&]()
|
||||
auto rename = [&]()
|
||||
{
|
||||
if (!selection.empty())
|
||||
if (!selection.empty()) renameQueued = *selection.begin();
|
||||
};
|
||||
|
||||
auto add = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto it = selection.rbegin(); it != selection.rend(); ++it)
|
||||
anm2::Animation animation;
|
||||
animation.name = localize.get(TEXT_NEW_ANIMATION);
|
||||
if (anm2::Animation* referenceAnimation = document.animation_get())
|
||||
{
|
||||
auto i = *it;
|
||||
if (overlayIndex == i) overlayIndex = -1;
|
||||
if (reference.animationIndex == i) reference.animationIndex = -1;
|
||||
anm2.animations.items.erase(anm2.animations.items.begin() + i);
|
||||
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();
|
||||
}
|
||||
selection.clear();
|
||||
}
|
||||
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, behavior());
|
||||
};
|
||||
|
||||
auto remove = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_ANIMATIONS), Document::ANIMATIONS, behavior());
|
||||
};
|
||||
|
||||
auto duplicate = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
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_REMOVE_ANIMATIONS), Document::ANIMATIONS, behavior());
|
||||
};
|
||||
|
||||
auto merge = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
if (mergeSelection.contains(overlayIndex)) overlayIndex = -1;
|
||||
auto merged = anm2.animations_merge(mergeReference, mergeSelection, (merge::Type)settings.mergeType,
|
||||
settings.mergeIsDeleteAnimationsAfter);
|
||||
|
||||
selection = {merged};
|
||||
reference = {merged};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_MERGE_ANIMATIONS), Document::ANIMATIONS, behavior());
|
||||
};
|
||||
|
||||
auto merge_popup_open = [&]()
|
||||
{
|
||||
mergePopup.open();
|
||||
mergeSelection.clear();
|
||||
mergeReference = *selection.begin();
|
||||
};
|
||||
|
||||
auto merge_quick = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
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, behavior());
|
||||
};
|
||||
|
||||
auto default_set = [&]()
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_DEFAULT_ANIMATION), Document::ANIMATIONS,
|
||||
anm2.animations.defaultAnimation = anm2.animations.items[*selection.begin()].name);
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& i : selection)
|
||||
clipboardText += anm2.animations.items[i].to_string();
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto cut = [&]()
|
||||
{
|
||||
copy();
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_CUT_ANIMATIONS), Document::ANIMATIONS, remove());
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto clipboardText = clipboard.get();
|
||||
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))
|
||||
{
|
||||
if (!indices.empty())
|
||||
{
|
||||
auto index = *indices.rbegin();
|
||||
selection = {index};
|
||||
reference = {index};
|
||||
newAnimationSelectedIndex = index;
|
||||
}
|
||||
}
|
||||
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, behavior());
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_ANIMATIONS_WINDOW), &settings.windowIsAnimations))
|
||||
@@ -52,6 +217,7 @@ namespace anm2ed::imgui
|
||||
|
||||
if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
|
||||
selection.start(anm2.animations.items.size());
|
||||
|
||||
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
|
||||
@@ -70,9 +236,13 @@ namespace anm2ed::imgui
|
||||
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))
|
||||
if (isNewAnimation || renameQueued == i)
|
||||
{
|
||||
renameState = RENAME_FORCE_EDIT;
|
||||
renameQueued = -1;
|
||||
}
|
||||
if (selectable_input_text(animation.name, rename_format_get(i), animation.name, selection.contains((int)i),
|
||||
ImGuiSelectableFlags_None, renameState))
|
||||
{
|
||||
reference = {(int)i};
|
||||
document.frames.clear();
|
||||
@@ -85,15 +255,20 @@ namespace anm2ed::imgui
|
||||
document.change(Document::ANIMATIONS);
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) hovered = (int)i;
|
||||
|
||||
if (isNewAnimation)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
isUpdateScroll = true;
|
||||
newAnimationSelectedIndex = -1;
|
||||
}
|
||||
ImGui::PopFont();
|
||||
|
||||
if (isUpdateScroll && isReferenced)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
isUpdateScroll = false;
|
||||
}
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
@@ -145,68 +320,42 @@ namespace anm2ed::imgui
|
||||
|
||||
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_RENAME], shortcut::FOCUSED)) rename();
|
||||
if (shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED)) merge_quick();
|
||||
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))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_RENAME), settings.shortcutRename.c_str(), false,
|
||||
selection.size() == 1))
|
||||
rename();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ADD), settings.shortcutAdd.c_str())) add();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_DUPLICATE), settings.shortcutDuplicate.c_str())) duplicate();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_MERGE), settings.shortcutMerge.c_str())) merge_quick();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REMOVE), settings.shortcutRemove.c_str())) remove();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_DEFAULT), settings.shortcutDefault.c_str(), false,
|
||||
selection.size() == 1))
|
||||
default_set();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false, !selection.empty())) cut();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty()))
|
||||
copy();
|
||||
}
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
{
|
||||
paste();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
@@ -215,132 +364,45 @@ namespace anm2ed::imgui
|
||||
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());
|
||||
}
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) 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);
|
||||
}
|
||||
shortcut(manager.chords[SHORTCUT_DUPLICATE]);
|
||||
if (ImGui::Button(localize.get(BASIC_DUPLICATE), widgetSize)) duplicate();
|
||||
ImGui::EndDisabled();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_DUPLICATE_ANIMATION), settings.shortcutDuplicate);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.size() != 1);
|
||||
if (ImGui::Button(localize.get(LABEL_MERGE), widgetSize)) merge_popup_open();
|
||||
ImGui::EndDisabled();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_OPEN_MERGE_POPUP), settings.shortcutMerge);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.empty());
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize)) remove();
|
||||
ImGui::EndDisabled();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_ANIMATION), settings.shortcutRemove);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.size() != 1);
|
||||
shortcut(manager.chords[SHORTCUT_DEFAULT]);
|
||||
if (ImGui::Button(localize.get(BASIC_DEFAULT), widgetSize)) default_set();
|
||||
ImGui::EndDisabled();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_SET_DEFAULT_ANIMATION), settings.shortcutDefault);
|
||||
|
||||
mergePopup.trigger();
|
||||
|
||||
if (ImGui::BeginPopupModal(mergePopup.label(), &mergePopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
auto merge_close = [&]()
|
||||
auto close = [&]()
|
||||
{
|
||||
mergeSelection.clear();
|
||||
mergePopup.close();
|
||||
@@ -360,16 +422,16 @@ namespace anm2ed::imgui
|
||||
{
|
||||
mergeSelection.start(anm2.animations.items.size());
|
||||
|
||||
for (std::size_t index = 0; index < anm2.animations.items.size(); ++index)
|
||||
for (int i = 0; i < (int)anm2.animations.items.size(); i++)
|
||||
{
|
||||
if ((int)index == mergeReference) continue;
|
||||
if (i == mergeReference) continue;
|
||||
|
||||
auto& animation = anm2.animations.items[index];
|
||||
auto& animation = anm2.animations.items[i];
|
||||
|
||||
ImGui::PushID((int)index);
|
||||
ImGui::PushID(i);
|
||||
|
||||
ImGui::SetNextItemSelectionUserData((int)index);
|
||||
ImGui::Selectable(animation.name.c_str(), mergeSelection.contains((int)index));
|
||||
ImGui::SetNextItemSelectionUserData(i);
|
||||
ImGui::Selectable(animation.name.c_str(), mergeSelection.contains(i));
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -407,30 +469,33 @@ namespace anm2ed::imgui
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
ImGui::BeginDisabled(mergeSelection.empty());
|
||||
if (ImGui::Button(localize.get(LABEL_MERGE), widgetSize))
|
||||
{
|
||||
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();
|
||||
}
|
||||
merge();
|
||||
close();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) merge_close();
|
||||
if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
auto isNextAnimation = shortcut(manager.chords[SHORTCUT_NEXT_ANIMATION], shortcut::GLOBAL);
|
||||
auto isPreviousAnimation = shortcut(manager.chords[SHORTCUT_PREVIOUS_ANIMATION], shortcut::GLOBAL);
|
||||
|
||||
if ((isPreviousAnimation || isNextAnimation) && !anm2.animations.items.empty())
|
||||
{
|
||||
if (isPreviousAnimation)
|
||||
reference.animationIndex = glm::clamp(--reference.animationIndex, 0, (int)anm2.animations.items.size() - 1);
|
||||
if (isNextAnimation)
|
||||
reference.animationIndex = glm::clamp(++reference.animationIndex, 0, (int)anm2.animations.items.size() - 1);
|
||||
|
||||
selection = {reference.animationIndex};
|
||||
isUpdateScroll = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include "strings.h"
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -12,6 +12,9 @@ namespace anm2ed::imgui
|
||||
{
|
||||
PopupHelper mergePopup{PopupHelper(LABEL_ANIMATIONS_MERGE_POPUP)};
|
||||
int newAnimationSelectedIndex{-1};
|
||||
int renameQueued{-1};
|
||||
bool isInContextMenu{};
|
||||
bool isUpdateScroll{};
|
||||
RenameState renameState{RENAME_SELECTABLE};
|
||||
|
||||
public:
|
||||
+125
-77
@@ -1,11 +1,12 @@
|
||||
#include "events.h"
|
||||
#include "events.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "log.h"
|
||||
#include "map_.h"
|
||||
#include "strings.h"
|
||||
#include "toast.h"
|
||||
#include "log.hpp"
|
||||
#include "map_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::resource;
|
||||
@@ -17,12 +18,94 @@ namespace anm2ed::imgui
|
||||
{
|
||||
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;
|
||||
auto rename_format_get = [&](int id) { return std::format("###Document #{} Event #{}", manager.selected, id); };
|
||||
auto rename = [&]()
|
||||
{
|
||||
if (!selection.empty()) renameQueued = *selection.begin();
|
||||
};
|
||||
|
||||
auto add = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto id = map::next_id_get(anm2.content.events);
|
||||
anm2::Event event{};
|
||||
event.name = localize.get(TEXT_NEW_EVENT);
|
||||
anm2.content.events[id] = event;
|
||||
selection = {id};
|
||||
reference = {id};
|
||||
newEventId = id;
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_EVENT), Document::EVENTS, behavior());
|
||||
};
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.events_unused();
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
{
|
||||
for (auto& animation : anm2.animations.items)
|
||||
for (auto& trigger : animation.triggers.frames)
|
||||
if (trigger.eventID == id) trigger.eventID = -1;
|
||||
|
||||
anm2.content.events.erase(id);
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_EVENTS), Document::EVENTS, behavior());
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.events[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxEventIdBefore = anm2.content.events.empty() ? -1 : anm2.content.events.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_EVENTS));
|
||||
if (anm2.events_deserialize(clipboard.get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.events.empty())
|
||||
{
|
||||
auto maxEventIdAfter = anm2.content.events.rbegin()->first;
|
||||
if (maxEventIdAfter > maxEventIdBefore)
|
||||
{
|
||||
newEventId = maxEventIdAfter;
|
||||
selection = {maxEventIdAfter};
|
||||
reference = maxEventIdAfter;
|
||||
}
|
||||
}
|
||||
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)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_EVENTS), Document::EVENTS, behavior());
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_EVENTS_WINDOW), &settings.windowIsEvents))
|
||||
{
|
||||
@@ -38,16 +121,19 @@ namespace anm2ed::imgui
|
||||
|
||||
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 (isNewEvent || renameQueued == id)
|
||||
{
|
||||
renameState = RENAME_FORCE_EDIT;
|
||||
renameQueued = -1;
|
||||
}
|
||||
if (selectable_input_text(event.name, rename_format_get(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)
|
||||
{
|
||||
@@ -68,51 +154,37 @@ namespace anm2ed::imgui
|
||||
|
||||
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_RENAME], shortcut::FOCUSED)) rename();
|
||||
if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add();
|
||||
if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
|
||||
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))
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_RENAME), settings.shortcutRename.c_str(), false,
|
||||
selection.size() == 1))
|
||||
rename();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ADD), settings.shortcutAdd.c_str())) add();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty()))
|
||||
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();
|
||||
}
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
@@ -122,36 +194,12 @@ namespace anm2ed::imgui
|
||||
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());
|
||||
}
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) 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();
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_EVENTS), settings.shortcutRemove);
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Events
|
||||
{
|
||||
int newEventId{-1};
|
||||
int renameQueued{-1};
|
||||
RenameState renameState{RENAME_SELECTABLE};
|
||||
|
||||
public:
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "frame_properties.h"
|
||||
#include "frame_properties.hpp"
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <limits>
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
|
||||
#include "math_.h"
|
||||
#include "strings.h"
|
||||
#include "types.h"
|
||||
#include "math_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
using namespace anm2ed::util::math;
|
||||
using namespace anm2ed::types;
|
||||
@@ -19,238 +22,276 @@ namespace anm2ed::imgui
|
||||
auto& document = *manager.get();
|
||||
auto& frames = document.frames.selection;
|
||||
auto& type = document.reference.itemType;
|
||||
auto regionLabelsString = std::vector<std::string>{localize.get(BASIC_NONE)};
|
||||
auto regionLabels = std::vector<const char*>{regionLabelsString[0].c_str()};
|
||||
auto regionIds = std::vector<int>{-1};
|
||||
auto interpolationLabelsString =
|
||||
std::vector<std::string>{localize.get(BASIC_NONE), localize.get(BASIC_LINEAR), localize.get(BASIC_EASE_IN),
|
||||
localize.get(BASIC_EASE_OUT), localize.get(BASIC_EASE_IN_OUT)};
|
||||
auto interpolationLabels =
|
||||
std::vector<const char*>{interpolationLabelsString[0].c_str(), interpolationLabelsString[1].c_str(),
|
||||
interpolationLabelsString[2].c_str(), interpolationLabelsString[3].c_str(),
|
||||
interpolationLabelsString[4].c_str()};
|
||||
auto interpolationValues =
|
||||
std::vector<int>{anm2::Frame::Interpolation::NONE, anm2::Frame::Interpolation::LINEAR,
|
||||
anm2::Frame::Interpolation::EASE_IN, anm2::Frame::Interpolation::EASE_OUT,
|
||||
anm2::Frame::Interpolation::EASE_IN_OUT};
|
||||
|
||||
if (type == anm2::LAYER && document.reference.itemID != -1)
|
||||
{
|
||||
auto spritesheetID = document.anm2.content.layers.at(document.reference.itemID).spritesheetID;
|
||||
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
|
||||
if (regionIt != document.regionBySpritesheet.end() && !regionIt->second.ids.empty() &&
|
||||
!regionIt->second.labels.empty())
|
||||
{
|
||||
regionLabels = regionIt->second.labels;
|
||||
regionIds = regionIt->second.ids;
|
||||
}
|
||||
}
|
||||
|
||||
if (frames.size() <= 1)
|
||||
{
|
||||
auto frame = document.frame_get();
|
||||
auto useFrame = frame ? *frame : anm2::Frame();
|
||||
auto displayFrame = frame && type == anm2::LAYER && document.reference.itemID != -1
|
||||
? document.anm2.frame_effective(document.reference.itemID, *frame)
|
||||
: useFrame;
|
||||
|
||||
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))
|
||||
if (combo_id_mapped(localize.get(BASIC_EVENT), frame ? &useFrame.eventID : &dummy_value_negative<int>(),
|
||||
document.event.ids, document.event.labels) &&
|
||||
frame)
|
||||
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))
|
||||
if (input_int_range(localize.get(BASIC_AT_FRAME), frame ? useFrame.atFrame : dummy_value<int>(), 0,
|
||||
std::numeric_limits<int>::max(), STEP, STEP_FAST,
|
||||
!frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0) &&
|
||||
frame)
|
||||
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>()))
|
||||
if (ImGui::Checkbox(localize.get(BASIC_VISIBLE), frame ? &useFrame.isVisible : &dummy_value<bool>()) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_VISIBILITY), Document::FRAMES,
|
||||
frame->isVisible = useFrame.isVisible);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_VISIBILITY));
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_SOUNDS));
|
||||
|
||||
auto childSize = imgui::size_without_footer_get();
|
||||
|
||||
if (ImGui::BeginChild("##Sounds Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
if (!useFrame.soundIDs.empty())
|
||||
{
|
||||
for (auto [i, id] : std::views::enumerate(useFrame.soundIDs))
|
||||
{
|
||||
ImGui::PushID(i);
|
||||
if (combo_id_mapped("##Sound", frame ? &id : &dummy_value_negative<int>(), document.sound.ids,
|
||||
document.sound.labels) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_SOUND), Document::FRAMES,
|
||||
frame->soundIDs[i] = id);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_SOUND));
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = imgui::widget_size_with_row_get(2);
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize) && frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_TRIGGER_SOUND), Document::FRAMES,
|
||||
frame->soundIDs.push_back(-1));
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_TRIGGER_SOUND));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(useFrame.soundIDs.empty());
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize) && frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_TRIGGER_SOUND), Document::FRAMES,
|
||||
frame->soundIDs.pop_back());
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REMOVE_TRIGGER_SOUND));
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_);
|
||||
bool isRegionSet = frame && displayFrame.regionID != -1 && displayFrame.crop == frame->crop &&
|
||||
displayFrame.size == frame->size && displayFrame.pivot == frame->pivot;
|
||||
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_ || isRegionSet);
|
||||
{
|
||||
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);
|
||||
auto cropDisplay = frame ? displayFrame.crop : vec2();
|
||||
auto cropEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_CROP), frame ? &cropDisplay : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.crop) : "");
|
||||
if (cropEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_CROP));
|
||||
if (frame && cropEdit != edit::NONE) frame->crop = cropDisplay;
|
||||
if (cropEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
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);
|
||||
auto sizeDisplay = frame ? displayFrame.size : vec2();
|
||||
auto sizeEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_SIZE), frame ? &sizeDisplay : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.size) : "");
|
||||
if (sizeEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_SIZE));
|
||||
if (frame && sizeEdit != edit::NONE) frame->size = sizeDisplay;
|
||||
if (sizeEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
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);
|
||||
auto positionEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_POSITION), frame ? &frame->position : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(frame->position) : "");
|
||||
if (positionEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_POSITION));
|
||||
else if (positionEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_POSITION));
|
||||
|
||||
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_);
|
||||
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_ || isRegionSet);
|
||||
{
|
||||
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);
|
||||
auto pivotDisplay = frame ? displayFrame.pivot : vec2();
|
||||
auto pivotEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_PIVOT), frame ? &pivotDisplay : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.pivot) : "");
|
||||
if (pivotEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_PIVOT));
|
||||
if (frame && pivotEdit != edit::NONE) frame->pivot = pivotDisplay;
|
||||
if (pivotEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
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);
|
||||
auto scaleEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_SCALE), frame ? &frame->scale : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(frame->scale) : "");
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SCALE));
|
||||
if (scaleEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_SCALE));
|
||||
else if (scaleEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
|
||||
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);
|
||||
auto rotationEdit =
|
||||
drag_float_persistent(localize.get(BASIC_ROTATION), frame ? &frame->rotation : &dummy_value<float>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? float_format_get(frame->rotation) : "");
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROTATION));
|
||||
if (rotationEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_ROTATION));
|
||||
else if (rotationEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
|
||||
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))
|
||||
!frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0) &&
|
||||
frame)
|
||||
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);
|
||||
auto tintEdit =
|
||||
color_edit4_persistent(localize.get(BASIC_TINT), frame ? &frame->tint : &dummy_value<vec4>());
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TINT));
|
||||
if (tintEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_TINT));
|
||||
else if (tintEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
|
||||
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);
|
||||
auto colorOffsetEdit = color_edit3_persistent(localize.get(BASIC_COLOR_OFFSET),
|
||||
frame ? &frame->colorOffset : &dummy_value<vec3>());
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COLOR_OFFSET));
|
||||
if (colorOffsetEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_COLOR_OFFSET));
|
||||
else if (colorOffsetEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
|
||||
if (ImGui::Checkbox(localize.get(BASIC_VISIBLE), frame ? &useFrame.isVisible : &dummy_value<bool>()))
|
||||
ImGui::BeginDisabled(type != anm2::LAYER);
|
||||
if (combo_id_mapped(localize.get(BASIC_REGION), frame ? &useFrame.regionID : &dummy_value_negative<int>(),
|
||||
regionIds, regionLabels) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_REGION_PROPERTIES), Document::FRAMES,
|
||||
frame->regionID = useFrame.regionID;
|
||||
auto effectiveFrame = document.anm2.frame_effective(document.reference.itemID, *frame);
|
||||
frame->crop = effectiveFrame.crop;
|
||||
frame->size = effectiveFrame.size;
|
||||
frame->pivot = effectiveFrame.pivot);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REGION));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
auto interpolationValue = frame ? static_cast<int>(useFrame.interpolation) : dummy_value<int>();
|
||||
if (combo_id_mapped(localize.get(BASIC_INTERPOLATED), &interpolationValue, interpolationValues,
|
||||
interpolationLabels) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_INTERPOLATION), Document::FRAMES,
|
||||
frame->interpolation = static_cast<anm2::Frame::Interpolation>(interpolationValue));
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FRAME_INTERPOLATION));
|
||||
|
||||
if (ImGui::Checkbox(localize.get(BASIC_VISIBLE), frame ? &useFrame.isVisible : &dummy_value<bool>()) &&
|
||||
frame)
|
||||
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);
|
||||
if (ImGui::Button(localize.get(LABEL_FLIP_X), widgetSize) && frame)
|
||||
{
|
||||
if (ImGui::IsKeyDown(ImGuiMod_Ctrl))
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_X), Document::FRAMES,
|
||||
frame->scale.x = -frame->scale.x;
|
||||
frame->position.x = -frame->position.x);
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
if (ImGui::Button(localize.get(LABEL_FLIP_Y), widgetSize) && frame)
|
||||
{
|
||||
if (ImGui::IsKeyDown(ImGuiMod_Ctrl))
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_Y), Document::FRAMES,
|
||||
frame->scale.y = -frame->scale.y;
|
||||
frame->position.y = -frame->position.y);
|
||||
}
|
||||
else
|
||||
{
|
||||
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));
|
||||
}
|
||||
changeAllFrameProperties.update(document, settings);
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
dummy_value_negative<int>() = -1;
|
||||
dummy_value<float>() = 0;
|
||||
dummy_value<int>() = 0;
|
||||
dummy_value<bool>() = 0;
|
||||
dummy_value<vec2>() = vec2();
|
||||
dummy_value<vec3>() = vec3();
|
||||
dummy_value<vec4>() = vec4();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "manager.h"
|
||||
#include "settings.h"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class FrameProperties
|
||||
{
|
||||
public:
|
||||
void update(Manager&, Settings&);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <glm/vec2.hpp>
|
||||
|
||||
#include "manager.hpp"
|
||||
#include "wizard/change_all_frame_properties.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class FrameProperties
|
||||
{
|
||||
wizard::ChangeAllFrameProperties changeAllFrameProperties{};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&);
|
||||
};
|
||||
}
|
||||
+102
-73
@@ -1,11 +1,12 @@
|
||||
#include "layers.h"
|
||||
#include "layers.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "log.h"
|
||||
#include "map_.h"
|
||||
#include "strings.h"
|
||||
#include "toast.h"
|
||||
#include "log.hpp"
|
||||
#include "map_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::resource;
|
||||
@@ -18,12 +19,70 @@ namespace anm2ed::imgui
|
||||
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;
|
||||
auto add = [&]() { manager.layer_properties_open(); };
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.layers_unused();
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
anm2.content.layers.erase(id);
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_LAYERS), Document::LAYERS, behavior());
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.layers[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxLayerIdBefore = anm2.content.layers.empty() ? -1 : anm2.content.layers.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_LAYERS));
|
||||
if (anm2.layers_deserialize(clipboard.get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.layers.empty())
|
||||
{
|
||||
auto maxLayerIdAfter = anm2.content.layers.rbegin()->first;
|
||||
if (maxLayerIdAfter > maxLayerIdBefore)
|
||||
{
|
||||
newLayerId = maxLayerIdAfter;
|
||||
selection = {maxLayerIdAfter};
|
||||
reference = maxLayerIdAfter;
|
||||
}
|
||||
}
|
||||
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)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_LAYERS), Document::LAYERS, behavior());
|
||||
};
|
||||
|
||||
auto properties = [&](int id) { manager.layer_properties_open(id); };
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_LAYERS_WINDOW), &settings.windowIsLayers))
|
||||
{
|
||||
@@ -49,13 +108,7 @@ namespace anm2ed::imgui
|
||||
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::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) properties(id);
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
@@ -72,51 +125,35 @@ namespace anm2ed::imgui
|
||||
|
||||
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_ADD], shortcut::FOCUSED)) add();
|
||||
if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
|
||||
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))
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, selection.size() == 1))
|
||||
properties(*selection.begin());
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty()))
|
||||
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();
|
||||
}
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
@@ -126,24 +163,12 @@ namespace anm2ed::imgui
|
||||
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();
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add();
|
||||
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();
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_LAYERS), settings.shortcutRemove);
|
||||
}
|
||||
ImGui::End();
|
||||
@@ -160,18 +185,21 @@ namespace anm2ed::imgui
|
||||
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);
|
||||
|
||||
combo_id_mapped(localize.get(LABEL_SPRITESHEET), &layer.spritesheetID, document.spritesheet.ids,
|
||||
document.spritesheet.labels);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_SPRITESHEET));
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize))
|
||||
{
|
||||
if (reference == -1)
|
||||
{
|
||||
auto add = [&]()
|
||||
auto layer_add = [&]()
|
||||
{
|
||||
auto id = map::next_id_get(anm2.content.layers);
|
||||
anm2.content.layers[id] = layer;
|
||||
@@ -179,17 +207,17 @@ namespace anm2ed::imgui
|
||||
newLayerId = id;
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_LAYER), Document::LAYERS, add());
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_LAYER), Document::LAYERS, layer_add());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto set = [&]()
|
||||
auto layer_set = [&]()
|
||||
{
|
||||
anm2.content.layers[reference] = layer;
|
||||
selection = {reference};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_LAYER_PROPERTIES), Document::LAYERS, set());
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_LAYER_PROPERTIES), Document::LAYERS, layer_set());
|
||||
}
|
||||
|
||||
manager.layer_properties_close();
|
||||
@@ -197,6 +225,7 @@ namespace anm2ed::imgui
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.layer_properties_close();
|
||||
|
||||
manager.layer_properties_end();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
+98
-70
@@ -1,11 +1,12 @@
|
||||
#include "nulls.h"
|
||||
#include "nulls.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "log.h"
|
||||
#include "map_.h"
|
||||
#include "strings.h"
|
||||
#include "toast.h"
|
||||
#include "log.hpp"
|
||||
#include "map_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
@@ -18,12 +19,69 @@ namespace anm2ed::imgui
|
||||
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;
|
||||
auto add = [&]() { manager.null_properties_open(); };
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.nulls_unused();
|
||||
if (unused.empty()) return;
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
anm2.content.nulls.erase(id);
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_NULLS), Document::NULLS, behavior());
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.nulls[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxNullIdBefore = anm2.content.nulls.empty() ? -1 : anm2.content.nulls.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_NULLS));
|
||||
if (anm2.nulls_deserialize(clipboard.get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.nulls.empty())
|
||||
{
|
||||
auto maxNullIdAfter = anm2.content.nulls.rbegin()->first;
|
||||
if (maxNullIdAfter > maxNullIdBefore)
|
||||
{
|
||||
newNullId = maxNullIdAfter;
|
||||
selection = {maxNullIdAfter};
|
||||
reference = maxNullIdAfter;
|
||||
}
|
||||
}
|
||||
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)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_NULLS), Document::NULLS, behavior());
|
||||
};
|
||||
|
||||
auto properties = [&](int id) { manager.null_properties_open(id); };
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_NULLS_WINDOW), &settings.windowIsNulls))
|
||||
{
|
||||
@@ -48,11 +106,7 @@ namespace anm2ed::imgui
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newNullId = -1;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
hovered = id;
|
||||
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) manager.null_properties_open(id);
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) properties(id);
|
||||
|
||||
if (isReferenced) ImGui::PopFont();
|
||||
|
||||
@@ -69,51 +123,35 @@ namespace anm2ed::imgui
|
||||
|
||||
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_ADD], shortcut::FOCUSED)) add();
|
||||
if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
|
||||
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))
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, selection.size() == 1))
|
||||
properties(*selection.begin());
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty()))
|
||||
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();
|
||||
}
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
@@ -123,24 +161,12 @@ namespace anm2ed::imgui
|
||||
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();
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add();
|
||||
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();
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_NULLS), settings.shortcutRemove);
|
||||
}
|
||||
ImGui::End();
|
||||
@@ -165,11 +191,12 @@ namespace anm2ed::imgui
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize))
|
||||
{
|
||||
if (reference == -1)
|
||||
{
|
||||
auto add = [&]()
|
||||
auto null_add = [&]()
|
||||
{
|
||||
auto id = map::next_id_get(anm2.content.nulls);
|
||||
anm2.content.nulls[id] = null;
|
||||
@@ -177,17 +204,17 @@ namespace anm2ed::imgui
|
||||
newNullId = id;
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_NULL), Document::NULLS, add());
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_NULL), Document::NULLS, null_add());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto set = [&]()
|
||||
auto null_set = [&]()
|
||||
{
|
||||
anm2.content.nulls[reference] = null;
|
||||
selection = {reference};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_NULL_PROPERTIES), Document::NULLS, set());
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_NULL_PROPERTIES), Document::NULLS, null_set());
|
||||
}
|
||||
|
||||
manager.null_properties_close();
|
||||
@@ -195,6 +222,7 @@ namespace anm2ed::imgui
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.null_properties_close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "onionskin.h"
|
||||
#include "onionskin.hpp"
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#include "imgui_.h"
|
||||
#include "strings.h"
|
||||
#include "imgui_.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace glm;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "manager.h"
|
||||
#include "manager.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -0,0 +1,569 @@
|
||||
#include "regions.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "document.hpp"
|
||||
#include "log.hpp"
|
||||
#include "map_.hpp"
|
||||
#include "math_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "vector_.hpp"
|
||||
|
||||
#include "../../util/map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
void Regions::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& selection = document.region.selection;
|
||||
auto& frame = document.frames;
|
||||
auto& spritesheetReference = document.spritesheet.reference;
|
||||
auto& reference = document.region.reference;
|
||||
auto style = ImGui::GetStyle();
|
||||
|
||||
auto spritesheet = map::find(anm2.content.spritesheets, spritesheetReference);
|
||||
|
||||
if (manager.isMakeRegionRequested)
|
||||
{
|
||||
spritesheetReference = manager.makeRegionSpritesheetId;
|
||||
reference = -1;
|
||||
editRegion = manager.makeRegion;
|
||||
isPreserveEditRegionOnOpen = true;
|
||||
propertiesPopup.open();
|
||||
manager.isMakeRegionRequested = false;
|
||||
spritesheet = map::find(anm2.content.spritesheets, spritesheetReference);
|
||||
}
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
if (!spritesheet) return;
|
||||
|
||||
auto unused = anm2.regions_unused(*spritesheet);
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
{
|
||||
for (auto& animation : anm2.animations.items)
|
||||
for (auto& layerAnimation : animation.layerAnimations | std::views::values)
|
||||
for (auto& frame : layerAnimation.frames)
|
||||
if (frame.regionID == id) frame.regionID = -1;
|
||||
|
||||
spritesheet->regions.erase(id);
|
||||
auto& order = spritesheet->regionOrder;
|
||||
order.erase(std::remove(order.begin(), order.end(), id), order.end());
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_REGIONS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto trim = [&]()
|
||||
{
|
||||
if (!spritesheet || selection.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
if (anm2.regions_trim(spritesheetReference, selection))
|
||||
{
|
||||
if (reference != -1 && !selection.contains(reference)) reference = *selection.begin();
|
||||
document.reference = {document.reference.animationIndex};
|
||||
frame.reference = -1;
|
||||
frame.selection.clear();
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_TRIM_REGIONS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (!spritesheet || selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += spritesheet->region_to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (!spritesheet || clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxRegionIdBefore = spritesheet->regions.empty() ? -1 : spritesheet->regions.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_REGIONS));
|
||||
if (spritesheet->regions_deserialize(clipboard.get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!spritesheet->regions.empty())
|
||||
{
|
||||
auto maxRegionIdAfter = spritesheet->regions.rbegin()->first;
|
||||
if (maxRegionIdAfter > maxRegionIdBefore)
|
||||
{
|
||||
newRegionId = maxRegionIdAfter;
|
||||
selection = {maxRegionIdAfter};
|
||||
reference = maxRegionIdAfter;
|
||||
}
|
||||
}
|
||||
document.change(Document::SPRITESHEETS);
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_REGIONS_FAILED), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_REGIONS_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_REGIONS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto add_open = [&]()
|
||||
{
|
||||
reference = -1;
|
||||
editRegion = anm2::Spritesheet::Region{};
|
||||
propertiesPopup.open();
|
||||
};
|
||||
|
||||
auto properties_open = [&](int id)
|
||||
{
|
||||
if (!spritesheet || !spritesheet->regions.contains(id)) return;
|
||||
reference = id;
|
||||
propertiesPopup.open();
|
||||
};
|
||||
|
||||
auto context_menu = [&]()
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Right))
|
||||
ImGui::OpenPopup("##Region Context Menu");
|
||||
|
||||
if (ImGui::BeginPopup("##Region Context Menu"))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, selection.size() == 1))
|
||||
properties_open(*selection.begin());
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_TRIM), nullptr, false, !selection.empty())) trim();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIM_REGIONS));
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) copy();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_REGIONS_WINDOW), &settings.windowIsRegions))
|
||||
{
|
||||
if (!spritesheet)
|
||||
{
|
||||
ImGui::TextUnformatted(localize.get(TEXT_SELECT_SPRITESHEET));
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
auto childSize = size_without_footer_get();
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
if (ImGui::BeginChild("##Regions Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto regionChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 2);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
|
||||
|
||||
auto rebuild_order = [&]()
|
||||
{
|
||||
spritesheet->regionOrder.clear();
|
||||
spritesheet->regionOrder.reserve(spritesheet->regions.size());
|
||||
for (auto id : spritesheet->regions | std::views::keys)
|
||||
spritesheet->regionOrder.push_back(id);
|
||||
};
|
||||
if (spritesheet->regionOrder.size() != spritesheet->regions.size())
|
||||
rebuild_order();
|
||||
else
|
||||
{
|
||||
bool isOrderValid = true;
|
||||
for (auto id : spritesheet->regionOrder)
|
||||
if (!spritesheet->regions.contains(id))
|
||||
{
|
||||
isOrderValid = false;
|
||||
break;
|
||||
}
|
||||
if (!isOrderValid) rebuild_order();
|
||||
}
|
||||
|
||||
selection.set_index_map(&spritesheet->regionOrder);
|
||||
selection.start(spritesheet->regionOrder.size());
|
||||
if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused))
|
||||
{
|
||||
selection.clear();
|
||||
for (auto& id : spritesheet->regionOrder)
|
||||
selection.insert(id);
|
||||
}
|
||||
if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear();
|
||||
auto scroll_to_item = [&](float itemHeight, bool isTarget)
|
||||
{
|
||||
if (!isTarget) return;
|
||||
auto windowHeight = ImGui::GetWindowHeight();
|
||||
auto targetTop = ImGui::GetCursorPosY();
|
||||
auto targetBottom = targetTop + itemHeight;
|
||||
auto visibleTop = ImGui::GetScrollY();
|
||||
auto visibleBottom = visibleTop + windowHeight;
|
||||
if (targetTop < visibleTop)
|
||||
ImGui::SetScrollY(targetTop);
|
||||
else if (targetBottom > visibleBottom)
|
||||
ImGui::SetScrollY(targetBottom - windowHeight);
|
||||
};
|
||||
int scrollTargetId = -1;
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
(ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)))
|
||||
{
|
||||
auto& order = spritesheet->regionOrder;
|
||||
if (!order.empty())
|
||||
{
|
||||
int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1;
|
||||
int current = reference;
|
||||
if (current == -1 && !selection.empty()) current = *selection.begin();
|
||||
auto it = std::find(order.begin(), order.end(), current);
|
||||
int index = it == order.end() ? 0 : (int)std::distance(order.begin(), it);
|
||||
index = std::clamp(index + delta, 0, (int)order.size() - 1);
|
||||
int nextId = order[index];
|
||||
selection = {nextId};
|
||||
reference = nextId;
|
||||
document.reference = {document.reference.animationIndex};
|
||||
frame.reference = -1;
|
||||
frame.selection.clear();
|
||||
scrollTargetId = nextId;
|
||||
}
|
||||
}
|
||||
bool isValid = spritesheet->is_valid();
|
||||
auto& texture = isValid ? spritesheet->texture : resources.icons[icon::NONE];
|
||||
auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
for (int i = 0; i < (int)spritesheet->regionOrder.size(); i++)
|
||||
{
|
||||
int id = spritesheet->regionOrder[i];
|
||||
auto regionIt = spritesheet->regions.find(id);
|
||||
if (regionIt == spritesheet->regions.end()) continue;
|
||||
auto& region = regionIt->second;
|
||||
auto isNewRegion = newRegionId == id;
|
||||
auto nameCStr = region.name.c_str();
|
||||
auto isSelected = selection.contains(id);
|
||||
auto isReferenced = id == reference;
|
||||
|
||||
ImGui::PushID(id);
|
||||
|
||||
scroll_to_item(regionChildSize.y, scrollTargetId == id);
|
||||
|
||||
if (ImGui::BeginChild("##Region Child", regionChildSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::SetNextItemSelectionUserData(i);
|
||||
ImGui::SetNextItemStorageID(id);
|
||||
if (ImGui::Selectable("##Region Selectable", isSelected, 0, regionChildSize))
|
||||
{
|
||||
reference = id;
|
||||
document.reference = {document.reference.animationIndex};
|
||||
frame.reference = -1;
|
||||
frame.selection.clear();
|
||||
}
|
||||
if (scrollTargetId == id) ImGui::SetItemDefaultFocus();
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) propertiesPopup.open();
|
||||
|
||||
auto viewport = ImGui::GetMainViewport();
|
||||
auto maxPreviewSize = to_vec2(viewport->Size) * 0.5f;
|
||||
vec2 regionSize = glm::max(region.size, vec2(1.0f));
|
||||
vec2 previewSize = regionSize;
|
||||
if (previewSize.x > maxPreviewSize.x || previewSize.y > maxPreviewSize.y)
|
||||
{
|
||||
auto scale = glm::min(maxPreviewSize.x / previewSize.x, maxPreviewSize.y / previewSize.y);
|
||||
previewSize *= scale;
|
||||
}
|
||||
vec2 uvMin{};
|
||||
vec2 uvMax{1.0f, 1.0f};
|
||||
if (isValid)
|
||||
{
|
||||
uvMin = region.crop / vec2(texture.size);
|
||||
uvMax = (region.crop + region.size) / vec2(texture.size);
|
||||
}
|
||||
|
||||
auto textWidth = ImGui::CalcTextSize(nameCStr).x;
|
||||
auto tooltipPadding = style.WindowPadding.x * 4.0f;
|
||||
auto minWidth = previewSize.x + style.ItemSpacing.x + textWidth + tooltipPadding;
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
auto childFlags = ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY;
|
||||
auto noScrollFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
||||
|
||||
if (ImGui::BeginChild("##Region Tooltip Image Child", to_imvec2(previewSize), childFlags, noScrollFlags))
|
||||
ImGui::ImageWithBg(texture.id, to_imvec2(previewSize), to_imvec2(uvMin), to_imvec2(uvMax), ImVec4(),
|
||||
tintColor);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
auto infoChildFlags = ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY;
|
||||
if (ImGui::BeginChild("##Region Info Tooltip Child", ImVec2(), infoChildFlags, noScrollFlags))
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(nameCStr);
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region.crop.x, region.crop.y))
|
||||
.c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region.size.x, region.size.y))
|
||||
.c_str());
|
||||
if (region.origin == anm2::Spritesheet::Region::CUSTOM)
|
||||
{
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y))
|
||||
.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
StringType originString = LABEL_REGION_ORIGIN_CENTER;
|
||||
if (region.origin == anm2::Spritesheet::Region::TOP_LEFT) originString = LABEL_REGION_ORIGIN_TOP_LEFT;
|
||||
auto originLabel = localize.get(originString);
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_ORIGIN), std::make_format_args(originLabel)).c_str());
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
if (ImGui::BeginDragDropSource())
|
||||
{
|
||||
static std::vector<int> dragDropSelection{};
|
||||
dragDropSelection.assign(selection.begin(), selection.end());
|
||||
ImGui::SetDragDropPayload("Region Drag Drop", dragDropSelection.data(),
|
||||
dragDropSelection.size() * sizeof(int));
|
||||
|
||||
for (auto regionId : dragDropSelection)
|
||||
{
|
||||
auto dragIt = spritesheet->regions.find(regionId);
|
||||
if (dragIt == spritesheet->regions.end()) continue;
|
||||
ImGui::TextUnformatted(dragIt->second.name.c_str());
|
||||
}
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
if (ImGui::BeginDragDropTarget())
|
||||
{
|
||||
if (auto payload = ImGui::AcceptDragDropPayload("Region Drag Drop"))
|
||||
{
|
||||
auto payloadIds = (int*)(payload->Data);
|
||||
int payloadCount = (int)(payload->DataSize / sizeof(int));
|
||||
std::vector<int> indices{};
|
||||
indices.reserve(payloadCount);
|
||||
for (int payloadIndex = 0; payloadIndex < payloadCount; payloadIndex++)
|
||||
{
|
||||
int payloadId = payloadIds[payloadIndex];
|
||||
int index = vector::find_index(spritesheet->regionOrder, payloadId);
|
||||
if (index != -1) indices.push_back(index);
|
||||
}
|
||||
if (!indices.empty())
|
||||
{
|
||||
std::sort(indices.begin(), indices.end());
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_REGIONS), Document::SPRITESHEETS,
|
||||
vector::move_indices(spritesheet->regionOrder, indices, i));
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
auto imageSize = to_imvec2(vec2(regionChildSize.y));
|
||||
auto aspectRatio = region.size.y != 0.0f ? (float)region.size.x / region.size.y : 1.0f;
|
||||
|
||||
if (imageSize.x / imageSize.y > aspectRatio)
|
||||
imageSize.x = imageSize.y * aspectRatio;
|
||||
else
|
||||
imageSize.y = imageSize.x / aspectRatio;
|
||||
|
||||
ImGui::SetCursorPos(cursorPos);
|
||||
ImGui::ImageWithBg(texture.id, imageSize, to_imvec2(uvMin), to_imvec2(uvMax), ImVec4(), tintColor);
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(regionChildSize.y + style.ItemSpacing.x,
|
||||
regionChildSize.y - regionChildSize.y / 2 - ImGui::GetTextLineHeight() / 2));
|
||||
|
||||
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(nameCStr);
|
||||
if (isReferenced) ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (isNewRegion)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newRegionId = -1;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
selection.finish();
|
||||
|
||||
if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add_open();
|
||||
if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
context_menu();
|
||||
|
||||
auto rowOneWidgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), rowOneWidgetSize)) add_open();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_REGION), settings.shortcutAdd);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), rowOneWidgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_REGIONS), settings.shortcutAdd);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
propertiesPopup.trigger();
|
||||
|
||||
if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
auto childSize = child_size_get(5);
|
||||
auto& region = reference == -1 ? editRegion : spritesheet->regions.at(reference);
|
||||
|
||||
if (propertiesPopup.isJustOpened)
|
||||
{
|
||||
if (!isPreserveEditRegionOnOpen)
|
||||
editRegion = anm2::Spritesheet::Region{};
|
||||
isPreserveEditRegionOnOpen = false;
|
||||
}
|
||||
|
||||
if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
const char* originOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT),
|
||||
localize.get(LABEL_REGION_ORIGIN_CENTER),
|
||||
localize.get(LABEL_REGION_ORIGIN_CUSTOM)};
|
||||
|
||||
if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere();
|
||||
input_text_string(localize.get(BASIC_NAME), ®ion.name);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME));
|
||||
ImGui::DragFloat2(localize.get(BASIC_CROP), value_ptr(region.crop), DRAG_SPEED, 0.0f, 0.0f,
|
||||
math::vec2_format_get(region.crop));
|
||||
ImGui::DragFloat2(localize.get(BASIC_SIZE), value_ptr(region.size), DRAG_SPEED, 0.0f, 0.0f,
|
||||
math::vec2_format_get(region.size));
|
||||
ImGui::BeginDisabled(region.origin != anm2::Spritesheet::Region::CUSTOM);
|
||||
ImGui::DragFloat2(localize.get(BASIC_PIVOT), value_ptr(region.pivot), DRAG_SPEED, 0.0f, 0.0f,
|
||||
math::vec2_format_get(region.pivot));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), (int*)®ion.origin, originOptions,
|
||||
IM_ARRAYSIZE(originOptions)))
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REGION_PROPERTIES_ORIGIN));
|
||||
|
||||
if (region.origin == anm2::Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == anm2::Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)};
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
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(spritesheet->regions);
|
||||
spritesheet->regions[id] = region;
|
||||
spritesheet->regionOrder.push_back(id);
|
||||
selection = {id};
|
||||
newRegionId = id;
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_REGION), Document::SPRITESHEETS, add());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto set = [&]()
|
||||
{
|
||||
spritesheet->regions.at(reference) = region;
|
||||
selection = {reference};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_REGION_PROPERTIES), Document::SPRITESHEETS, set());
|
||||
}
|
||||
|
||||
frame.reference = -1;
|
||||
frame.selection.clear();
|
||||
|
||||
propertiesPopup.close();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) propertiesPopup.close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
propertiesPopup.end();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Regions
|
||||
{
|
||||
public:
|
||||
anm2::Spritesheet::Region editRegion{};
|
||||
int newRegionId{-1};
|
||||
bool isPreserveEditRegionOnOpen{};
|
||||
imgui::PopupHelper propertiesPopup{imgui::PopupHelper(LABEL_REGION_PROPERTIES, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
|
||||
void update(Manager&, Settings&, Resources&, Clipboard&);
|
||||
};
|
||||
}
|
||||
+342
-125
@@ -1,14 +1,19 @@
|
||||
#include "sounds.h"
|
||||
#include "sounds.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
|
||||
#include "log.h"
|
||||
#include "strings.h"
|
||||
#include "toast.h"
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::dialog;
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -17,158 +22,370 @@ namespace anm2ed::imgui
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& reference = document.sound.reference;
|
||||
auto& unused = document.sound.unused;
|
||||
auto& hovered = document.null.hovered;
|
||||
auto& selection = document.sound.selection;
|
||||
auto style = ImGui::GetStyle();
|
||||
|
||||
hovered = -1;
|
||||
auto add_open = [&]() { dialog.file_open(Dialog::SOUND_OPEN); };
|
||||
auto replace_open = [&]() { dialog.file_open(Dialog::SOUND_REPLACE); };
|
||||
|
||||
auto play = [&](anm2::Sound& sound) { sound.play(); };
|
||||
|
||||
auto add = [&](const std::filesystem::path& path)
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
int id{};
|
||||
auto pathString = path::to_utf8(path);
|
||||
if (anm2.sound_add(document.directory_get(), path, id))
|
||||
{
|
||||
selection = {id};
|
||||
newSoundId = id;
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SOUND_INITIALIZED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED), std::make_format_args(pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(pathString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_SOUND), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.sounds_unused();
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
anm2.content.sounds.erase(id);
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_SOUNDS), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto reload = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : selection)
|
||||
{
|
||||
anm2::Sound& sound = anm2.content.sounds[id];
|
||||
sound.reload(document.directory_get());
|
||||
auto pathString = path::to_utf8(sound.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_RELOAD_SOUND), std::make_format_args(id, pathString)));
|
||||
logger.info(
|
||||
std::vformat(localize.get(TOAST_RELOAD_SOUND, anm2ed::ENGLISH), std::make_format_args(id, pathString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_RELOAD_SOUNDS), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto replace = [&](const std::filesystem::path& path)
|
||||
{
|
||||
if (selection.size() != 1 || path.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto& id = *selection.begin();
|
||||
anm2::Sound& sound = anm2.content.sounds[id];
|
||||
sound = anm2::Sound(document.directory_get(), path);
|
||||
auto pathString = path::to_utf8(sound.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_REPLACE_SOUND), std::make_format_args(id, pathString)));
|
||||
logger.info(
|
||||
std::vformat(localize.get(TOAST_REPLACE_SOUND, anm2ed::ENGLISH), std::make_format_args(id, pathString)));
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SOUND), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto open_directory = [&](anm2::Sound& sound)
|
||||
{
|
||||
if (sound.path.empty()) return;
|
||||
std::error_code ec{};
|
||||
auto absolutePath = std::filesystem::weakly_canonical(document.directory_get() / sound.path, ec);
|
||||
if (ec) absolutePath = document.directory_get() / sound.path;
|
||||
auto target = std::filesystem::is_directory(absolutePath) ? absolutePath
|
||||
: std::filesystem::is_directory(absolutePath.parent_path()) ? absolutePath.parent_path()
|
||||
: document.directory_get();
|
||||
dialog.file_explorer_open(target);
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.sounds[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxSoundIdBefore = anm2.content.sounds.empty() ? -1 : anm2.content.sounds.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(TOAST_SOUNDS_PASTE));
|
||||
if (anm2.sounds_deserialize(clipboard.get(), document.directory_get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.sounds.empty())
|
||||
{
|
||||
auto maxSoundIdAfter = anm2.content.sounds.rbegin()->first;
|
||||
if (maxSoundIdAfter > maxSoundIdBefore)
|
||||
{
|
||||
newSoundId = maxSoundIdAfter;
|
||||
selection = {maxSoundIdAfter};
|
||||
reference = maxSoundIdAfter;
|
||||
}
|
||||
}
|
||||
document.change(Document::SOUNDS);
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_SOUNDS), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto context_menu = [&]()
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Right))
|
||||
ImGui::OpenPopup("##Sound Context Menu");
|
||||
|
||||
if (ImGui::BeginPopup("##Sound Context Menu"))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_PLAY), nullptr, false, selection.size() == 1))
|
||||
play(anm2.content.sounds[*selection.begin()]);
|
||||
if (ImGui::MenuItem(localize.get(BASIC_OPEN_DIRECTORY), nullptr, false, selection.size() == 1))
|
||||
open_directory(anm2.content.sounds[*selection.begin()]);
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_RELOAD), nullptr, false, !selection.empty())) reload();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REPLACE), nullptr, false, selection.size() == 1)) replace_open();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) copy();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_SOUNDS_WINDOW), &settings.windowIsSounds))
|
||||
{
|
||||
auto childSize = imgui::size_without_footer_get();
|
||||
|
||||
if (ImGui::BeginChild("##Sounds Child", childSize, true))
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
if (ImGui::BeginChild("##Sounds Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto soundChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 2);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
|
||||
|
||||
selection.start(anm2.content.sounds.size());
|
||||
if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused))
|
||||
{
|
||||
selection.clear();
|
||||
for (auto& id : anm2.content.sounds | std::views::keys)
|
||||
selection.insert(id);
|
||||
}
|
||||
if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear();
|
||||
auto scroll_to_item = [&](float itemHeight, bool isTarget)
|
||||
{
|
||||
if (!isTarget) return;
|
||||
auto windowHeight = ImGui::GetWindowHeight();
|
||||
auto targetTop = ImGui::GetCursorPosY();
|
||||
auto targetBottom = targetTop + itemHeight;
|
||||
auto visibleTop = ImGui::GetScrollY();
|
||||
auto visibleBottom = visibleTop + windowHeight;
|
||||
if (targetTop < visibleTop)
|
||||
ImGui::SetScrollY(targetTop);
|
||||
else if (targetBottom > visibleBottom)
|
||||
ImGui::SetScrollY(targetBottom - windowHeight);
|
||||
};
|
||||
int scrollTargetId = -1;
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
(ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)))
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
ids.reserve(anm2.content.sounds.size());
|
||||
for (auto& [id, sound] : anm2.content.sounds)
|
||||
ids.push_back(id);
|
||||
if (!ids.empty())
|
||||
{
|
||||
int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1;
|
||||
int current = reference;
|
||||
if (current == -1 && !selection.empty()) current = *selection.begin();
|
||||
auto it = std::find(ids.begin(), ids.end(), current);
|
||||
int index = it == ids.end() ? 0 : (int)std::distance(ids.begin(), it);
|
||||
index = std::clamp(index + delta, 0, (int)ids.size() - 1);
|
||||
int nextId = ids[index];
|
||||
selection = {nextId};
|
||||
reference = nextId;
|
||||
scrollTargetId = nextId;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [id, sound] : anm2.content.sounds)
|
||||
{
|
||||
auto isSelected = selection.contains(id);
|
||||
auto isReferenced = reference == id;
|
||||
const std::string pathString = sound.path.empty() ? std::string{anm2::NO_PATH} : sound.path.string();
|
||||
const char* pathLabel = pathString.c_str();
|
||||
|
||||
auto isNewSound = newSoundId == id;
|
||||
ImGui::PushID(id);
|
||||
ImGui::SetNextItemSelectionUserData(id);
|
||||
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
|
||||
if (ImGui::Selectable(pathLabel, isSelected)) sound.play();
|
||||
if (ImGui::IsItemHovered()) hovered = id;
|
||||
if (newSoundId == id)
|
||||
|
||||
scroll_to_item(soundChildSize.y, scrollTargetId == id);
|
||||
|
||||
if (ImGui::BeginChild("##Sound Child", soundChildSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto isSelected = selection.contains(id);
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
bool isValid = sound.is_valid();
|
||||
auto& soundIcon = isValid ? resources.icons[icon::SOUND] : resources.icons[icon::NONE];
|
||||
auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
auto pathString = path::to_utf8(sound.path);
|
||||
|
||||
ImGui::SetNextItemSelectionUserData(id);
|
||||
ImGui::SetNextItemStorageID(id);
|
||||
if (ImGui::Selectable("##Sound Selectable", isSelected, 0, soundChildSize))
|
||||
{
|
||||
reference = id;
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) play(sound);
|
||||
}
|
||||
if (scrollTargetId == id) ImGui::SetItemDefaultFocus();
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) open_directory(sound);
|
||||
|
||||
auto textWidth = ImGui::CalcTextSize(pathString.c_str()).x;
|
||||
auto tooltipPadding = style.WindowPadding.x * 4.0f;
|
||||
auto minWidth = textWidth + style.ItemSpacing.x + tooltipPadding;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(pathString.c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::Text("%s: %d", localize.get(BASIC_ID), id);
|
||||
if (!isValid)
|
||||
{
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", localize.get(TOOLTIP_SOUND_INVALID));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Text("%s", localize.get(TEXT_SOUND_PLAY));
|
||||
ImGui::Text("%s", localize.get(TEXT_OPEN_DIRECTORY));
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
ImGui::SetCursorPos(cursorPos);
|
||||
auto imageSize = to_imvec2(vec2(soundChildSize.y));
|
||||
ImGui::ImageWithBg(soundIcon.id, imageSize, ImVec2(), ImVec2(1, 1), ImVec4(), tintColor);
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(soundChildSize.y + style.ItemSpacing.x,
|
||||
soundChildSize.y - soundChildSize.y / 2 - ImGui::GetTextLineHeight() / 2));
|
||||
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(id, pathString)).c_str());
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (isNewSound)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newSoundId = -1;
|
||||
}
|
||||
|
||||
if (isReferenced) ImGui::PopFont();
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(pathLabel);
|
||||
ImGui::PopFont();
|
||||
ImGui::Text("%s: %d", localize.get(BASIC_ID), id);
|
||||
ImGui::Text("%s", localize.get(TOOLTIP_SOUNDS_PLAY));
|
||||
if (!sound.is_valid())
|
||||
{
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", localize.get(TOOLTIP_SOUND_INVALID));
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
selection.finish();
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (!selection.empty())
|
||||
{
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.sounds[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
}
|
||||
else if (hovered > -1)
|
||||
clipboard.set(anm2.content.sounds[hovered].to_string(hovered));
|
||||
};
|
||||
|
||||
auto paste = [&](merge::Type type)
|
||||
{
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(TOAST_SOUNDS_PASTE));
|
||||
if (anm2.sounds_deserialize(clipboard.get(), document.directory_get().string(), type, &errorString))
|
||||
document.change(Document::SOUNDS);
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
}
|
||||
};
|
||||
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (imgui::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(),
|
||||
!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 = imgui::widget_size_with_row_get(2);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
context_menu();
|
||||
|
||||
auto widgetSize = imgui::widget_size_with_row_get(4);
|
||||
|
||||
imgui::shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) dialog.file_open(dialog::SOUND_OPEN);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add_open();
|
||||
imgui::set_item_tooltip_shortcut(localize.get(TOOLTIP_SOUND_ADD), settings.shortcutAdd);
|
||||
|
||||
if (dialog.is_selected(Dialog::SOUND_OPEN))
|
||||
{
|
||||
add(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
imgui::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.sounds.erase(id);
|
||||
unused.clear();
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_SOUNDS), Document::SOUNDS, remove_unused());
|
||||
};
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused();
|
||||
imgui::set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_SOUNDS), settings.shortcutRemove);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.empty());
|
||||
if (ImGui::Button(localize.get(BASIC_RELOAD), widgetSize)) reload();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RELOAD_SOUNDS));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.size() != 1);
|
||||
if (ImGui::Button(localize.get(BASIC_REPLACE), widgetSize)) replace_open();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SOUND));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (dialog.is_selected(Dialog::SOUND_REPLACE))
|
||||
{
|
||||
replace(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add_open();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (dialog.is_selected(dialog::SOUND_OPEN))
|
||||
{
|
||||
auto add = [&]()
|
||||
{
|
||||
int id{};
|
||||
if (anm2.sound_add(document.directory_get().string(), dialog.path, id))
|
||||
{
|
||||
selection = {id};
|
||||
newSoundId = id;
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, dialog.path)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SOUND_INITIALIZED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, dialog.path)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED), std::make_format_args(dialog.path)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(dialog.path)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_SOUND), Document::SOUNDS, add());
|
||||
|
||||
dialog.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "dialog.h"
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include "clipboard.hpp"
|
||||
#include "dialog.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -1,17 +1,16 @@
|
||||
#include "spritesheet_editor.h"
|
||||
#include "spritesheet_editor.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <utility>
|
||||
|
||||
#include "imgui_.h"
|
||||
#include "imgui_.hpp"
|
||||
#include "imgui_internal.h"
|
||||
#include "math_.h"
|
||||
#include "strings.h"
|
||||
#include "tool.h"
|
||||
#include "types.h"
|
||||
#include "math_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "tool.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
using namespace anm2ed::canvas;
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
@@ -19,12 +18,6 @@ using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
constexpr auto BORDER_DASH_LENGTH = 1.0f;
|
||||
constexpr auto BORDER_DASH_GAP = 0.5f;
|
||||
constexpr auto BORDER_DASH_OFFSET = 0.0f;
|
||||
|
||||
constexpr auto PIVOT_COLOR = color::PINK;
|
||||
|
||||
SpritesheetEditor::SpritesheetEditor() : Canvas(vec2()) {}
|
||||
|
||||
void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources)
|
||||
@@ -49,7 +42,11 @@ namespace anm2ed::imgui
|
||||
auto& tool = settings.tool;
|
||||
auto& shaderGrid = resources.shaders[shader::GRID];
|
||||
auto& shaderTexture = resources.shaders[shader::TEXTURE];
|
||||
auto& shaderLine = resources.shaders[shader::LINE];
|
||||
auto& dashedShader = resources.shaders[shader::DASHED];
|
||||
auto& frames = document.frames.selection;
|
||||
auto& regionReference = document.region.reference;
|
||||
auto& regionSelection = document.region.selection;
|
||||
|
||||
auto reset_checker_pan = [&]()
|
||||
{
|
||||
@@ -80,6 +77,23 @@ namespace anm2ed::imgui
|
||||
|
||||
auto center_view = [&]() { pan = -size * 0.5f; };
|
||||
|
||||
auto fit_view = [&]()
|
||||
{
|
||||
if (spritesheet && spritesheet->texture.is_valid())
|
||||
set_to_rect(zoom, pan, {0, 0, (float)spritesheet->texture.size.x, (float)spritesheet->texture.size.y});
|
||||
};
|
||||
|
||||
auto zoom_adjust = [&](float delta)
|
||||
{
|
||||
auto focus = position_translate(zoom, pan, size * 0.5f);
|
||||
auto previousZoom = zoom;
|
||||
zoom_set(zoom, pan, focus, delta);
|
||||
if (zoom != previousZoom) hasPendingZoomPanAdjust = true;
|
||||
};
|
||||
|
||||
auto zoom_in = [&]() { zoom_adjust(zoomStep); };
|
||||
auto zoom_out = [&]() { zoom_adjust(-zoomStep); };
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_SPRITESHEET_EDITOR_WINDOW), &settings.windowIsSpritesheetEditor))
|
||||
{
|
||||
|
||||
@@ -121,8 +135,7 @@ namespace anm2ed::imgui
|
||||
ImGui::SameLine();
|
||||
|
||||
imgui::shortcut(manager.chords[SHORTCUT_FIT]);
|
||||
if (ImGui::Button(localize.get(LABEL_FIT), widgetSize))
|
||||
if (spritesheet) set_to_rect(zoom, pan, {0, 0, spritesheet->texture.size.x, spritesheet->texture.size.y});
|
||||
if (ImGui::Button(localize.get(LABEL_FIT), widgetSize)) fit_view();
|
||||
imgui::set_item_tooltip_shortcut(localize.get(TOOLTIP_FIT), settings.shortcutFit);
|
||||
|
||||
auto mousePosInt = ivec2(mousePos);
|
||||
@@ -166,17 +179,25 @@ namespace anm2ed::imgui
|
||||
ImGui::EndChild();
|
||||
|
||||
auto drawList = ImGui::GetCurrentWindow()->DrawList;
|
||||
size_set(to_vec2(ImGui::GetContentRegionAvail()));
|
||||
|
||||
auto cursorScreenPos = ImGui::GetCursorScreenPos();
|
||||
auto min = ImGui::GetCursorScreenPos();
|
||||
auto max = to_imvec2(to_vec2(min) + size);
|
||||
|
||||
size_set(to_vec2(ImGui::GetContentRegionAvail()));
|
||||
auto mouseScreenPos = ImGui::GetIO().MousePos;
|
||||
bool isMouseOverCanvas = mouseScreenPos.x >= min.x && mouseScreenPos.x <= max.x && mouseScreenPos.y >= min.y &&
|
||||
mouseScreenPos.y <= max.y;
|
||||
auto hoverMousePos = vec2();
|
||||
if (isMouseOverCanvas)
|
||||
hoverMousePos = position_translate(zoom, pan, to_ivec2(mouseScreenPos) - to_ivec2(cursorScreenPos));
|
||||
|
||||
bind();
|
||||
viewport_set();
|
||||
clear(isTransparent ? vec4() : vec4(backgroundColor, 1.0f));
|
||||
clear(isTransparent ? vec4(0) : vec4(backgroundColor, 1.0f));
|
||||
|
||||
auto frame = document.frame_get();
|
||||
auto item = document.item_get();
|
||||
|
||||
if (spritesheet && spritesheet->texture.is_valid())
|
||||
{
|
||||
@@ -194,15 +215,71 @@ namespace anm2ed::imgui
|
||||
rect_render(dashedShader, spritesheetTransform, spritesheetModel, color::WHITE, BORDER_DASH_LENGTH,
|
||||
BORDER_DASH_GAP, BORDER_DASH_OFFSET);
|
||||
|
||||
if (frame && reference.itemID > -1 &&
|
||||
anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet)
|
||||
if (hoveredRegionId != -1)
|
||||
{
|
||||
auto cropModel = math::quad_model_get(frame->size, frame->crop);
|
||||
auto regionIt = spritesheet->regions.find(hoveredRegionId);
|
||||
if (regionIt != spritesheet->regions.end())
|
||||
{
|
||||
auto& region = regionIt->second;
|
||||
auto cropModel = math::quad_model_get(region.size, region.crop);
|
||||
auto cropTransform = transform * cropModel;
|
||||
rect_fill_render(shaderLine, cropTransform, cropModel, vec4(1.0f, 1.0f, 1.0f, 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
int highlightedRegionId = -1;
|
||||
if (frame && reference.itemID > -1 &&
|
||||
anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet && frame->regionID != -1 &&
|
||||
spritesheet->regions.contains(frame->regionID))
|
||||
{
|
||||
highlightedRegionId = frame->regionID;
|
||||
}
|
||||
else if (regionReference != -1 && spritesheet->regions.contains(regionReference))
|
||||
{
|
||||
highlightedRegionId = regionReference;
|
||||
}
|
||||
|
||||
auto draw_region_rect = [&](anm2::Spritesheet::Region& region, vec4 regionColor)
|
||||
{
|
||||
auto cropModel = math::quad_model_get(region.size, region.crop);
|
||||
auto cropTransform = transform * cropModel;
|
||||
rect_render(dashedShader, cropTransform, cropModel, color::RED);
|
||||
rect_render(dashedShader, cropTransform, cropModel, regionColor, BORDER_DASH_LENGTH, BORDER_DASH_GAP,
|
||||
BORDER_DASH_OFFSET);
|
||||
};
|
||||
|
||||
for (auto& [id, region] : spritesheet->regions)
|
||||
{
|
||||
if (id == highlightedRegionId) continue;
|
||||
draw_region_rect(region, color::WHITE);
|
||||
|
||||
auto pivotTransform =
|
||||
transform * math::quad_model_get(canvas::PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f);
|
||||
transform * math::quad_model_get(PIVOT_SIZE, region.crop + region.pivot, PIVOT_SIZE * 0.5f);
|
||||
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color::WHITE);
|
||||
}
|
||||
|
||||
if (highlightedRegionId != -1)
|
||||
{
|
||||
auto regionIt = spritesheet->regions.find(highlightedRegionId);
|
||||
if (regionIt != spritesheet->regions.end())
|
||||
{
|
||||
auto& region = regionIt->second;
|
||||
draw_region_rect(region, color::RED);
|
||||
|
||||
auto pivotTransform =
|
||||
transform * math::quad_model_get(PIVOT_SIZE, region.crop + region.pivot, PIVOT_SIZE * 0.5f);
|
||||
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
bool isFrameOnSpritesheet =
|
||||
frame && reference.itemID > -1 && anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet;
|
||||
if (isFrameOnSpritesheet && frame->regionID == -1)
|
||||
{
|
||||
auto frameModel = math::quad_model_get(frame->size, frame->crop);
|
||||
auto frameTransform = transform * frameModel;
|
||||
rect_render(shaderLine, frameTransform, frameModel, color::RED);
|
||||
|
||||
auto pivotTransform = transform * math::quad_model_get(PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f);
|
||||
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR);
|
||||
}
|
||||
}
|
||||
@@ -262,11 +339,11 @@ namespace anm2ed::imgui
|
||||
|
||||
auto frame = document.frame_get();
|
||||
auto useTool = tool;
|
||||
auto step = isMod ? canvas::STEP_FAST : canvas::STEP;
|
||||
auto step = isMod ? STEP_FAST : STEP;
|
||||
auto stepX = isGridSnap ? step * gridSize.x : step;
|
||||
auto stepY = isGridSnap ? step * gridSize.y : step;
|
||||
previousMousePos = mousePos;
|
||||
mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos));
|
||||
mousePos = position_translate(zoom, pan, to_ivec2(ImGui::GetMousePos()) - to_ivec2(cursorScreenPos));
|
||||
|
||||
auto snap_rect = [&](glm::vec2 minPoint, glm::vec2 maxPoint)
|
||||
{
|
||||
@@ -290,14 +367,68 @@ namespace anm2ed::imgui
|
||||
return std::pair{minPoint, maxPoint};
|
||||
};
|
||||
|
||||
auto frame_change_apply = [&](anm2::FrameChange frameChange, anm2::ChangeType changeType = anm2::ADJUST)
|
||||
{ item->frames_change(frameChange, reference.itemType, changeType, frames); };
|
||||
auto clamp_vec2_to_int = [](const vec2& value)
|
||||
{ return vec2(ivec2(value)); };
|
||||
auto region_set_all = [&](const vec2& crop, const vec2& size)
|
||||
{
|
||||
if (!spritesheet) return;
|
||||
for (auto id : regionSelection)
|
||||
{
|
||||
auto it = spritesheet->regions.find(id);
|
||||
if (it == spritesheet->regions.end()) continue;
|
||||
it->second.crop = clamp_vec2_to_int(crop);
|
||||
it->second.size = clamp_vec2_to_int(size);
|
||||
}
|
||||
};
|
||||
auto region_offset_all = [&](const vec2& delta)
|
||||
{
|
||||
if (!spritesheet) return;
|
||||
for (auto id : regionSelection)
|
||||
{
|
||||
auto it = spritesheet->regions.find(id);
|
||||
if (it == spritesheet->regions.end()) continue;
|
||||
it->second.crop = clamp_vec2_to_int(it->second.crop + delta);
|
||||
it->second.size = clamp_vec2_to_int(it->second.size);
|
||||
}
|
||||
};
|
||||
|
||||
if (isMouseMiddleDown) useTool = tool::PAN;
|
||||
if (tool == tool::MOVE && isMouseRightDown) useTool = tool::CROP;
|
||||
if (tool == tool::CROP && isMouseRightDown) useTool = tool::MOVE;
|
||||
if (tool == tool::DRAW && isMouseRightDown) useTool = tool::ERASE;
|
||||
if (tool == tool::ERASE && isMouseRightDown) useTool = tool::DRAW;
|
||||
|
||||
auto& areaType = tool::INFO[useTool].areaType;
|
||||
auto cursor = areaType == tool::SPRITESHEET_EDITOR || areaType == tool::ALL ? tool::INFO[useTool].cursor
|
||||
hoveredRegionId = -1;
|
||||
|
||||
if (useTool == tool::PAN && spritesheet && spritesheet->texture.is_valid() && isMouseOverCanvas)
|
||||
{
|
||||
for (auto& [id, region] : spritesheet->regions)
|
||||
{
|
||||
auto minPoint = glm::min(region.crop, region.crop + region.size);
|
||||
auto maxPoint = glm::max(region.crop, region.crop + region.size);
|
||||
if (hoverMousePos.x >= minPoint.x && hoverMousePos.x <= maxPoint.x && hoverMousePos.y >= minPoint.y &&
|
||||
hoverMousePos.y <= maxPoint.y)
|
||||
{
|
||||
hoveredRegionId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto& toolInfo = tool::INFO[useTool];
|
||||
auto& areaType = toolInfo.areaType;
|
||||
bool isAreaAllowed = areaType == tool::ALL || areaType == tool::SPRITESHEET_EDITOR;
|
||||
bool isFrameRequired =
|
||||
!(useTool == tool::PAN || useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER);
|
||||
bool isRegionInUse = frame && frame->regionID != -1 && (useTool == tool::CROP || useTool == tool::MOVE);
|
||||
bool isFrameAvailable = !isFrameRequired || (frame && !isRegionInUse) ||
|
||||
(useTool == tool::CROP && !frame && !regionSelection.empty()) ||
|
||||
(useTool == tool::MOVE && !frame && regionReference != -1);
|
||||
bool isSpritesheetRequired = useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER;
|
||||
bool isSpritesheetAvailable = !isSpritesheetRequired || (spritesheet && spritesheet->texture.is_valid());
|
||||
auto cursor = (isAreaAllowed && isFrameAvailable && isSpritesheetAvailable) ? toolInfo.cursor
|
||||
: ImGuiMouseCursor_NotAllowed;
|
||||
ImGui::SetMouseCursor(cursor);
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
@@ -305,17 +436,84 @@ namespace anm2ed::imgui
|
||||
switch (useTool)
|
||||
{
|
||||
case tool::PAN:
|
||||
if (isMouseLeftClicked && hoveredRegionId != -1)
|
||||
{
|
||||
regionReference = hoveredRegionId;
|
||||
regionSelection = {hoveredRegionId};
|
||||
if (frame && reference.itemID > -1 &&
|
||||
anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet)
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_REGION), Document::FRAMES,
|
||||
{
|
||||
anm2::FrameChange change{};
|
||||
change.regionID = hoveredRegionId;
|
||||
if (spritesheet)
|
||||
{
|
||||
auto regionIt = spritesheet->regions.find(hoveredRegionId);
|
||||
if (regionIt != spritesheet->regions.end())
|
||||
{
|
||||
change.cropX = regionIt->second.crop.x;
|
||||
change.cropY = regionIt->second.crop.y;
|
||||
change.sizeX = regionIt->second.size.x;
|
||||
change.sizeY = regionIt->second.size.y;
|
||||
change.pivotX = regionIt->second.pivot.x;
|
||||
change.pivotY = regionIt->second.pivot.y;
|
||||
}
|
||||
}
|
||||
frame_change_apply(change);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isMouseDown || isMouseMiddleDown) pan += mouseDelta;
|
||||
break;
|
||||
case tool::MOVE:
|
||||
if (!frame) break;
|
||||
if (isRegionInUse) break;
|
||||
if (!frame && regionReference != -1)
|
||||
{
|
||||
if (!spritesheet) break;
|
||||
auto regionIt = spritesheet->regions.find(regionReference);
|
||||
if (regionIt == spritesheet->regions.end()) break;
|
||||
|
||||
auto& region = regionIt->second;
|
||||
if (isBegin) document.snapshot(localize.get(EDIT_REGION_MOVE));
|
||||
bool isPivotEdited = isMouseDown || isLeftPressed || isRightPressed || isUpPressed || isDownPressed;
|
||||
if (isPivotEdited) region.origin = anm2::Spritesheet::Region::CUSTOM;
|
||||
if (isMouseDown) region.pivot = ivec2(mousePos) - ivec2(region.crop);
|
||||
if (isLeftPressed) region.pivot.x -= step;
|
||||
if (isRightPressed) region.pivot.x += step;
|
||||
if (isUpPressed) region.pivot.y -= step;
|
||||
if (isDownPressed) region.pivot.y += step;
|
||||
|
||||
if (isDuring)
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y))
|
||||
.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnd) document.change(Document::SPRITESHEETS);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!item || frames.empty()) break;
|
||||
if (isBegin) document.snapshot(localize.get(EDIT_FRAME_PIVOT));
|
||||
if (isMouseDown) frame->pivot = vec2(ivec2(mousePos - frame->crop));
|
||||
if (isLeftPressed) frame->pivot.x -= step;
|
||||
if (isRightPressed) frame->pivot.x += step;
|
||||
if (isUpPressed) frame->pivot.y -= step;
|
||||
if (isDownPressed) frame->pivot.y += step;
|
||||
frame->pivot = vec2(ivec2(frame->pivot));
|
||||
if (isMouseDown)
|
||||
{
|
||||
frame->crop = ivec2(frame->crop);
|
||||
frame_change_apply(
|
||||
{.pivotX = (int)(mousePos.x - frame->crop.x), .pivotY = (int)(mousePos.y - frame->crop.y)});
|
||||
}
|
||||
if (isLeftPressed) frame_change_apply({.pivotX = step}, anm2::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.pivotX = step}, anm2::ADD);
|
||||
if (isUpPressed) frame_change_apply({.pivotY = step}, anm2::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.pivotY = step}, anm2::ADD);
|
||||
|
||||
frame_change_apply({.pivotX = (int)frame->pivot.x, .pivotY = (int)frame->pivot.y});
|
||||
|
||||
if (isDuring)
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
@@ -330,40 +528,115 @@ namespace anm2ed::imgui
|
||||
if (isEnd) document.change(Document::FRAMES);
|
||||
break;
|
||||
case tool::CROP:
|
||||
if (!frame) break;
|
||||
if (isRegionInUse) break;
|
||||
if (frames.empty())
|
||||
{
|
||||
if (!spritesheet || regionSelection.empty()) break;
|
||||
if (isBegin) document.snapshot(localize.get(EDIT_REGION_CROP));
|
||||
|
||||
if (isMouseClicked)
|
||||
{
|
||||
cropAnchor = mousePos;
|
||||
region_set_all(vec2((int)cropAnchor.x, (int)cropAnchor.y), vec2());
|
||||
}
|
||||
if (isMouseDown)
|
||||
{
|
||||
auto [minPoint, maxPoint] = snap_rect(glm::min(cropAnchor, mousePos), glm::max(cropAnchor, mousePos));
|
||||
region_set_all(vec2(minPoint), vec2(maxPoint - minPoint));
|
||||
}
|
||||
if (isLeftPressed) region_offset_all(vec2(stepX * -1, 0));
|
||||
if (isRightPressed) region_offset_all(vec2(stepX, 0));
|
||||
if (isUpPressed) region_offset_all(vec2(0, stepY * -1));
|
||||
if (isDownPressed) region_offset_all(vec2(0, stepY));
|
||||
|
||||
if (isDuring)
|
||||
{
|
||||
if (!isMouseDown)
|
||||
{
|
||||
for (auto id : regionSelection)
|
||||
{
|
||||
auto it = spritesheet->regions.find(id);
|
||||
if (it == spritesheet->regions.end()) continue;
|
||||
|
||||
auto& region = it->second;
|
||||
auto minPoint = glm::min(region.crop, region.crop + region.size);
|
||||
auto maxPoint = glm::max(region.crop, region.crop + region.size);
|
||||
region.crop = clamp_vec2_to_int(minPoint);
|
||||
region.size = clamp_vec2_to_int(maxPoint - minPoint);
|
||||
|
||||
if (isGridSnap)
|
||||
{
|
||||
auto [snapMin, snapMax] = snap_rect(region.crop, region.crop + region.size);
|
||||
region.crop = clamp_vec2_to_int(snapMin);
|
||||
region.size = clamp_vec2_to_int(snapMax - snapMin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
auto it = spritesheet->regions.find(*regionSelection.begin());
|
||||
if (it != spritesheet->regions.end())
|
||||
{
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_CROP),
|
||||
std::make_format_args(it->second.crop.x, it->second.crop.y))
|
||||
.c_str());
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_SIZE),
|
||||
std::make_format_args(it->second.size.x, it->second.size.y))
|
||||
.c_str());
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnd) document.change(Document::SPRITESHEETS);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!item) break;
|
||||
if (isBegin) document.snapshot(localize.get(EDIT_FRAME_CROP));
|
||||
|
||||
if (isMouseClicked)
|
||||
{
|
||||
cropAnchor = mousePos;
|
||||
frame->crop = vec2(ivec2(cropAnchor));
|
||||
frame->size = vec2();
|
||||
frame_change_apply({.cropX = (int)cropAnchor.x, .cropY = (int)cropAnchor.y, .sizeX = {}, .sizeY = {}});
|
||||
}
|
||||
if (isMouseDown)
|
||||
{
|
||||
auto [minPoint, maxPoint] = snap_rect(glm::min(cropAnchor, mousePos), glm::max(cropAnchor, mousePos));
|
||||
frame->crop = vec2(ivec2(minPoint));
|
||||
frame->size = vec2(ivec2(maxPoint - minPoint));
|
||||
frame_change_apply({.cropX = minPoint.x,
|
||||
.cropY = minPoint.y,
|
||||
.sizeX = maxPoint.x - minPoint.x,
|
||||
.sizeY = maxPoint.y - minPoint.y});
|
||||
}
|
||||
if (isLeftPressed) frame->crop.x -= stepX;
|
||||
if (isRightPressed) frame->crop.x += stepX;
|
||||
if (isUpPressed) frame->crop.y -= stepY;
|
||||
if (isDownPressed) frame->crop.y += stepY;
|
||||
frame->crop = vec2(ivec2(frame->crop));
|
||||
frame->size = vec2(ivec2(frame->size));
|
||||
if (isLeftPressed) frame_change_apply({.cropX = stepX}, anm2::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.cropX = stepX}, anm2::ADD);
|
||||
if (isUpPressed) frame_change_apply({.cropY = stepY}, anm2::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.cropY = stepY}, anm2::ADD);
|
||||
|
||||
frame_change_apply(
|
||||
{.cropX = frame->crop.x, .cropY = frame->crop.y, .sizeX = frame->size.x, .sizeY = frame->size.y});
|
||||
|
||||
if (isDuring)
|
||||
{
|
||||
if (!isMouseDown)
|
||||
{
|
||||
auto minPoint = glm::min(frame->crop, frame->crop + frame->size);
|
||||
auto maxPoint = glm::max(frame->crop, frame->crop + frame->size);
|
||||
frame->crop = vec2(ivec2(minPoint));
|
||||
frame->size = vec2(ivec2(maxPoint - minPoint));
|
||||
|
||||
frame_change_apply({.cropX = minPoint.x,
|
||||
.cropY = minPoint.y,
|
||||
.sizeX = maxPoint.x - minPoint.x,
|
||||
.sizeY = maxPoint.y - minPoint.y});
|
||||
|
||||
if (isGridSnap)
|
||||
{
|
||||
auto [snapMin, snapMax] = snap_rect(frame->crop, frame->crop + frame->size);
|
||||
frame->crop = snapMin;
|
||||
frame->size = snapMax - snapMin;
|
||||
|
||||
frame_change_apply({.cropX = snapMin.x,
|
||||
.cropY = snapMin.y,
|
||||
.sizeX = snapMax.x - snapMin.x,
|
||||
.sizeY = snapMax.y - snapMin.y});
|
||||
}
|
||||
}
|
||||
if (ImGui::BeginTooltip())
|
||||
@@ -387,7 +660,10 @@ namespace anm2ed::imgui
|
||||
if (isMouseClicked)
|
||||
document.snapshot(useTool == tool::DRAW ? localize.get(EDIT_DRAW) : localize.get(EDIT_ERASE));
|
||||
if (isMouseDown) spritesheet->texture.pixel_line(ivec2(previousMousePos), ivec2(mousePos), color);
|
||||
if (isMouseReleased) document.change(Document::SPRITESHEETS);
|
||||
if (isMouseReleased)
|
||||
{
|
||||
document.change(Document::SPRITESHEETS);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case tool::COLOR_PICKER:
|
||||
@@ -413,6 +689,75 @@ namespace anm2ed::imgui
|
||||
break;
|
||||
}
|
||||
|
||||
if (tool == tool::PAN && hoveredRegionId != -1 && spritesheet)
|
||||
{
|
||||
auto regionIt = spritesheet->regions.find(hoveredRegionId);
|
||||
if (regionIt != spritesheet->regions.end())
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
auto& region = regionIt->second;
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(region.name.c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_ID), std::make_format_args(hoveredRegionId)).c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region.crop.x, region.crop.y)).c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region.size.x, region.size.y)).c_str());
|
||||
if (region.origin == anm2::Spritesheet::Region::CUSTOM)
|
||||
{
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y))
|
||||
.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
StringType originString = LABEL_REGION_ORIGIN_CENTER;
|
||||
if (region.origin == anm2::Spritesheet::Region::TOP_LEFT) originString = LABEL_REGION_ORIGIN_TOP_LEFT;
|
||||
auto originLabel = localize.get(originString);
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_ORIGIN), std::make_format_args(originLabel)).c_str());
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((isMouseDown || isKeyDown) && useTool != tool::PAN)
|
||||
{
|
||||
if (!isAreaAllowed && areaType == tool::ANIMATION_PREVIEW)
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
ImGui::TextUnformatted(localize.get(TEXT_TOOL_ANIMATION_PREVIEW));
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
else if (isSpritesheetRequired && !isSpritesheetAvailable)
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
ImGui::TextUnformatted(localize.get(TEXT_SELECT_SPRITESHEET));
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
else if (isFrameRequired && !isFrameAvailable)
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
if (isRegionInUse)
|
||||
ImGui::TextUnformatted(localize.get(TEXT_REGION_IN_USE));
|
||||
else if (useTool == tool::CROP)
|
||||
ImGui::TextUnformatted(localize.get(TEXT_SELECT_FRAME_OR_REGION));
|
||||
else
|
||||
ImGui::TextUnformatted(localize.get(TEXT_SELECT_FRAME));
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseWheel != 0 || isZoomIn || isZoomOut)
|
||||
{
|
||||
auto focus = mouseWheel != 0 ? vec2(mousePos) : vec2();
|
||||
@@ -425,7 +770,33 @@ namespace anm2ed::imgui
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (tool == tool::PAN &&
|
||||
ImGui::BeginPopupContextWindow("##Spritesheet Editor Context Menu", ImGuiMouseButton_Right))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_CENTER_VIEW), settings.shortcutCenterView.c_str())) center_view();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_FIT), settings.shortcutFit.c_str(), false,
|
||||
spritesheet && spritesheet->texture.is_valid()))
|
||||
fit_view();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_IN), settings.shortcutZoomIn.c_str())) zoom_in();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_OUT), settings.shortcutZoomOut.c_str())) zoom_out();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (!document.isSpritesheetEditorSet)
|
||||
{
|
||||
@@ -439,5 +810,7 @@ namespace anm2ed::imgui
|
||||
|
||||
settings.editorSize = size;
|
||||
settings.editorStartZoom = zoom;
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "canvas.h"
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include "canvas.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -17,6 +17,7 @@ namespace anm2ed::imgui
|
||||
float checkerSyncZoom{};
|
||||
bool isCheckerPanInitialized{};
|
||||
bool hasPendingZoomPanAdjust{};
|
||||
int hoveredRegionId{-1};
|
||||
|
||||
public:
|
||||
SpritesheetEditor();
|
||||
+542
-184
@@ -1,14 +1,19 @@
|
||||
#include "spritesheets.h"
|
||||
#include "spritesheets.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
|
||||
#include "log.h"
|
||||
#include "document.h"
|
||||
#include "filesystem_.h"
|
||||
#include "strings.h"
|
||||
#include "toast.h"
|
||||
#include "document.hpp"
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "working_directory.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::resource;
|
||||
@@ -17,77 +22,326 @@ using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
static constexpr auto PADDING_MAX = 100;
|
||||
|
||||
void Spritesheets::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog,
|
||||
Clipboard& clipboard)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& selection = document.spritesheet.selection;
|
||||
auto& unused = document.spritesheet.unused;
|
||||
auto& hovered = document.spritesheet.hovered;
|
||||
auto& reference = document.spritesheet.reference;
|
||||
auto& region = document.region;
|
||||
auto style = ImGui::GetStyle();
|
||||
std::function<void()> pack{};
|
||||
|
||||
hovered = -1;
|
||||
auto add_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_OPEN); };
|
||||
auto replace_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_REPLACE); };
|
||||
auto set_file_path_open = [&]() { dialog.file_save(Dialog::SPRITESHEET_PATH_SET); };
|
||||
auto merge_open = [&]()
|
||||
{
|
||||
if (selection.size() <= 1) return;
|
||||
mergeSelection = selection;
|
||||
mergePopup.open();
|
||||
};
|
||||
auto pack_open = [&]()
|
||||
{
|
||||
if (selection.size() != 1) return;
|
||||
auto id = *selection.begin();
|
||||
if (!anm2.content.spritesheets.contains(id)) return;
|
||||
if (anm2.content.spritesheets.at(id).regions.empty()) return;
|
||||
packId = id;
|
||||
packPopup.open();
|
||||
};
|
||||
|
||||
auto add = [&](const std::filesystem::path& path)
|
||||
{
|
||||
if (path.empty()) return;
|
||||
document.spritesheet_add(path);
|
||||
newSpritesheetId = document.spritesheet.reference;
|
||||
};
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.spritesheets_unused();
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
{
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
anm2.content.spritesheets.erase(id);
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_SPRITESHEETS), Document::ALL, behavior());
|
||||
};
|
||||
|
||||
auto reload = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : selection)
|
||||
{
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
spritesheet.reload(document.directory_get());
|
||||
document.spritesheet_hash_set_saved(id);
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_RELOAD_SPRITESHEETS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto replace = [&](const std::filesystem::path& path)
|
||||
{
|
||||
if (selection.size() != 1 || path.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto& id = *selection.begin();
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
spritesheet.reload(document.directory_get(), path);
|
||||
document.spritesheet_hash_set_saved(id);
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SPRITESHEET), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto set_file_path = [&](const std::filesystem::path& path)
|
||||
{
|
||||
if (selection.size() != 1 || path.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto id = *selection.begin();
|
||||
if (!anm2.content.spritesheets.contains(id)) return;
|
||||
WorkingDirectory workingDirectory(document.directory_get());
|
||||
anm2.content.spritesheets[id].path = path::make_relative(path);
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_SPRITESHEET_FILE_PATH), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto save = [&](const std::set<int>& ids)
|
||||
{
|
||||
if (ids.empty()) return;
|
||||
|
||||
for (auto& id : ids)
|
||||
{
|
||||
if (!anm2.content.spritesheets.contains(id)) continue;
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
if (spritesheet.save(document.directory_get()))
|
||||
{
|
||||
document.spritesheet_hash_set_saved(id);
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED), std::make_format_args(id, pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto save_open = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
if (settings.fileIsWarnOverwrite)
|
||||
{
|
||||
saveSelection = selection;
|
||||
overwritePopup.open();
|
||||
}
|
||||
else
|
||||
{
|
||||
save(selection);
|
||||
}
|
||||
};
|
||||
|
||||
auto merge = [&]()
|
||||
{
|
||||
if (mergeSelection.size() <= 1) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto baseID = *mergeSelection.begin();
|
||||
if (anm2.spritesheets_merge(mergeSelection, (anm2::SpritesheetMergeOrigin)settings.mergeSpritesheetsOrigin,
|
||||
settings.mergeSpritesheetsIsMakeRegions,
|
||||
settings.mergeSpritesheetsIsMakePrimaryRegion,
|
||||
(origin::Type)settings.mergeSpritesheetsRegionOrigin))
|
||||
{
|
||||
selection = {baseID};
|
||||
reference = baseID;
|
||||
region.reference = -1;
|
||||
region.selection.clear();
|
||||
toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS));
|
||||
logger.info(localize.get(TOAST_MERGE_SPRITESHEETS, anm2ed::ENGLISH));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED));
|
||||
logger.error(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED, anm2ed::ENGLISH));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_MERGE_SPRITESHEETS), Document::ALL, behavior());
|
||||
};
|
||||
pack = [&]()
|
||||
{
|
||||
int id = packId != -1 ? packId : (selection.size() == 1 ? *selection.begin() : -1);
|
||||
if (id == -1) return;
|
||||
if (!anm2.content.spritesheets.contains(id)) return;
|
||||
if (anm2.content.spritesheets.at(id).regions.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto padding = std::max(0, settings.packPadding);
|
||||
if (anm2.spritesheet_pack(id, padding))
|
||||
{
|
||||
toasts.push(localize.get(TOAST_PACK_SPRITESHEET));
|
||||
logger.info(localize.get(TOAST_PACK_SPRITESHEET, anm2ed::ENGLISH));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(localize.get(TOAST_PACK_SPRITESHEET_FAILED));
|
||||
logger.error(localize.get(TOAST_PACK_SPRITESHEET_FAILED, anm2ed::ENGLISH));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PACK_SPRITESHEET), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto open_directory = [&](anm2::Spritesheet& spritesheet)
|
||||
{
|
||||
if (spritesheet.path.empty()) return;
|
||||
std::error_code ec{};
|
||||
auto absolutePath = std::filesystem::weakly_canonical(document.directory_get() / spritesheet.path, ec);
|
||||
if (ec) absolutePath = document.directory_get() / spritesheet.path;
|
||||
auto target = std::filesystem::is_directory(absolutePath) ? absolutePath
|
||||
: std::filesystem::is_directory(absolutePath.parent_path()) ? absolutePath.parent_path()
|
||||
: document.directory_get();
|
||||
dialog.file_explorer_open(target);
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.spritesheets[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxSpritesheetIdBefore =
|
||||
anm2.content.spritesheets.empty() ? -1 : anm2.content.spritesheets.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_SPRITESHEETS));
|
||||
if (anm2.spritesheets_deserialize(clipboard.get(), document.directory_get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.spritesheets.empty())
|
||||
{
|
||||
auto maxSpritesheetIdAfter = anm2.content.spritesheets.rbegin()->first;
|
||||
if (maxSpritesheetIdAfter > maxSpritesheetIdBefore)
|
||||
{
|
||||
newSpritesheetId = maxSpritesheetIdAfter;
|
||||
selection = {maxSpritesheetIdAfter};
|
||||
reference = maxSpritesheetIdAfter;
|
||||
region.reference = -1;
|
||||
region.selection.clear();
|
||||
}
|
||||
}
|
||||
document.change(Document::SPRITESHEETS);
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_FAILED), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_SPRITESHEETS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto context_menu = [&]()
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Right))
|
||||
ImGui::OpenPopup("##Spritesheet Context Menu");
|
||||
|
||||
if (ImGui::BeginPopup("##Spritesheet Context Menu"))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_OPEN_DIRECTORY), nullptr, false, selection.size() == 1))
|
||||
open_directory(anm2.content.spritesheets[*selection.begin()]);
|
||||
if (ImGui::MenuItem(localize.get(BASIC_SET_FILE_PATH), nullptr, false, selection.size() == 1))
|
||||
set_file_path_open();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
bool isPackable = selection.size() == 1 && anm2.content.spritesheets.contains(*selection.begin()) &&
|
||||
!anm2.content.spritesheets.at(*selection.begin()).regions.empty();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_RELOAD), nullptr, false, !selection.empty())) reload();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REPLACE), nullptr, false, selection.size() == 1)) replace_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_MERGE), settings.shortcutMerge.c_str(), false, selection.size() > 1))
|
||||
merge_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PACK), nullptr, false, isPackable)) pack_open();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PACK_SPRITESHEET));
|
||||
if (ImGui::MenuItem(localize.get(BASIC_SAVE), nullptr, false, !selection.empty())) save_open();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) copy();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_SPRITESHEETS_WINDOW), &settings.windowIsSpritesheets))
|
||||
{
|
||||
auto style = ImGui::GetStyle();
|
||||
|
||||
auto context_menu = [&]()
|
||||
{
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (!selection.empty())
|
||||
{
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.spritesheets[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
}
|
||||
else if (hovered > -1)
|
||||
clipboard.set(anm2.content.spritesheets[hovered].to_string(hovered));
|
||||
};
|
||||
|
||||
auto paste = [&](merge::Type type)
|
||||
{
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_SPRITESHEETS));
|
||||
if (anm2.spritesheets_deserialize(clipboard.get(), document.directory_get().string(), type, &errorString))
|
||||
document.change(Document::SPRITESHEETS);
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_FAILED),
|
||||
std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_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);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
|
||||
{
|
||||
ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false, true);
|
||||
|
||||
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::PopStyleVar(2);
|
||||
};
|
||||
|
||||
auto childSize = size_without_footer_get(2);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
@@ -97,41 +351,81 @@ namespace anm2ed::imgui
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
|
||||
|
||||
selection.start(anm2.content.spritesheets.size(), ImGuiMultiSelectFlags_ClearOnEscape);
|
||||
selection.start(anm2.content.spritesheets.size());
|
||||
if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused))
|
||||
{
|
||||
selection.clear();
|
||||
for (auto& id : anm2.content.spritesheets | std::views::keys)
|
||||
selection.insert(id);
|
||||
}
|
||||
if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear();
|
||||
auto scroll_to_item = [&](float itemHeight, bool isTarget)
|
||||
{
|
||||
if (!isTarget) return;
|
||||
auto windowHeight = ImGui::GetWindowHeight();
|
||||
auto targetTop = ImGui::GetCursorPosY();
|
||||
auto targetBottom = targetTop + itemHeight;
|
||||
auto visibleTop = ImGui::GetScrollY();
|
||||
auto visibleBottom = visibleTop + windowHeight;
|
||||
if (targetTop < visibleTop)
|
||||
ImGui::SetScrollY(targetTop);
|
||||
else if (targetBottom > visibleBottom)
|
||||
ImGui::SetScrollY(targetBottom - windowHeight);
|
||||
};
|
||||
int scrollTargetId = -1;
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
(ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)))
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
ids.reserve(anm2.content.spritesheets.size());
|
||||
for (auto& [id, sheet] : anm2.content.spritesheets)
|
||||
ids.push_back(id);
|
||||
if (!ids.empty())
|
||||
{
|
||||
int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1;
|
||||
int current = reference;
|
||||
if (current == -1 && !selection.empty()) current = *selection.begin();
|
||||
auto it = std::find(ids.begin(), ids.end(), current);
|
||||
int index = it == ids.end() ? 0 : (int)std::distance(ids.begin(), it);
|
||||
index = std::clamp(index + delta, 0, (int)ids.size() - 1);
|
||||
int nextId = ids[index];
|
||||
selection = {nextId};
|
||||
reference = nextId;
|
||||
region.reference = -1;
|
||||
region.selection.clear();
|
||||
scrollTargetId = nextId;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [id, spritesheet] : anm2.content.spritesheets)
|
||||
{
|
||||
auto isNewSpritesheet = newSpritesheetId == id;
|
||||
ImGui::PushID(id);
|
||||
|
||||
scroll_to_item(spritesheetChildSize.y, scrollTargetId == id);
|
||||
|
||||
if (ImGui::BeginChild("##Spritesheet Child", spritesheetChildSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto isSelected = selection.contains(id);
|
||||
auto isReferenced = id == reference;
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
bool isTextureValid = spritesheet.texture.is_valid();
|
||||
auto& texture = isTextureValid ? spritesheet.texture : resources.icons[icon::NONE];
|
||||
auto textureRef = ImTextureRef(texture.id);
|
||||
auto tintColor = !isTextureValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
auto pathString = spritesheet.path.empty() ? std::string{anm2::NO_PATH} : spritesheet.path.string();
|
||||
bool isValid = spritesheet.texture.is_valid();
|
||||
auto& texture = isValid ? spritesheet.texture : resources.icons[icon::NONE];
|
||||
auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
auto pathCStr = pathString.c_str();
|
||||
|
||||
ImGui::SetNextItemSelectionUserData(id);
|
||||
ImGui::SetNextItemStorageID(id);
|
||||
if (ImGui::Selectable("##Spritesheet Selectable", isSelected, 0, spritesheetChildSize)) reference = id;
|
||||
if (ImGui::IsItemHovered())
|
||||
if (ImGui::Selectable("##Spritesheet Selectable", isSelected, 0, spritesheetChildSize))
|
||||
{
|
||||
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
filesystem::WorkingDirectory workingDirectory(document.directory_get().string());
|
||||
dialog.file_explorer_open(spritesheet.path.parent_path().string());
|
||||
}
|
||||
hovered = id;
|
||||
}
|
||||
if (newSpritesheetId == id)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newSpritesheetId = -1;
|
||||
reference = id;
|
||||
region.reference = -1;
|
||||
region.selection.clear();
|
||||
}
|
||||
if (scrollTargetId == id) ImGui::SetItemDefaultFocus();
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
open_directory(spritesheet);
|
||||
|
||||
auto viewport = ImGui::GetMainViewport();
|
||||
auto maxPreviewSize = to_vec2(viewport->Size) * 0.5f;
|
||||
@@ -157,7 +451,7 @@ namespace anm2ed::imgui
|
||||
auto noScrollFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
||||
if (ImGui::BeginChild("##Spritesheet Tooltip Image Child", to_imvec2(textureSize), childFlags,
|
||||
noScrollFlags))
|
||||
ImGui::ImageWithBg(textureRef, to_imvec2(textureSize), ImVec2(), ImVec2(1, 1), ImVec4(), tintColor);
|
||||
ImGui::ImageWithBg(texture.id, to_imvec2(textureSize), ImVec2(), ImVec2(1, 1), ImVec4(), tintColor);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
@@ -172,7 +466,7 @@ namespace anm2ed::imgui
|
||||
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||
|
||||
if (!isTextureValid)
|
||||
if (!isValid)
|
||||
ImGui::TextUnformatted(localize.get(TOOLTIP_SPRITESHEET_INVALID));
|
||||
else
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_TEXTURE_SIZE),
|
||||
@@ -196,156 +490,220 @@ namespace anm2ed::imgui
|
||||
imageSize.y = imageSize.x / aspectRatio;
|
||||
|
||||
ImGui::SetCursorPos(cursorPos);
|
||||
ImGui::ImageWithBg(textureRef, imageSize, ImVec2(), ImVec2(1, 1), ImVec4(), tintColor);
|
||||
ImGui::ImageWithBg(texture.id, imageSize, ImVec2(), ImVec2(1, 1), ImVec4(), tintColor);
|
||||
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(spritesheetChildSize.y + style.ItemSpacing.x,
|
||||
spritesheetChildSize.y - spritesheetChildSize.y / 2 - ImGui::GetTextLineHeight() / 2));
|
||||
|
||||
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathCStr)).c_str());
|
||||
auto spritesheetLabel = std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathCStr));
|
||||
if (document.spritesheet_is_dirty(id))
|
||||
spritesheetLabel =
|
||||
std::vformat(localize.get(FORMAT_SPRITESHEET_NOT_SAVED), std::make_format_args(spritesheetLabel));
|
||||
ImGui::TextUnformatted(spritesheetLabel.c_str());
|
||||
if (isReferenced) ImGui::PopFont();
|
||||
|
||||
context_menu();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (isNewSpritesheet)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newSpritesheetId = -1;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
context_menu();
|
||||
selection.finish();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
context_menu();
|
||||
|
||||
auto rowOneWidgetSize = widget_size_with_row_get(3);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), rowOneWidgetSize)) dialog.file_open(dialog::SPRITESHEET_OPEN);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), rowOneWidgetSize)) add_open();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_SPRITESHEET), settings.shortcutAdd);
|
||||
|
||||
if (dialog.is_selected(dialog::SPRITESHEET_OPEN))
|
||||
if (dialog.is_selected(Dialog::SPRITESHEET_OPEN))
|
||||
{
|
||||
document.spritesheet_add(dialog.path);
|
||||
newSpritesheetId = document.spritesheet.reference;
|
||||
add(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.empty());
|
||||
{
|
||||
if (ImGui::Button(localize.get(BASIC_RELOAD), rowOneWidgetSize))
|
||||
{
|
||||
auto reload = [&]()
|
||||
{
|
||||
for (auto& id : selection)
|
||||
{
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
spritesheet.reload(document.directory_get());
|
||||
auto pathString = spritesheet.path.string();
|
||||
toasts.push(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET),
|
||||
std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_RELOAD_SPRITESHEETS), Document::SPRITESHEETS, reload());
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RELOAD_SPRITESHEETS));
|
||||
}
|
||||
if (ImGui::Button(localize.get(BASIC_RELOAD), rowOneWidgetSize)) reload();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RELOAD_SPRITESHEETS));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.size() != 1);
|
||||
{
|
||||
if (ImGui::Button(localize.get(BASIC_REPLACE), rowOneWidgetSize)) dialog.file_open(dialog::SPRITESHEET_REPLACE);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SPRITESHEET));
|
||||
}
|
||||
if (ImGui::Button(localize.get(BASIC_REPLACE), rowOneWidgetSize)) replace_open();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SPRITESHEET));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (dialog.is_selected(dialog::SPRITESHEET_REPLACE))
|
||||
if (dialog.is_selected(Dialog::SPRITESHEET_REPLACE))
|
||||
{
|
||||
auto replace = [&]()
|
||||
{
|
||||
auto& id = *selection.begin();
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
spritesheet = anm2::Spritesheet(document.directory_get().string(), dialog.path);
|
||||
auto pathString = spritesheet.path.string();
|
||||
toasts.push(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
};
|
||||
replace(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SPRITESHEET), Document::SPRITESHEETS, replace());
|
||||
if (dialog.is_selected(Dialog::SPRITESHEET_PATH_SET))
|
||||
{
|
||||
set_file_path(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
auto rowTwoWidgetSize = widget_size_with_row_get(2);
|
||||
|
||||
ImGui::BeginDisabled(unused.empty());
|
||||
{
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), rowTwoWidgetSize))
|
||||
{
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
{
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
auto pathString = spritesheet.path.string();
|
||||
toasts.push(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET),
|
||||
std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
anm2.content.spritesheets.erase(id);
|
||||
}
|
||||
unused.clear();
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_SPRITESHEETS), Document::SPRITESHEETS,
|
||||
remove_unused());
|
||||
}
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_SPRITESHEETS), settings.shortcutRemove);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), rowTwoWidgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_SPRITESHEETS), settings.shortcutRemove);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.empty());
|
||||
{
|
||||
if (ImGui::Button(localize.get(BASIC_SAVE), rowTwoWidgetSize))
|
||||
{
|
||||
for (auto& id : selection)
|
||||
{
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
auto pathString = spritesheet.path.string();
|
||||
if (spritesheet.save(document.directory_get().string()))
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET),
|
||||
std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED),
|
||||
std::make_format_args(id, pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ImGui::Button(localize.get(BASIC_SAVE), rowTwoWidgetSize)) save_open();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SAVE_SPRITESHEETS));
|
||||
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add_open();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED) && selection.size() > 1) merge_open();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
mergePopup.trigger();
|
||||
if (ImGui::BeginPopupModal(mergePopup.label(), &mergePopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
settings.mergeSpritesheetsRegionOrigin =
|
||||
glm::clamp(settings.mergeSpritesheetsRegionOrigin, (int)origin::TOP_LEFT, (int)origin::ORIGIN_CENTER);
|
||||
|
||||
auto close = [&]()
|
||||
{
|
||||
mergeSelection.clear();
|
||||
mergePopup.close();
|
||||
};
|
||||
|
||||
auto optionsSize = child_size_get(6);
|
||||
if (ImGui::BeginChild("##Merge Spritesheets Options", optionsSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
ImGui::SeparatorText(localize.get(LABEL_REGION_PROPERTIES_ORIGIN));
|
||||
ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_BOTTOM), &settings.mergeSpritesheetsOrigin,
|
||||
anm2::APPEND_BOTTOM);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_BOTTOM_LEFT));
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_RIGHT), &settings.mergeSpritesheetsOrigin,
|
||||
anm2::APPEND_RIGHT);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_TOP_RIGHT));
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_OPTIONS));
|
||||
ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_SPRITESHEET_REGIONS), &settings.mergeSpritesheetsIsMakeRegions);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_SPRITESHEET_REGIONS));
|
||||
|
||||
ImGui::BeginDisabled(!settings.mergeSpritesheetsIsMakeRegions);
|
||||
ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION),
|
||||
&settings.mergeSpritesheetsIsMakePrimaryRegion);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION));
|
||||
|
||||
const char* regionOriginOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT),
|
||||
localize.get(LABEL_REGION_ORIGIN_CENTER)};
|
||||
ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), &settings.mergeSpritesheetsRegionOrigin,
|
||||
regionOriginOptions, IM_ARRAYSIZE(regionOriginOptions));
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
ImGui::BeginDisabled(mergeSelection.size() <= 1);
|
||||
if (ImGui::Button(localize.get(BASIC_MERGE), widgetSize))
|
||||
{
|
||||
merge();
|
||||
close();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
mergePopup.end();
|
||||
|
||||
packPopup.trigger();
|
||||
if (ImGui::BeginPopupModal(packPopup.label(), &packPopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
settings.packPadding = std::max(0, settings.packPadding);
|
||||
|
||||
auto close = [&]()
|
||||
{
|
||||
packId = -1;
|
||||
packPopup.close();
|
||||
};
|
||||
|
||||
auto optionsSize = child_size_get(1);
|
||||
if (ImGui::BeginChild("##Pack Spritesheet Options", optionsSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
ImGui::DragInt(localize.get(LABEL_PACK_PADDING), &settings.packPadding, DRAG_SPEED, 0, PADDING_MAX);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
bool isPackable = packId != -1 && anm2.content.spritesheets.contains(packId) &&
|
||||
!anm2.content.spritesheets.at(packId).regions.empty();
|
||||
ImGui::BeginDisabled(!isPackable);
|
||||
if (ImGui::Button(localize.get(BASIC_PACK), widgetSize))
|
||||
{
|
||||
if (pack) pack();
|
||||
close();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
packPopup.end();
|
||||
|
||||
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))
|
||||
{
|
||||
save(saveSelection);
|
||||
saveSelection.clear();
|
||||
overwritePopup.close();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_NO), widgetSize))
|
||||
{
|
||||
saveSelection.clear();
|
||||
overwritePopup.close();
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
overwritePopup.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "dialog.h"
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Spritesheets
|
||||
{
|
||||
int newSpritesheetId{-1};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&, Resources&, Dialog&, Clipboard& clipboard);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "dialog.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Spritesheets
|
||||
{
|
||||
int newSpritesheetId{-1};
|
||||
PopupHelper mergePopup{PopupHelper(LABEL_SPRITESHEETS_MERGE_POPUP, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
PopupHelper packPopup{PopupHelper(LABEL_SPRITESHEETS_PACK_POPUP, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
PopupHelper overwritePopup{PopupHelper(LABEL_TASKBAR_OVERWRITE_FILE, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
std::set<int> mergeSelection{};
|
||||
int packId{-1};
|
||||
std::set<int> saveSelection{};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&, Resources&, Dialog&, Clipboard& clipboard);
|
||||
};
|
||||
}
|
||||
+560
-653
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,11 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include "strings.h"
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -26,11 +26,12 @@ namespace anm2ed::imgui
|
||||
PopupHelper propertiesPopup{PopupHelper(LABEL_TIMELINE_PROPERTIES_POPUP, POPUP_NORMAL)};
|
||||
PopupHelper bakePopup{PopupHelper(LABEL_TIMELINE_BAKE_POPUP, POPUP_SMALL_NO_HEIGHT)};
|
||||
std::string addItemName{"New Item"};
|
||||
bool addItemIsRect{};
|
||||
bool addItemIsShowRect{};
|
||||
int addItemID{-1};
|
||||
int addItemSpritesheetID{-1};
|
||||
int hoveredTime{};
|
||||
anm2::Frame* draggedFrame{};
|
||||
anm2::Type draggedFrameType{};
|
||||
int draggedFrameIndex{-1};
|
||||
int draggedFrameStart{-1};
|
||||
int draggedFrameStartDuration{-1};
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
#include "tools.h"
|
||||
#include "tools.hpp"
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#include "strings.h"
|
||||
#include "tool.h"
|
||||
#include "types.h"
|
||||
#include "strings.hpp"
|
||||
#include "tool.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::types;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "manager.h"
|
||||
#include "resources.h"
|
||||
#include "settings.h"
|
||||
#include "strings.h"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -1,9 +1,12 @@
|
||||
#include "welcome.h"
|
||||
#include "welcome.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "strings.h"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::resource;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
@@ -12,6 +15,8 @@ namespace anm2ed::imgui
|
||||
{
|
||||
auto viewport = ImGui::GetMainViewport();
|
||||
auto windowHeight = viewport->Size.y - taskbar.height - documents.height;
|
||||
if (windowHeight < 1.0f)
|
||||
windowHeight = 1.0f;
|
||||
|
||||
ImGui::SetNextWindowViewport(viewport->ID);
|
||||
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height + documents.height));
|
||||
@@ -31,9 +36,11 @@ namespace anm2ed::imgui
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_NEW), widgetSize)) dialog.file_save(dialog::ANM2_NEW); // handled in taskbar.cpp
|
||||
if (ImGui::Button(localize.get(BASIC_NEW), widgetSize))
|
||||
dialog.file_save(Dialog::ANM2_NEW); // handled in taskbar.cpp
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(localize.get(BASIC_OPEN), widgetSize)) dialog.file_open(dialog::ANM2_OPEN); // handled in taskbar.cpp
|
||||
if (ImGui::Button(localize.get(BASIC_OPEN), widgetSize))
|
||||
dialog.file_open(Dialog::ANM2_OPEN); // handled in taskbar.cpp
|
||||
|
||||
if (ImGui::BeginChild("##Recent Files Child", {}, ImGuiChildFlags_Borders))
|
||||
{
|
||||
@@ -42,7 +49,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
ImGui::PushID(i);
|
||||
|
||||
auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string());
|
||||
auto label = std::format(FILE_LABEL_FORMAT, path::to_utf8(file.filename()), path::to_utf8(file));
|
||||
|
||||
if (ImGui::Selectable(label.c_str()))
|
||||
{
|
||||
@@ -73,7 +80,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
for (auto& file : manager.autosaveFiles)
|
||||
{
|
||||
auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string());
|
||||
auto label = std::format(FILE_LABEL_FORMAT, path::to_utf8(file.filename()), path::to_utf8(file));
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
}
|
||||
}
|
||||
@@ -97,6 +104,8 @@ namespace anm2ed::imgui
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
selectable_input_text_id() = {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "documents.h"
|
||||
#include "manager.h"
|
||||
#include "strings.h"
|
||||
#include "taskbar.h"
|
||||
#include "documents.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "taskbar.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
@@ -0,0 +1,193 @@
|
||||
#include "about.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <imgui.h>
|
||||
#include <vector>
|
||||
|
||||
#include "strings.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
|
||||
namespace anm2ed::imgui::wizard
|
||||
{
|
||||
static constexpr auto CREDIT_DELAY = 1.0f;
|
||||
static constexpr auto CREDIT_SCROLL_SPEED = 25.0f;
|
||||
|
||||
static constexpr About::Credit CREDITS[] = {
|
||||
{"Anm2Ed", font::BOLD},
|
||||
{"License: GPLv3"},
|
||||
{""},
|
||||
{"Designer", font::BOLD},
|
||||
{"Shweet"},
|
||||
{""},
|
||||
{"Additional Help", font::BOLD},
|
||||
{"im-tem"},
|
||||
{""},
|
||||
{"Localization", font::BOLD},
|
||||
{"Gabriel Asencio (Spanish (Latin America))"},
|
||||
{"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)"},
|
||||
{"Isaac Reflashed team"},
|
||||
{"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(About::Credit));
|
||||
|
||||
void About::reset(Resources& resources)
|
||||
{
|
||||
resources.music_track().play(true);
|
||||
creditsState = {};
|
||||
creditsState.spawnTimer = CREDIT_DELAY;
|
||||
}
|
||||
|
||||
void About::update(Resources& resources)
|
||||
{
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../resources.hpp"
|
||||
|
||||
namespace anm2ed::imgui::wizard
|
||||
{
|
||||
class About
|
||||
{
|
||||
public:
|
||||
struct Credit
|
||||
{
|
||||
const char* string{};
|
||||
resource::font::Type font{resource::font::REGULAR};
|
||||
};
|
||||
|
||||
struct ScrollingCredit
|
||||
{
|
||||
int index{};
|
||||
float offset{};
|
||||
};
|
||||
|
||||
struct CreditsState
|
||||
{
|
||||
std::vector<ScrollingCredit> active{};
|
||||
float spawnTimer{1.0f};
|
||||
int nextIndex{};
|
||||
};
|
||||
|
||||
int creditsIndex{};
|
||||
CreditsState creditsState{};
|
||||
|
||||
void reset(Resources& resources);
|
||||
void update(Resources& resources);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
#include "change_all_frame_properties.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "math_.hpp"
|
||||
|
||||
using namespace anm2ed::util::math;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui::wizard
|
||||
{
|
||||
void ChangeAllFrameProperties::update(Document& document, Settings& settings)
|
||||
{
|
||||
isChanged = false;
|
||||
|
||||
auto& frames = document.frames.selection;
|
||||
auto& isCropX = settings.changeIsCropX;
|
||||
auto& isCropY = settings.changeIsCropY;
|
||||
auto& isSizeX = settings.changeIsSizeX;
|
||||
auto& isSizeY = settings.changeIsSizeY;
|
||||
auto& isPositionX = settings.changeIsPositionX;
|
||||
auto& isPositionY = settings.changeIsPositionY;
|
||||
auto& isPivotX = settings.changeIsPivotX;
|
||||
auto& isPivotY = settings.changeIsPivotY;
|
||||
auto& isScaleX = settings.changeIsScaleX;
|
||||
auto& isScaleY = settings.changeIsScaleY;
|
||||
auto& isRotation = settings.changeIsRotation;
|
||||
auto& isDuration = settings.changeIsDuration;
|
||||
auto& isTintR = settings.changeIsTintR;
|
||||
auto& isTintG = settings.changeIsTintG;
|
||||
auto& isTintB = settings.changeIsTintB;
|
||||
auto& isTintA = settings.changeIsTintA;
|
||||
auto& isColorOffsetR = settings.changeIsColorOffsetR;
|
||||
auto& isColorOffsetG = settings.changeIsColorOffsetG;
|
||||
auto& isColorOffsetB = settings.changeIsColorOffsetB;
|
||||
auto& isVisibleSet = settings.changeIsVisibleSet;
|
||||
auto& isInterpolationSet = settings.changeIsInterpolationSet;
|
||||
auto& isFlipXSet = settings.changeIsFlipXSet;
|
||||
auto& isFlipYSet = settings.changeIsFlipYSet;
|
||||
auto& isRegion = settings.changeIsRegion;
|
||||
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& regionId = settings.changeRegionId;
|
||||
auto& isVisible = settings.changeIsVisible;
|
||||
auto& interpolation = settings.changeInterpolation;
|
||||
auto& isFlipX = settings.changeIsFlipX;
|
||||
auto& isFlipY = settings.changeIsFlipY;
|
||||
auto& itemType = document.reference.itemType;
|
||||
|
||||
#define PROPERTIES_WIDGET(body, checkboxLabel, isEnabled) \
|
||||
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), checkboxLabel, isEnabled) };
|
||||
|
||||
auto enum_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value,
|
||||
const std::vector<int>& ids, std::vector<const char*>& labels)
|
||||
{ PROPERTIES_WIDGET(combo_id_mapped(valueLabel, &value, ids, labels), checkboxLabel, isEnabled) };
|
||||
|
||||
auto color3_value = [&](const char* checkboxRLabel, const char* checkboxGLabel, const char* checkboxBLabel,
|
||||
const char* valueRLabel, const char* valueGLabel, const char* valueBLabel,
|
||||
const char* label, bool& isREnabled, bool& isGEnabled, bool& isBEnabled, vec3& value)
|
||||
{
|
||||
auto style = ImGui::GetStyle();
|
||||
|
||||
auto width = (ImGui::CalcItemWidth() - (ImGui::GetFrameHeightWithSpacing() * 2) - (style.ItemSpacing.x * 2) -
|
||||
ImGui::GetFrameHeight()) /
|
||||
3;
|
||||
|
||||
ivec3 valueAlt = {float_to_uint8(value.r), float_to_uint8(value.g), float_to_uint8(value.b)};
|
||||
|
||||
ImGui::PushItemWidth(width);
|
||||
|
||||
PROPERTIES_WIDGET(ImGui::DragInt(valueRLabel, &valueAlt.r, DRAG_SPEED, 0, 255, "R:%d"), checkboxRLabel,
|
||||
isREnabled);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
PROPERTIES_WIDGET(ImGui::DragInt(valueGLabel, &valueAlt.g, DRAG_SPEED, 0, 255, "G:%d"), checkboxGLabel,
|
||||
isGEnabled);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
PROPERTIES_WIDGET(ImGui::DragInt(valueBLabel, &valueAlt.b, DRAG_SPEED, 0, 255, "B:%d"), checkboxBLabel,
|
||||
isBEnabled);
|
||||
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
value = vec3(uint8_to_float(valueAlt.r), uint8_to_float(valueAlt.g), uint8_to_float(valueAlt.b));
|
||||
|
||||
ImVec4 buttonColor = {isREnabled ? value.r : 0, isGEnabled ? value.g : 0, isBEnabled ? value.b : 0, 1};
|
||||
|
||||
ImGui::ColorButton(label, buttonColor);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::TextUnformatted(label);
|
||||
};
|
||||
|
||||
auto color4_value = [&](const char* checkboxRLabel, const char* checkboxGLabel, const char* checkboxBLabel,
|
||||
const char* checkboxALabel, const char* valueRLabel, const char* valueGLabel,
|
||||
const char* valueBLabel, const char* valueALabel, const char* label, bool& isREnabled,
|
||||
bool& isGEnabled, bool& isBEnabled, bool& isAEnabled, vec4& value)
|
||||
{
|
||||
auto style = ImGui::GetStyle();
|
||||
auto width = (ImGui::CalcItemWidth() - (ImGui::GetFrameHeightWithSpacing() * 3) - (style.ItemSpacing.x * 3) -
|
||||
ImGui::GetFrameHeight()) /
|
||||
4;
|
||||
ivec4 valueAlt = {float_to_uint8(value.r), float_to_uint8(value.g), float_to_uint8(value.b),
|
||||
float_to_uint8(value.a)};
|
||||
|
||||
ImGui::PushItemWidth(width);
|
||||
|
||||
PROPERTIES_WIDGET(ImGui::DragInt(valueRLabel, &valueAlt.r, DRAG_SPEED, 0, 255, "R:%d"), checkboxRLabel,
|
||||
isREnabled);
|
||||
ImGui::SameLine();
|
||||
|
||||
PROPERTIES_WIDGET(ImGui::DragInt(valueGLabel, &valueAlt.g, DRAG_SPEED, 0, 255, "G:%d"), checkboxGLabel,
|
||||
isGEnabled);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
PROPERTIES_WIDGET(ImGui::DragInt(valueBLabel, &valueAlt.b, DRAG_SPEED, 0, 255, "B:%d"), checkboxBLabel,
|
||||
isBEnabled);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
PROPERTIES_WIDGET(ImGui::DragInt(valueALabel, &valueAlt.a, DRAG_SPEED, 0, 255, "A:%d"), checkboxALabel,
|
||||
isAEnabled);
|
||||
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
value = vec4(uint8_to_float(valueAlt.r), uint8_to_float(valueAlt.g), uint8_to_float(valueAlt.b),
|
||||
uint8_to_float(valueAlt.a));
|
||||
|
||||
ImVec4 buttonColor = {isREnabled ? value.r : 0, isGEnabled ? value.g : 0, isBEnabled ? value.b : 0,
|
||||
isAEnabled ? value.a : 1};
|
||||
ImGui::ColorButton(label, buttonColor);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::TextUnformatted(label);
|
||||
};
|
||||
|
||||
auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value)
|
||||
{
|
||||
PROPERTIES_WIDGET(ImGui::DragFloat(valueLabel, &value, DRAG_SPEED, 0.0f, 0.0f, float_format_get(value)),
|
||||
checkboxLabel, isEnabled);
|
||||
};
|
||||
|
||||
auto float2_value = [&](const char* checkboxXLabel, const char* checkboxYLabel, const char* valueXLabel,
|
||||
const char* valueYLabel, bool& isXEnabled, bool& isYEnabled, vec2& value)
|
||||
{
|
||||
auto style = ImGui::GetStyle();
|
||||
|
||||
auto width = (ImGui::CalcItemWidth() - ImGui::GetFrameHeightWithSpacing() - style.ItemSpacing.x) / 2;
|
||||
|
||||
ImGui::PushItemWidth(width);
|
||||
PROPERTIES_WIDGET(ImGui::DragFloat(valueXLabel, &value.x, DRAG_SPEED, 0.0f, 0.0f, float_format_get(value.x)),
|
||||
checkboxXLabel, isXEnabled);
|
||||
ImGui::SameLine();
|
||||
PROPERTIES_WIDGET(ImGui::DragFloat(valueYLabel, &value.y, DRAG_SPEED, 0.0f, 0.0f, float_format_get(value.y)),
|
||||
checkboxYLabel, isYEnabled);
|
||||
ImGui::PopItemWidth();
|
||||
};
|
||||
|
||||
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),
|
||||
checkboxLabel, isEnabled);
|
||||
};
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImGui::GetStyle().ItemInnerSpacing);
|
||||
|
||||
float2_value("##Is Position X", "##Is Position Y", "##Position X", localize.get(BASIC_POSITION), isPositionX,
|
||||
isPositionY, position);
|
||||
|
||||
ImGui::BeginDisabled(itemType != anm2::LAYER);
|
||||
float2_value("##Is Pivot X", "##Is Pivot Y", "##Pivot X", localize.get(BASIC_PIVOT), isPivotX, isPivotY, pivot);
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::BeginDisabled(itemType != anm2::LAYER);
|
||||
float2_value("##Is Crop X", "##Is Crop Y", "##Crop X", localize.get(BASIC_CROP), isCropX, isCropY, crop);
|
||||
float2_value("##Is Size X", "##Is Size Y", "##Size X", localize.get(BASIC_SIZE), isSizeX, isSizeY, size);
|
||||
ImGui::EndDisabled();
|
||||
|
||||
float2_value("##Is Scale X", "##Is Scale Y", "##Scale X", localize.get(BASIC_SCALE), isScaleX, isScaleY, 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 R", "##Is Tint G", "##Is Tint B", "##Is Tint A", "##Tint R", "##Tint G", "##Tint B",
|
||||
"##Tint A", localize.get(BASIC_TINT), isTintR, isTintG, isTintB, isTintA, tint);
|
||||
|
||||
color3_value("##Is Color Offset R", "##Is Color Offset G", "##Is Color Offset B", "##Color Offset R",
|
||||
"##Color Offset B", "##Color Offset G", localize.get(BASIC_COLOR_OFFSET), isColorOffsetR,
|
||||
isColorOffsetG, isColorOffsetB, colorOffset);
|
||||
|
||||
ImGui::BeginDisabled(itemType != anm2::LAYER);
|
||||
std::vector<int> fallbackIds{-1};
|
||||
std::vector<std::string> fallbackLabelsString{localize.get(BASIC_NONE)};
|
||||
std::vector<const char*> fallbackLabels{fallbackLabelsString[0].c_str()};
|
||||
std::vector<int> interpolationIds{anm2::Frame::Interpolation::NONE, anm2::Frame::Interpolation::LINEAR,
|
||||
anm2::Frame::Interpolation::EASE_IN, anm2::Frame::Interpolation::EASE_OUT,
|
||||
anm2::Frame::Interpolation::EASE_IN_OUT};
|
||||
std::vector<std::string> interpolationLabelsString{
|
||||
localize.get(BASIC_NONE), localize.get(BASIC_LINEAR), localize.get(BASIC_EASE_IN),
|
||||
localize.get(BASIC_EASE_OUT), localize.get(BASIC_EASE_IN_OUT)};
|
||||
std::vector<const char*> interpolationLabels{interpolationLabelsString[0].c_str(),
|
||||
interpolationLabelsString[1].c_str(),
|
||||
interpolationLabelsString[2].c_str(),
|
||||
interpolationLabelsString[3].c_str(),
|
||||
interpolationLabelsString[4].c_str()};
|
||||
|
||||
const Storage* regionStorage = nullptr;
|
||||
if (itemType == anm2::LAYER && document.reference.itemID != -1)
|
||||
{
|
||||
auto spritesheetID = document.anm2.content.layers.at(document.reference.itemID).spritesheetID;
|
||||
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
|
||||
if (regionIt != document.regionBySpritesheet.end()) regionStorage = ®ionIt->second;
|
||||
}
|
||||
auto regionIds = regionStorage && !regionStorage->ids.empty() ? regionStorage->ids : fallbackIds;
|
||||
auto regionLabels = regionStorage && !regionStorage->labels.empty() ? regionStorage->labels : fallbackLabels;
|
||||
PROPERTIES_WIDGET(combo_id_mapped(localize.get(BASIC_REGION), ®ionId, regionIds, regionLabels), "##Is Region",
|
||||
isRegion);
|
||||
ImGui::EndDisabled();
|
||||
|
||||
enum_value("##Is Interpolation", localize.get(BASIC_INTERPOLATED), isInterpolationSet, interpolation,
|
||||
interpolationIds, interpolationLabels);
|
||||
|
||||
bool_value("##Is Visible", localize.get(BASIC_VISIBLE), isVisibleSet, isVisible);
|
||||
|
||||
bool_value("##Is Flip X", localize.get(LABEL_FLIP_X), isFlipXSet, isFlipX);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
bool_value("##Is Flip Y", localize.get(LABEL_FLIP_Y), isFlipYSet, isFlipY);
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
#undef PROPERTIES_WIDGET
|
||||
|
||||
auto frame_change = [&](anm2::ChangeType changeType)
|
||||
{
|
||||
anm2::FrameChange frameChange;
|
||||
if (isCropX) frameChange.cropX = crop.x;
|
||||
if (isCropY) frameChange.cropY = crop.y;
|
||||
if (isSizeX) frameChange.sizeX = size.x;
|
||||
if (isSizeY) frameChange.sizeY = size.y;
|
||||
if (isPositionX) frameChange.positionX = position.x;
|
||||
if (isPositionY) frameChange.positionY = position.y;
|
||||
if (isPivotX) frameChange.pivotX = pivot.x;
|
||||
if (isPivotY) frameChange.pivotY = pivot.y;
|
||||
if (isScaleX) frameChange.scaleX = scale.x;
|
||||
if (isScaleY) frameChange.scaleY = scale.y;
|
||||
if (isRotation) frameChange.rotation = std::make_optional(rotation);
|
||||
if (isDuration) frameChange.duration = std::make_optional(duration);
|
||||
if (isRegion) frameChange.regionID = std::make_optional(regionId);
|
||||
if (isTintR) frameChange.tintR = tint.r;
|
||||
if (isTintG) frameChange.tintG = tint.g;
|
||||
if (isTintB) frameChange.tintB = tint.b;
|
||||
if (isTintA) frameChange.tintA = tint.a;
|
||||
if (isColorOffsetR) frameChange.colorOffsetR = colorOffset.r;
|
||||
if (isColorOffsetG) frameChange.colorOffsetG = colorOffset.g;
|
||||
if (isColorOffsetB) frameChange.colorOffsetB = colorOffset.b;
|
||||
if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible);
|
||||
if (isInterpolationSet)
|
||||
frameChange.interpolation = std::make_optional(static_cast<anm2::Frame::Interpolation>(interpolation));
|
||||
if (isFlipXSet) frameChange.isFlipX = std::make_optional(isFlipX);
|
||||
if (isFlipYSet) frameChange.isFlipY = std::make_optional(isFlipY);
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES,
|
||||
document.item_get()->frames_change(frameChange, itemType, changeType, frames));
|
||||
|
||||
isChanged = true;
|
||||
};
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
bool isAnyProperty = isCropX || isCropY || isSizeX || isSizeY || isPositionX || isPositionY || isPivotX ||
|
||||
isPivotY || isScaleX || isScaleY || isRotation || isDuration || isTintR || isTintG ||
|
||||
isTintB || isTintA || isColorOffsetR || isColorOffsetG || isColorOffsetB || isRegion ||
|
||||
isVisibleSet || isInterpolationSet || isFlipXSet || isFlipYSet;
|
||||
|
||||
auto rowWidgetSize = widget_size_with_row_get(5);
|
||||
|
||||
ImGui::BeginDisabled(!isAnyProperty);
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_ADJUST), rowWidgetSize)) frame_change(anm2::ADJUST);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADJUST));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), rowWidgetSize)) frame_change(anm2::ADD);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_VALUES));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_SUBTRACT), rowWidgetSize)) frame_change(anm2::SUBTRACT);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SUBTRACT_VALUES));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_MULTIPLY), rowWidgetSize)) frame_change(anm2::MULTIPLY);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MULTIPLY_VALUES));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_DIVIDE), rowWidgetSize)) frame_change(anm2::DIVIDE);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DIVIDE_VALUES));
|
||||
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "document.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui::wizard
|
||||
{
|
||||
class ChangeAllFrameProperties
|
||||
{
|
||||
public:
|
||||
bool isChanged{};
|
||||
|
||||
void update(Document&, Settings&);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
#include "configure.hpp"
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "sdl.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
|
||||
namespace anm2ed::imgui::wizard
|
||||
{
|
||||
void Configure::reset(Settings& settings) { temporary = settings; }
|
||||
|
||||
void Configure::update(Manager& manager, Settings& settings)
|
||||
{
|
||||
isSet = false;
|
||||
|
||||
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), temporary.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f");
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_UI_SCALE));
|
||||
input_float_range(localize.get(LABEL_ITEM_HEIGHT), temporary.timelineItemHeight, 0.75f, 1.25f, 0.05f, 0.05f,
|
||||
"%.2f");
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_HEIGHT));
|
||||
ImGui::Checkbox(localize.get(LABEL_VSYNC), &temporary.isVsync);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_VSYNC));
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_LOCALIZATION));
|
||||
ImGui::Combo(localize.get(LABEL_LANGUAGE), &temporary.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]), &temporary.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), &temporary.fileIsAutosave);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTOSAVE_ENABLED));
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_SNAPSHOTS));
|
||||
input_int_range(localize.get(LABEL_STACK_SIZE), temporary.fileSnapshotStackSize, 0, 100);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_STACK_SIZE));
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_COMPATIBILITY));
|
||||
ImGui::RadioButton(localize.get(LABEL_ISAAC), &temporary.fileCompatibility, anm2::ISAAC);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ISAAC));
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(localize.get(LABEL_ANM2ED), &temporary.fileCompatibility, anm2::ANM2ED);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ANM2ED));
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(localize.get(LABEL_ANM2ED_LIMITED), &temporary.fileCompatibility, anm2::ANM2ED_LIMITED);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ANM2ED_LIMITED));
|
||||
|
||||
ImGui::Checkbox(localize.get(LABEL_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE),
|
||||
&temporary.fileBakeSpecialInterpolatedFramesOnSave);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE));
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_OPTIONS));
|
||||
ImGui::Checkbox(localize.get(LABEL_OVERWRITE_WARNING), &temporary.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), temporary.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), temporary.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), temporary.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), &temporary.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 = &(temporary.*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);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
if (ImGui::Button(localize.get(BASIC_SAVE), widgetSize))
|
||||
{
|
||||
auto settingsPath = util::sdl::preferences_directory_get() / "settings.ini";
|
||||
settings = temporary;
|
||||
|
||||
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();
|
||||
|
||||
settings.save(settingsPath, ImGui::SaveIniSettingsToMemory(nullptr));
|
||||
|
||||
isSet = true;
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SETTINGS_SAVE));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_USE_DEFAULT_SETTINGS), widgetSize)) temporary = Settings();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_USE_DEFAULT_SETTINGS));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CLOSE]);
|
||||
if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) isSet = true;
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CLOSE_SETTINGS));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user