Compare commits

...

127 Commits

Author SHA1 Message Date
shweet f1b46fbf60 let's be honest 2026-04-14 17:20:07 -04:00
shweet 216a65c756 baked frames are selected 2026-04-14 17:17:45 -04:00
shweet 98d1bf1981 baked frames are selected 2026-04-14 17:15:06 -04:00
shweet c59ef05902 change baking frames 2026-04-14 17:02:32 -04:00
shweet b60c4bc295 I N T E R P O L A T I O N 2026-04-14 14:28:14 -04:00
shweet 15f85b84a9 fixed crop/size/pivot issue when no region selected 2026-04-14 10:23:40 -04:00
shweet 8fbccb2257 Merge branch 'staging' 2026-04-08 20:01:35 -04:00
shweet 378a7692e2 add #include format 2026-04-08 20:00:13 -04:00
shweet 0b91382f0f changes + more windows bs 2026-03-29 20:13:24 -04:00
shweet b1213d7d23 Merge branch 'staging'
# Conflicts:
#	.vscode/launch.json
#	.vscode/tasks.json
#	src/imgui/window/animation_preview.cpp
#	src/imgui/window/regions.cpp
2026-03-19 03:51:13 -04:00
shweet ad125c15a2 queue oh el 2026-03-19 03:46:42 -04:00
shweet 4f5966dad6 fix path issues 2026-03-17 00:47:36 -04:00
shweet a83dbd5b6c Render animation fixes, vs code tasks 2026-03-15 13:53:46 -04:00
shweet f58d89425f Render animation fixes, vs code tasks 2026-03-15 13:52:42 -04:00
shweet bbfafd7331 Render animation fixes, vs code tasks 2026-03-15 13:22:53 -04:00
shweet 1b5ba6b584 fix audio desync for realsies, stderr to log.txt 2026-03-10 00:36:21 -04:00
shweet c11b404392 let's actually stage those changes... 2026-03-09 23:09:37 -04:00
shweet 2d27b7e8fb staging some future refactoring, .h -> hpp, fix for rendering w/ audio 2026-03-09 23:08:57 -04:00
shweet 77f6e65b15 some bug fixes, added spritesheet filepath setting, etc. 2026-03-05 00:10:59 -05:00
shweet 5a48b07321 readmes 2026-02-06 12:05:37 -05:00
shweet 400231f246 again... 2026-02-06 11:59:53 -05:00
shweet 33e5846a6e update imgui settings 2026-02-06 11:58:26 -05:00
shweet 85073a2ab9 spritesheet hashing, navigation fix, etc. 2026-02-06 11:52:33 -05:00
shweet 64d6a1d95a Mega Region Update. 2026-02-05 21:34:42 -05:00
shweet 00bff4a91f default imgui settings 2026-01-30 01:27:54 -05:00
shweet 1b7a49c25d regions and a whole bunch of other shit 2026-01-30 01:13:28 -05:00
shweet ca3a0f6691 update dumps + fix trigger bug some more 2026-01-19 15:06:16 -05:00
shweet 76f134fe60 quick fix for crashing triggers 2026-01-18 16:39:16 -05:00
shweet 044ef8b818 remove unused fix 2026-01-18 16:32:33 -05:00
shweet cbfb3403b7 includes 2026-01-14 19:26:25 -05:00
shweet b90c15eab4 bring back windows dumps 2026-01-14 19:25:12 -05:00
shweet 3b551abb8a fixed rendering issues with non-30 fps 2026-01-14 04:36:24 -05:00
shweet 579ca08181 fix issue with rendering animations on linux 2026-01-14 04:03:26 -05:00
shweet 8a1058d559 FLIPPING!!!!!!! 2026-01-13 19:00:56 -05:00
shweet 7f4e05a927 fixed event handling and a lot of ID stuff OOPS 2026-01-13 03:16:38 -05:00
shweet f9087b8ed3 quick change to events + workshop 2026-01-13 02:49:01 -05:00
shweet 945fd31735 remove windows debug code (for now...) 2026-01-12 23:12:04 -05:00
shweet 6cac267998 fixes! 2026-01-12 23:10:10 -05:00
shweet 2797a971eb wtf 2026-01-11 18:25:37 -05:00
shweet 47edb034b3 update debug 2026-01-11 18:23:46 -05:00
shweet 283d3fd852 windows dumps 2026-01-11 18:17:44 -05:00
shweet 3117f190a5 windows dumps... 2026-01-11 18:12:28 -05:00
shweet 29abbb79fb cmakelists fix 2026-01-11 17:25:42 -05:00
shweet cc2f5455e1 again... 2026-01-11 17:15:28 -05:00
shweet ea6f1e0711 tweak windows crash includes 2026-01-11 17:14:37 -05:00
shweet 6fd1d57306 tweak windows crash 2026-01-11 17:14:01 -05:00
shweet d2bc5696bb render: open process as "wb" not "w" 2026-01-11 17:00:08 -05:00
shweet e68dfa3c94 tweaks 2026-01-11 16:46:50 -05:00
shweet d2e9204e25 windows dumps 2026-01-11 13:21:03 -05:00
shweet a39a816ea9 update cmake 2026-01-11 13:13:45 -05:00
shweet bd3fdbf930 saving fixes? 2026-01-11 01:20:21 -05:00
shweet 5124f77817 random sound selection for triggers 2026-01-06 16:26:57 -05:00
shweet b41b6df19e handling saving issues in certain contexts/new shortcuts 2026-01-05 04:16:51 -05:00
shweet a8143b6d0c Fixed random crashing issue 2026-01-02 03:42:43 -05:00
shweet 95b441c4cf document change fix 2025-12-30 14:36:38 -05:00
shweet 58077b5522 fix mp4 crash 2025-12-29 00:17:46 -05:00
shweet f7109c24d0 change all frame properties disallows root/nulls in some cases 2025-12-28 18:41:37 -05:00
shweet c0b4aaa63e width multiplier i suppose 2025-12-28 18:17:07 -05:00
shweet 7e988dbb7f font fallback? 2025-12-28 17:49:24 -05:00
shweet 33c55806a4 INTS!!!!!!! 2025-12-28 16:52:50 -05:00
shweet 5539b73374 temp font fix for now 2025-12-28 16:32:08 -05:00
shweet c0de0c4079 BRUH 2025-12-28 14:54:08 -05:00
shweet cd0f6e9438 erm... 2025-12-28 14:46:15 -05:00
shweet 23fac343a9 IDIOT!!!!!!!!!!!!!!!! 2025-12-28 14:38:02 -05:00
shweet 233b2139fd change all frame properties adjustments 2025-12-28 14:29:36 -05:00
shweet 8096521f28 timeline width calculation 2025-12-27 19:33:56 -05:00
shweet e108ec15b8 removed frame tooltip, kind of just was distracting 2025-12-27 16:10:01 -05:00
shweet a6964c40bc fixed issue with adding layers/nulls not having their base layer/null properties 2025-12-26 18:07:16 -05:00
shweet a8f38321ca le workshop 2025-12-26 17:06:03 -05:00
shweet 5b8469d7ac (int) 2025-12-26 16:58:42 -05:00
shweet 9853e049b0 le workshop 2025-12-26 16:46:48 -05:00
shweet f64d622d87 multiselect changing on animation preview/spritesheet editor 2025-12-26 16:45:50 -05:00
shweet ed68851f5c update le readmes 2025-12-21 15:18:21 -05:00
shweet 99e56eabdf window pos 2025-12-20 13:46:50 -05:00
shweet 4574ff0f84 add item fixes 2025-12-18 23:56:15 -05:00
shweet 82c5dcc176 b 2025-12-18 02:30:22 -05:00
shweet 0043cbd44a bbbbbbbbbb 2025-12-18 02:26:07 -05:00
shweet 92d6d90130 brrrrrr 2025-12-18 02:24:46 -05:00
shweet e896c7f9cc fffffffffffffffffffffffffffffffffffff 2025-12-18 01:37:26 -05:00
shweet 99d4d06594 FFFFFFFFFFFFFF 2025-12-18 01:36:20 -05:00
shweet 50154973ef fuck everything 2025-12-18 01:36:07 -05:00
shweet d6fab7c738 bbbbbbbb 2025-12-18 01:23:41 -05:00
shweet 09cfaf9af8 bro 2025-12-18 01:15:10 -05:00
shweet 4475c79438 sneed 2025-12-18 01:08:36 -05:00
shweet d6c2ee3401 breh 2025-12-18 00:57:04 -05:00
shweet df7dfde1d6 this is insane but sure 2025-12-18 00:36:19 -05:00
shweet 7e1b84e83c :) 2025-12-18 00:25:28 -05:00
shweet 6257e7dffb gormf 2025-12-18 00:07:04 -05:00
shweet e81030d9e3 mgggggggggggh 2025-12-17 23:58:31 -05:00
shweet 62c845f47b blork 2025-12-17 23:50:32 -05:00
shweet 99a1caa096 eh? 2025-12-17 23:34:52 -05:00
shweet 3095a56a01 erm... 2025-12-17 23:17:48 -05:00
shweet 0ba585511c windows moment 2025-12-17 23:09:58 -05:00
shweet 119bbc4081 Refactor + render animation tweaks + updated frame properties + bug fixes 2025-12-17 23:02:00 -05:00
shweet b4b4fe3714 AAAAAAAAAH ROOT TRANSFORM 2025-12-16 20:22:42 -05:00
shweet 4cf5304c79 OH SHIT ROOT FRAME TINT/COLOR OFFSET! 2025-12-16 20:18:49 -05:00
shweet 4e1226739a Settings fix 2025-12-16 12:35:19 -05:00
shweet ed2f92d412 Removed texture inset and fix closing document bug 2025-12-16 01:52:06 -05:00
shweet ffed82a591 2.2 2025-12-13 23:40:15 -05:00
shweet ee89d8880d erm 2025-12-13 23:10:56 -05:00
shweet 225a584a21 let's try this 2025-12-13 23:06:14 -05:00
shweet efa9533a52 try this 2025-12-13 22:59:53 -05:00
shweet 8fa103d0b7 bbbbbbbbbbbbbbbbb 2025-12-13 22:43:32 -05:00
shweet b9b0fb9974 mmmmmmmmmmmmmmmm 2025-12-13 22:33:50 -05:00
shweet c23179c134 FILE* 2025-12-13 22:32:33 -05:00
shweet 4fc004dbe0 ummmm 2025-12-13 22:23:18 -05:00
shweet 29fc0f3449 mmmmmmmm 2025-12-13 22:14:47 -05:00
shweet 898dfcc68f more fixes? 2025-12-13 22:02:48 -05:00
shweet 31ac9707e7 preliminary utf-16 stuff 2025-12-13 21:54:29 -05:00
shweet 1a3c75a84a fixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 2025-12-13 20:57:51 -05:00
shweet d34ff73512 CHANGES! 2025-12-13 20:12:52 -05:00
shweet 21b66d8337 checking... 2025-12-13 02:51:10 -05:00
shweet 4977f4476c maybe... 2025-12-13 02:33:12 -05:00
shweet 61275d9e75 windows fixes 2025-12-12 23:17:06 -08:00
shweet 94a3371509 update defaults 2025-12-13 01:27:29 -05:00
shweet c77701fe72 yeah these too 2025-12-13 01:21:57 -05:00
shweet 3be9f0746f did someone say CONTEXT MENUS? + other fun stuff 2025-12-13 01:19:56 -05:00
shweet 9c1ccd3223 Spanish localization 2025-12-09 22:18:11 -05:00
shweet dd55e6e2a2 Update to strings 2025-12-09 17:06:45 -05:00
shweet e6a4e5af35 Locale -> "Destination"; feels like a much better term to use 2025-12-07 01:57:29 -05:00
shweet fef1ff50e3 Fffffffffff 2025-12-06 22:49:13 -05:00
shweet 10b37e9854 Fix for opening files with program association; shouldn't open popup anymore 2025-12-06 22:47:01 -05:00
shweet 63c0ffe166 Fixed frame properties crash 2025-12-04 19:05:44 -05:00
shweet 183f3390fa Timeline context menu update; baking/splitting shortcuts 2025-12-04 02:38:18 -05:00
shweet cc6a502ff9 socket issues 2025-12-03 17:30:28 -05:00
shweet 45f0271c29 issue 2025-12-03 17:13:24 -05:00
shweet b4fddb7714 minor bug fixes 2025-12-03 02:11:25 -05:00
171 changed files with 58038 additions and 1285842 deletions
View File
+5 -3
View File
@@ -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 @@
]
}
]
}
}
+65 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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\""
}
]
}
}
+14
View File
@@ -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
View File
@@ -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
+147
View File
@@ -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
View File
@@ -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);
};
}
}
+7 -7
View File
@@ -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
View File
@@ -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;
}
}
-81
View File
@@ -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 = {});
};
}
+96
View File
@@ -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&);
};
}
+8 -5
View File
@@ -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;
}
}
}
+13 -4
View File
@@ -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
View File
@@ -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};
}
}
}
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+25 -9
View File
@@ -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
View File
@@ -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}};
}
+4 -4
View File
@@ -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)
+7 -7
View File
@@ -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
View File
@@ -1,6 +1,6 @@
#include "event.h"
#include "event.hpp"
#include "xml_.h"
#include "xml_.hpp"
using namespace anm2ed::util;
using namespace tinyxml2;
-1
View File
@@ -9,7 +9,6 @@ namespace anm2ed::anm2
{
public:
std::string name{};
int soundID{-1};
Event() = default;
Event(tinyxml2::XMLElement*, int&);
+137 -45
View File
@@ -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", &regionID);
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); }
}
}
-55
View File
@@ -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
}
+80
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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(); }
}
+1 -2
View File
@@ -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
View File
@@ -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", &region.name);
child->QueryFloatAttribute("XCrop", &region.crop.x);
child->QueryFloatAttribute("YCrop", &region.crop.y);
child->QueryFloatAttribute("Width", &region.size.x);
child->QueryFloatAttribute("Height", &region.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", &region.pivot.x);
child->QueryFloatAttribute("YPivot", &region.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", &region.name);
element->QueryFloatAttribute("XCrop", &region.crop.x);
element->QueryFloatAttribute("YCrop", &region.crop.y);
element->QueryFloatAttribute("Width", &region.size.x);
element->QueryFloatAttribute("Height", &region.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", &region.pivot.x);
element->QueryFloatAttribute("YPivot", &region.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);
}
}
-28
View File
@@ -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();
};
}
+54
View File
@@ -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
View File
@@ -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);
-64
View File
@@ -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;
};
}
+67
View File
@@ -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
View File
@@ -1,4 +1,4 @@
#include "clipboard.h"
#include "clipboard.hpp"
#include <SDL3/SDL.h>
+27 -26
View File
@@ -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();
}
};
+33 -36
View File
@@ -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
View File
@@ -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)));
}
};
+32 -17
View File
@@ -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
View File
@@ -1,6 +1,6 @@
#include "framebuffer.h"
#include "framebuffer.hpp"
#include "texture.h"
#include "texture.hpp"
using namespace anm2ed::resource;
using namespace glm;
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+42 -6
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-35
View File
@@ -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&);
};
};
+42
View File
@@ -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
View File
@@ -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;
+445 -102
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+210 -169
View File
@@ -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();
}
}
-13
View File
@@ -1,13 +0,0 @@
#pragma once
#include "manager.h"
#include "settings.h"
namespace anm2ed::imgui
{
class FrameProperties
{
public:
void update(Manager&, Settings&);
};
}
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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
{
+3 -3
View File
@@ -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
{
+569
View File
@@ -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), &region.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*)&region.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();
}
}
+20
View File
@@ -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
View File
@@ -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
{
+423 -50
View File
@@ -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
View File
@@ -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();
}
}
-18
View File
@@ -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);
};
}
+24
View File
@@ -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);
};
}
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};
+4 -4
View File
@@ -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
{
+15 -6
View File
@@ -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
{
+193
View File
@@ -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();
}
}
}
+36
View File
@@ -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 = &regionIt->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), &regionId, 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&);
};
}
+221
View File
@@ -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