Compare commits
296 Commits
main
...
2ac2c45b11
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ac2c45b11 | |||
| b576143afb | |||
| bc5b39057d | |||
| 1369a9048e | |||
| a89f5e6cf2 | |||
| 2ec11e5099 | |||
| 94a6877e7c | |||
| dc23a5648d | |||
| 9adfad9609 | |||
| cee5f141a3 | |||
| 603bb0c4e7 | |||
| 5752bc6ae9 | |||
| 93f3037410 | |||
| 9c7c89fed4 | |||
| 45a7d49d40 | |||
| de9bca8bb5 | |||
| 6427f218e7 | |||
| 6d0cc4eb15 | |||
| a6306c2759 | |||
| 7c76703355 | |||
| 9c3f56954e | |||
| e880f23040 | |||
| defa9fc212 | |||
| ea96f38875 | |||
| b67f3d63cf | |||
| fb111dcdc9 | |||
| 62561624ed | |||
| b5bd6d42f7 | |||
| c640519772 | |||
| fb844f79fd | |||
| 6dac909869 | |||
| 65b262207c | |||
| ef50f4a361 | |||
| 888e94a77c | |||
| c56d301b29 | |||
| 91e1c2c9a3 | |||
| 2087505921 | |||
| 58afa672c7 | |||
| 8dc476d205 | |||
| 73fac0f8e4 | |||
| a487b0ba48 | |||
| efd568a416 | |||
| 4f0909f30c | |||
| fdc1defaba | |||
| 07ed23c2d1 | |||
| 5d5bb24711 | |||
| 21c448d6f1 | |||
| cd9206344d | |||
| 4d06608cc9 | |||
| a64a63def7 | |||
| 19cb14b5dc | |||
| 4de6f496ad | |||
| e1cce05bd6 | |||
| 1ae79ab3c1 | |||
| acdaf3bb8e | |||
| f20595aff6 | |||
| 779d6b0387 | |||
| 3128a0d309 | |||
| ae69f7437f | |||
| 9971b2b7f2 | |||
| 3e15b2f46c | |||
| 7dcf76c3aa | |||
| 155e67fcec | |||
| 2a030318b1 | |||
| 103fe4fb12 | |||
| b2335b1656 | |||
| 692fe08d9f | |||
| 8b12ae35d4 | |||
| 87b1851d59 | |||
| 389cd93e68 | |||
| 6652127545 | |||
| e152616d7f | |||
| ac4d065c78 | |||
| 578b1f6082 | |||
| beb7f717f1 | |||
| 7a9b14a86f | |||
| f3925f8423 | |||
| dd641c047b | |||
| 22006eaf47 | |||
| 537f0dcb2f | |||
| 2ea850cbcc | |||
| e10e16f491 | |||
| 6369c3c969 | |||
| ead7f58285 | |||
| 0e77ca6ba8 | |||
| 1e0500a3f7 | |||
| 4ed72ebc80 | |||
| 6960bd3410 | |||
| 5ee2dd271c | |||
| 5ac807c6bd | |||
| 4af55a7d3f | |||
| 712c28068d | |||
| 777723b68c | |||
| cc3490d9d8 | |||
| d9be3f910a | |||
| 8a7db3bca8 | |||
| 3a78361aea | |||
| 6e3296469a | |||
| 561193b2ab | |||
| 8de9dadf1d | |||
| 853307697a | |||
| fd1772a417 | |||
| 1df506a176 | |||
| b349f24931 | |||
| 5841878df9 | |||
| c8d769c02c | |||
| d28aa25358 | |||
| 76808d60e3 | |||
| 9dd53f9212 | |||
| 0e03e5940a | |||
| e15894e4ea | |||
| 37b1cf82f3 | |||
| 39444af84e | |||
| da584ce0f0 | |||
| 455c91bf29 | |||
| 6fda4d4a90 | |||
| b84dfc049d | |||
| 3a1ca7a8e6 | |||
| b80bd759aa | |||
| a2e47c862e | |||
| 7b882896f1 | |||
| def1a170dc | |||
| 6a3cd867f0 | |||
| 55b725e876 | |||
| d664e9fc39 | |||
| 1dcd96ab36 | |||
| b6a25474ff | |||
| b4c2117992 | |||
| 9a4c595f64 | |||
| ce33eaaef2 | |||
| cc33fbdde2 | |||
| c18297f221 | |||
| 2f8f12a8fd | |||
| 728116da8f | |||
| 36f9e73dd4 | |||
| 9b6c5b0849 | |||
| cc4eaef3e6 | |||
| 77c2a68cc5 | |||
| 647dd81992 | |||
| c5c31f0a56 | |||
| b6c66f3e41 | |||
| 1065183e75 | |||
| dc03491b0d | |||
| 8c99454bf5 | |||
| 0fc73d51d2 | |||
| 831e5deeae | |||
| 22dfde8e7c | |||
| 9a7f4bc0d2 | |||
| 860e5ad31e | |||
| 9b00acec6f | |||
| 53fc5f9a57 | |||
| f6780d183c | |||
| 48fdfd849d | |||
| 52da64fc96 | |||
| 9759abde44 | |||
| 06a44705d0 | |||
| 3ae84de123 | |||
| 8c0784f9c3 | |||
| 995752da75 | |||
| 18617cdbd2 | |||
| 56cb9eaacb | |||
| a5dbf05ab5 | |||
| bbe3db1747 | |||
| 07293c0590 | |||
| 901aff1051 | |||
| 75dd5cfdc9 | |||
| 483bbb4a9c | |||
| 58f163788b | |||
| 8232b0efc8 | |||
| 23c308db1b | |||
| 881b5271a2 | |||
| 952a00e7d3 | |||
| b68ddc42c6 | |||
| 9a7e1c4def | |||
| 5226746c1a | |||
| 5dbeb0504d | |||
| ee3fb36047 | |||
| 1c40602744 | |||
| 818014127a | |||
| d37145660a | |||
| c58b9a3718 | |||
| a6a4e7b249 | |||
| 2b50c2157f | |||
| 0eded78c4c | |||
| 99b2eeb99d | |||
| 7b14c356db | |||
| bad2670f87 | |||
| b3710498f3 | |||
| 1bc90d88b4 | |||
| ddca24779e | |||
| 1ab2a9b846 | |||
| 9c6b52eb8e | |||
| 9d05d193a7 | |||
| e6e80b94ba | |||
| 4e70c90ca8 | |||
| a8faa82b70 | |||
| 4f4ac380ac | |||
| 374cb5b075 | |||
| b0445382dd | |||
| 3701fd2a71 | |||
| 1e4b4cad73 | |||
| a6aa31da79 | |||
| b82cc1e4bd | |||
| 1d44036933 | |||
| 61f86f5aae | |||
| acd8ef6658 | |||
| c22f2e7fa2 | |||
| b7d9dfbf31 | |||
| 92338a0911 | |||
| f7d32f2835 | |||
| 737c29cca4 | |||
| 37a59c01ac | |||
| 7ae37038b3 | |||
| bbe8378630 | |||
| 466c1d0cc0 | |||
| 4a7eff24bf | |||
| f968488e34 | |||
| a12a3454c4 | |||
| 8a92bc973b | |||
| dbaf50cb6e | |||
| 92e9de0441 | |||
| 7280678593 | |||
| b7c087617b | |||
| d8e958769b | |||
| b85c530df7 | |||
| 3823a612ae | |||
| 2a3402e991 | |||
| bbb85bb133 | |||
| d0b0dc3865 | |||
| 6fc8b9e5d2 | |||
| 217450e161 | |||
| 3930f39b14 | |||
| 19f815e3d2 | |||
| 9e0a88726c | |||
| 47eb1ec0b2 | |||
| 0d2a1bd0ae | |||
| 85a5d19a3e | |||
| 02f14f1bf5 | |||
| e00eec30d4 | |||
| 43e3a74c42 | |||
| 75dfc85978 | |||
| 9ce49ef19c | |||
| 36fea6b870 | |||
| 8130a922d0 | |||
| f1e2743d58 | |||
| 7d80afce2f | |||
| 4212387b70 | |||
| bdcd44b340 | |||
| 05064b3a0d | |||
| aa32c47e18 | |||
| 2e0ebd0e13 | |||
| 2754df9f46 | |||
| 9ab73a0354 | |||
| d61c7f37c3 | |||
| ad255a6ddf | |||
| 88507df90e | |||
| 10e5d5b5ae | |||
| c16cab87bd | |||
| 7319cb9aa9 | |||
| 677d0b33a8 | |||
| f1ee1b28a1 | |||
| 2da247f0fb | |||
| 37854ea8b9 | |||
| dc252b2f24 | |||
| d0ef88be89 | |||
| 4ec2d093e8 | |||
| 4eee018367 | |||
| 44aebf61b2 | |||
| f6d3de8cbf | |||
| c62bc4d744 | |||
| 8ebb22325c | |||
| e5d98c2dc3 | |||
| abe578a338 | |||
| 313a360c01 | |||
| 551013c771 | |||
| cc377b5eb5 | |||
| 6c435dafb7 | |||
| 3f5711773e | |||
| a7bb04f54b | |||
| 6604f30ef3 | |||
| 93d8aaaffd | |||
| f9e4bcaeea | |||
| 3d80791245 | |||
| 126280ff7c | |||
| 20b5dba41e | |||
| dfdb7a4468 | |||
| 4d715afd60 | |||
| ac0d0ab49c | |||
| a67e7fc9bb | |||
| 31322bbd83 | |||
| 23eba07901 | |||
| 8014345b99 | |||
| 99eda95cee | |||
| ec5ecbdb54 | |||
| e0ea4597e6 | |||
| c38ff8209b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -53,3 +53,5 @@ linux/Makefile
|
|||||||
|
|
||||||
webgl/build
|
webgl/build
|
||||||
webgl/.vscode
|
webgl/.vscode
|
||||||
|
|
||||||
|
out/
|
||||||
|
|||||||
566
CMakeLists.txt
Normal file
566
CMakeLists.txt
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.29)
|
||||||
|
|
||||||
|
project(PanoPainter
|
||||||
|
VERSION 0.0.0
|
||||||
|
DESCRIPTION "Panoramic painting and animation application"
|
||||||
|
LANGUAGES C CXX)
|
||||||
|
|
||||||
|
if(POLICY CMP0091)
|
||||||
|
cmake_policy(SET CMP0091 NEW)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||||
|
|
||||||
|
include(PanoPainterOptions)
|
||||||
|
if(PP_ENABLE_ASAN AND MSVC AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||||
|
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||||
|
else()
|
||||||
|
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
include(PanoPainterWarnings)
|
||||||
|
include(PanoPainterSources)
|
||||||
|
include(PanoPainterVersion)
|
||||||
|
include(PanoPainterRuntime)
|
||||||
|
|
||||||
|
if(PP_ENABLE_CLANG_TIDY)
|
||||||
|
find_program(PP_CLANG_TIDY_EXE NAMES clang-tidy)
|
||||||
|
if(PP_CLANG_TIDY_EXE)
|
||||||
|
set(CMAKE_CXX_CLANG_TIDY "${PP_CLANG_TIDY_EXE}")
|
||||||
|
else()
|
||||||
|
message(WARNING "PP_ENABLE_CLANG_TIDY is ON but clang-tidy was not found.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(PP_ENABLE_CPPCHECK)
|
||||||
|
find_program(PP_CPPCHECK_EXE NAMES cppcheck)
|
||||||
|
if(PP_CPPCHECK_EXE)
|
||||||
|
set(CMAKE_CXX_CPPCHECK
|
||||||
|
"${PP_CPPCHECK_EXE}"
|
||||||
|
"--enable=warning,style,performance,portability"
|
||||||
|
"--inline-suppr"
|
||||||
|
"--suppress=missingIncludeSystem")
|
||||||
|
else()
|
||||||
|
message(WARNING "PP_ENABLE_CPPCHECK is ON but cppcheck was not found.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(pp_project_options INTERFACE)
|
||||||
|
target_compile_features(pp_project_options INTERFACE cxx_std_23)
|
||||||
|
|
||||||
|
add_library(pp_project_warnings INTERFACE)
|
||||||
|
pp_configure_project_warnings(pp_project_warnings)
|
||||||
|
|
||||||
|
if(PP_USE_VCPKG_TINYXML2)
|
||||||
|
find_package(tinyxml2 CONFIG REQUIRED)
|
||||||
|
add_library(pp_xml_tinyxml2 INTERFACE)
|
||||||
|
target_link_libraries(pp_xml_tinyxml2
|
||||||
|
INTERFACE
|
||||||
|
tinyxml2::tinyxml2)
|
||||||
|
else()
|
||||||
|
add_library(pp_vendor_tinyxml2 STATIC
|
||||||
|
libs/tinyxml2/tinyxml2.cpp)
|
||||||
|
target_include_directories(pp_vendor_tinyxml2
|
||||||
|
SYSTEM PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/tinyxml2")
|
||||||
|
target_link_libraries(pp_vendor_tinyxml2
|
||||||
|
PUBLIC
|
||||||
|
pp_project_options)
|
||||||
|
add_library(pp_xml_tinyxml2 ALIAS pp_vendor_tinyxml2)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_custom_target(panopainter_modernization_status
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E echo "PanoPainter modernization scaffold configured."
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E echo "Roadmap: docs/modernization/roadmap.md"
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E echo "Debt log: docs/modernization/debt.md"
|
||||||
|
VERBATIM)
|
||||||
|
|
||||||
|
add_custom_target(panopainter_validate_shaders
|
||||||
|
COMMAND "${CMAKE_COMMAND}"
|
||||||
|
"-DPP_SHADER_DIR=${CMAKE_CURRENT_SOURCE_DIR}/data/shaders"
|
||||||
|
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ValidatePanoPainterShaders.cmake"
|
||||||
|
VERBATIM)
|
||||||
|
|
||||||
|
add_library(pp_foundation STATIC
|
||||||
|
src/foundation/binary_stream.cpp
|
||||||
|
src/foundation/event.cpp
|
||||||
|
src/foundation/log.cpp
|
||||||
|
src/foundation/parse.cpp
|
||||||
|
src/foundation/task_queue.cpp
|
||||||
|
src/foundation/trace.cpp)
|
||||||
|
target_include_directories(pp_foundation
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(pp_foundation
|
||||||
|
PUBLIC
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
|
||||||
|
add_library(pp_assets STATIC
|
||||||
|
src/assets/image_format.cpp
|
||||||
|
src/assets/image_metadata.cpp
|
||||||
|
src/assets/image_pixels.cpp
|
||||||
|
src/assets/ppi_header.cpp
|
||||||
|
src/assets/settings_document.cpp)
|
||||||
|
target_include_directories(pp_assets
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_include_directories(pp_assets
|
||||||
|
SYSTEM PRIVATE
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/stb")
|
||||||
|
if(MSVC)
|
||||||
|
set_source_files_properties(src/assets/image_pixels.cpp
|
||||||
|
PROPERTIES
|
||||||
|
COMPILE_OPTIONS "/analyze-")
|
||||||
|
endif()
|
||||||
|
target_link_libraries(pp_assets
|
||||||
|
PUBLIC
|
||||||
|
pp_foundation
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
|
||||||
|
add_library(pp_paint STATIC
|
||||||
|
src/paint/brush.cpp
|
||||||
|
src/paint/blend.cpp
|
||||||
|
src/paint/stroke.cpp
|
||||||
|
src/paint/stroke_script.cpp)
|
||||||
|
target_include_directories(pp_paint
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(pp_paint
|
||||||
|
PUBLIC
|
||||||
|
pp_foundation
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
|
||||||
|
add_library(pp_document STATIC
|
||||||
|
src/document/document.cpp
|
||||||
|
src/document/ppi_export.cpp
|
||||||
|
src/document/ppi_import.cpp)
|
||||||
|
target_include_directories(pp_document
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(pp_document
|
||||||
|
PUBLIC
|
||||||
|
pp_foundation
|
||||||
|
pp_assets
|
||||||
|
pp_paint
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
|
||||||
|
add_library(pp_renderer_api STATIC
|
||||||
|
src/renderer_api/recording_renderer.cpp
|
||||||
|
src/renderer_api/renderer_api.cpp
|
||||||
|
src/renderer_api/shader_catalog.cpp)
|
||||||
|
target_include_directories(pp_renderer_api
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(pp_renderer_api
|
||||||
|
PUBLIC
|
||||||
|
pp_foundation
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
|
||||||
|
if(PP_ENABLE_OPENGL)
|
||||||
|
add_library(pp_renderer_gl STATIC
|
||||||
|
src/renderer_gl/command_plan.cpp
|
||||||
|
src/renderer_gl/opengl_capabilities.cpp
|
||||||
|
src/renderer_gl/shader_bindings.cpp)
|
||||||
|
target_include_directories(pp_renderer_gl
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(pp_renderer_gl
|
||||||
|
PUBLIC
|
||||||
|
pp_renderer_api
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(pp_paint_renderer STATIC
|
||||||
|
src/paint_renderer/compositor.cpp)
|
||||||
|
target_include_directories(pp_paint_renderer
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(pp_paint_renderer
|
||||||
|
PUBLIC
|
||||||
|
pp_foundation
|
||||||
|
pp_paint
|
||||||
|
pp_renderer_api
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
|
||||||
|
add_library(pp_ui_core STATIC
|
||||||
|
src/ui_core/color.cpp
|
||||||
|
src/ui_core/layout_value.cpp
|
||||||
|
src/ui_core/layout_xml.cpp)
|
||||||
|
target_include_directories(pp_ui_core
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(pp_ui_core
|
||||||
|
PUBLIC
|
||||||
|
pp_foundation
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_xml_tinyxml2
|
||||||
|
pp_project_warnings)
|
||||||
|
|
||||||
|
add_library(pp_platform_api STATIC
|
||||||
|
src/platform_api/platform_services.cpp
|
||||||
|
src/platform_api/platform_services.h)
|
||||||
|
target_include_directories(pp_platform_api
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(pp_platform_api
|
||||||
|
PUBLIC
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
|
||||||
|
add_library(pp_app_core STATIC
|
||||||
|
src/app_core/about_menu.h
|
||||||
|
src/app_core/app_preferences.h
|
||||||
|
src/app_core/app_status.h
|
||||||
|
src/app_core/brush_ui.h
|
||||||
|
src/app_core/canvas_tool_ui.h
|
||||||
|
src/app_core/document_animation.h
|
||||||
|
src/app_core/document_canvas.h
|
||||||
|
src/app_core/document_cloud.h
|
||||||
|
src/app_core/document_export.cpp
|
||||||
|
src/app_core/document_import.h
|
||||||
|
src/app_core/document_layer.h
|
||||||
|
src/app_core/document_platform_io.h
|
||||||
|
src/app_core/document_recording.h
|
||||||
|
src/app_core/document_resize.h
|
||||||
|
src/app_core/document_route.cpp
|
||||||
|
src/app_core/document_sharing.h
|
||||||
|
src/app_core/document_session.cpp
|
||||||
|
src/app_core/file_menu.h
|
||||||
|
src/app_core/grid_ui.h
|
||||||
|
src/app_core/history_ui.h
|
||||||
|
src/app_core/main_toolbar.h
|
||||||
|
src/app_core/quick_ui.h
|
||||||
|
src/app_core/tools_menu.h)
|
||||||
|
target_include_directories(pp_app_core
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(pp_app_core
|
||||||
|
PUBLIC
|
||||||
|
pp_foundation
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
|
||||||
|
if(PP_BUILD_TOOLS)
|
||||||
|
add_subdirectory(tools/pano_cli)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(PP_BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(PP_BUILD_APP)
|
||||||
|
if(WIN32)
|
||||||
|
add_library(pp_legacy_vendor OBJECT
|
||||||
|
${PP_VENDOR_SOURCES})
|
||||||
|
target_link_libraries(pp_legacy_vendor
|
||||||
|
PUBLIC
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
target_include_directories(pp_legacy_vendor
|
||||||
|
PUBLIC
|
||||||
|
${PP_LEGACY_INCLUDE_DIRS})
|
||||||
|
target_compile_definitions(pp_legacy_vendor
|
||||||
|
PUBLIC
|
||||||
|
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
_CRT_SECURE_NO_WARNINGS
|
||||||
|
_SCL_SECURE_NO_WARNINGS
|
||||||
|
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||||
|
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||||
|
_CONSOLE
|
||||||
|
WITH_CURL=1)
|
||||||
|
set_target_properties(pp_legacy_vendor PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
|
||||||
|
add_library(pp_legacy_renderer_gl OBJECT
|
||||||
|
${PP_LEGACY_RENDERER_GL_SOURCES})
|
||||||
|
target_link_libraries(pp_legacy_renderer_gl
|
||||||
|
PUBLIC
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_renderer_api
|
||||||
|
pp_project_warnings)
|
||||||
|
if(TARGET pp_renderer_gl)
|
||||||
|
target_link_libraries(pp_legacy_renderer_gl PRIVATE pp_renderer_gl)
|
||||||
|
endif()
|
||||||
|
target_include_directories(pp_legacy_renderer_gl
|
||||||
|
PUBLIC
|
||||||
|
${PP_LEGACY_INCLUDE_DIRS})
|
||||||
|
target_compile_definitions(pp_legacy_renderer_gl
|
||||||
|
PUBLIC
|
||||||
|
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
_CRT_SECURE_NO_WARNINGS
|
||||||
|
_SCL_SECURE_NO_WARNINGS
|
||||||
|
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||||
|
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||||
|
_CONSOLE
|
||||||
|
WITH_CURL=1)
|
||||||
|
set_target_properties(pp_legacy_renderer_gl PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
target_precompile_headers(pp_legacy_renderer_gl PRIVATE src/pch.h)
|
||||||
|
|
||||||
|
add_library(pp_legacy_assets_io OBJECT
|
||||||
|
${PP_LEGACY_ASSETS_IO_SOURCES})
|
||||||
|
target_link_libraries(pp_legacy_assets_io
|
||||||
|
PUBLIC
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_assets
|
||||||
|
pp_project_warnings)
|
||||||
|
target_include_directories(pp_legacy_assets_io
|
||||||
|
PUBLIC
|
||||||
|
${PP_LEGACY_INCLUDE_DIRS})
|
||||||
|
target_compile_definitions(pp_legacy_assets_io
|
||||||
|
PUBLIC
|
||||||
|
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
_CRT_SECURE_NO_WARNINGS
|
||||||
|
_SCL_SECURE_NO_WARNINGS
|
||||||
|
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||||
|
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||||
|
_CONSOLE
|
||||||
|
WITH_CURL=1)
|
||||||
|
set_target_properties(pp_legacy_assets_io PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
target_precompile_headers(pp_legacy_assets_io PRIVATE src/pch.h)
|
||||||
|
|
||||||
|
add_library(pp_legacy_paint_document OBJECT
|
||||||
|
${PP_LEGACY_PAINT_DOCUMENT_SOURCES})
|
||||||
|
target_link_libraries(pp_legacy_paint_document
|
||||||
|
PUBLIC
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_assets
|
||||||
|
pp_document
|
||||||
|
pp_paint
|
||||||
|
pp_paint_renderer
|
||||||
|
pp_renderer_api
|
||||||
|
pp_project_warnings)
|
||||||
|
if(TARGET pp_renderer_gl)
|
||||||
|
target_link_libraries(pp_legacy_paint_document PRIVATE pp_renderer_gl)
|
||||||
|
endif()
|
||||||
|
target_include_directories(pp_legacy_paint_document
|
||||||
|
PUBLIC
|
||||||
|
${PP_LEGACY_INCLUDE_DIRS})
|
||||||
|
target_compile_definitions(pp_legacy_paint_document
|
||||||
|
PUBLIC
|
||||||
|
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
_CRT_SECURE_NO_WARNINGS
|
||||||
|
_SCL_SECURE_NO_WARNINGS
|
||||||
|
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||||
|
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||||
|
_CONSOLE
|
||||||
|
WITH_CURL=1)
|
||||||
|
set_target_properties(pp_legacy_paint_document PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
target_precompile_headers(pp_legacy_paint_document PRIVATE src/pch.h)
|
||||||
|
|
||||||
|
add_library(pp_legacy_engine STATIC
|
||||||
|
${PP_LEGACY_ENGINE_SOURCES}
|
||||||
|
$<TARGET_OBJECTS:pp_legacy_assets_io>
|
||||||
|
$<TARGET_OBJECTS:pp_legacy_paint_document>
|
||||||
|
$<TARGET_OBJECTS:pp_legacy_renderer_gl>
|
||||||
|
$<TARGET_OBJECTS:pp_legacy_vendor>)
|
||||||
|
|
||||||
|
target_link_libraries(pp_legacy_engine
|
||||||
|
PUBLIC
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_assets
|
||||||
|
pp_document
|
||||||
|
pp_paint
|
||||||
|
pp_paint_renderer
|
||||||
|
pp_renderer_api
|
||||||
|
pp_project_warnings)
|
||||||
|
if(TARGET pp_renderer_gl)
|
||||||
|
target_link_libraries(pp_legacy_engine PRIVATE pp_renderer_gl)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_include_directories(pp_legacy_engine
|
||||||
|
PUBLIC
|
||||||
|
${PP_LEGACY_INCLUDE_DIRS})
|
||||||
|
|
||||||
|
target_compile_definitions(pp_legacy_engine
|
||||||
|
PUBLIC
|
||||||
|
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
_CRT_SECURE_NO_WARNINGS
|
||||||
|
_SCL_SECURE_NO_WARNINGS
|
||||||
|
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||||
|
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||||
|
_CONSOLE
|
||||||
|
WITH_CURL=1)
|
||||||
|
set_target_properties(pp_legacy_engine PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
|
||||||
|
target_precompile_headers(pp_legacy_engine PRIVATE src/pch.h)
|
||||||
|
|
||||||
|
add_library(pp_legacy_ui_core OBJECT
|
||||||
|
${PP_LEGACY_UI_CORE_SOURCES})
|
||||||
|
|
||||||
|
target_link_libraries(pp_legacy_ui_core
|
||||||
|
PUBLIC
|
||||||
|
pp_app_core
|
||||||
|
pp_legacy_engine
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_renderer_api
|
||||||
|
pp_project_warnings)
|
||||||
|
if(TARGET pp_renderer_gl)
|
||||||
|
target_link_libraries(pp_legacy_ui_core PRIVATE pp_renderer_gl)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_include_directories(pp_legacy_ui_core
|
||||||
|
PUBLIC
|
||||||
|
${PP_LEGACY_INCLUDE_DIRS})
|
||||||
|
|
||||||
|
target_compile_definitions(pp_legacy_ui_core
|
||||||
|
PUBLIC
|
||||||
|
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
_CRT_SECURE_NO_WARNINGS
|
||||||
|
_SCL_SECURE_NO_WARNINGS
|
||||||
|
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||||
|
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||||
|
_CONSOLE
|
||||||
|
WITH_CURL=1)
|
||||||
|
set_target_properties(pp_legacy_ui_core PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
|
||||||
|
target_precompile_headers(pp_legacy_ui_core PRIVATE src/pch.h)
|
||||||
|
|
||||||
|
add_library(pp_legacy_app STATIC
|
||||||
|
${PP_LEGACY_APP_SOURCES}
|
||||||
|
$<TARGET_OBJECTS:pp_legacy_ui_core>)
|
||||||
|
|
||||||
|
target_link_libraries(pp_legacy_app
|
||||||
|
PUBLIC
|
||||||
|
pp_legacy_engine
|
||||||
|
pp_legacy_ui_core
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_renderer_api
|
||||||
|
pp_project_warnings)
|
||||||
|
if(TARGET pp_renderer_gl)
|
||||||
|
target_link_libraries(pp_legacy_app PRIVATE pp_renderer_gl)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_include_directories(pp_legacy_app
|
||||||
|
PUBLIC
|
||||||
|
${PP_LEGACY_INCLUDE_DIRS})
|
||||||
|
|
||||||
|
target_compile_definitions(pp_legacy_app
|
||||||
|
PUBLIC
|
||||||
|
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
_CRT_SECURE_NO_WARNINGS
|
||||||
|
_SCL_SECURE_NO_WARNINGS
|
||||||
|
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||||
|
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||||
|
_CONSOLE
|
||||||
|
WITH_CURL=1)
|
||||||
|
set_target_properties(pp_legacy_app PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
|
||||||
|
target_precompile_headers(pp_legacy_app PRIVATE src/pch.h)
|
||||||
|
|
||||||
|
add_library(pp_panopainter_ui STATIC
|
||||||
|
${PP_PANOPAINTER_UI_SOURCES})
|
||||||
|
target_link_libraries(pp_panopainter_ui
|
||||||
|
PUBLIC
|
||||||
|
pp_legacy_app
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
target_precompile_headers(pp_panopainter_ui REUSE_FROM pp_legacy_app)
|
||||||
|
set_target_properties(pp_panopainter_ui PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
|
||||||
|
add_library(panopainter_app STATIC
|
||||||
|
${PP_PANOPAINTER_APP_SOURCES})
|
||||||
|
target_include_directories(panopainter_app
|
||||||
|
PUBLIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
|
target_link_libraries(panopainter_app
|
||||||
|
PUBLIC
|
||||||
|
pp_app_core
|
||||||
|
pp_legacy_app
|
||||||
|
pp_panopainter_ui
|
||||||
|
pp_platform_api
|
||||||
|
pp_project_options
|
||||||
|
PRIVATE
|
||||||
|
pp_project_warnings)
|
||||||
|
pp_add_version_generation(panopainter_app "$<IF:$<CONFIG:Debug>,debug,release>")
|
||||||
|
|
||||||
|
add_library(pp_platform_windows OBJECT
|
||||||
|
${PP_WINDOWS_PLATFORM_SOURCES})
|
||||||
|
target_link_libraries(pp_platform_windows
|
||||||
|
PUBLIC
|
||||||
|
panopainter_app
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.lib"
|
||||||
|
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.lib>"
|
||||||
|
"$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.lib>"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/yuv.lib"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.lib"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.lib"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/lib/win64/openvr_api.lib"
|
||||||
|
comdlg32
|
||||||
|
gdi32
|
||||||
|
opengl32
|
||||||
|
ole32
|
||||||
|
shell32
|
||||||
|
shlwapi
|
||||||
|
user32
|
||||||
|
wbemuuid
|
||||||
|
PRIVATE
|
||||||
|
pp_project_options
|
||||||
|
pp_project_warnings)
|
||||||
|
target_precompile_headers(pp_platform_windows REUSE_FROM pp_legacy_app)
|
||||||
|
set_target_properties(pp_platform_windows PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
|
||||||
|
add_executable(PanoPainter WIN32
|
||||||
|
${PP_WINDOWS_APP_SOURCES}
|
||||||
|
$<TARGET_OBJECTS:pp_platform_windows>)
|
||||||
|
|
||||||
|
target_link_libraries(PanoPainter
|
||||||
|
PRIVATE
|
||||||
|
pp_project_options
|
||||||
|
pp_project_warnings
|
||||||
|
pp_platform_windows)
|
||||||
|
|
||||||
|
set_target_properties(PanoPainter PROPERTIES
|
||||||
|
VS_GLOBAL_CharacterSet "Unicode")
|
||||||
|
|
||||||
|
pp_configure_windows_runtime_payloads(PanoPainter)
|
||||||
|
else()
|
||||||
|
message(WARNING "PP_BUILD_APP is enabled, but the root CMake app target is currently Windows-only. Platform alignment is tracked in Phase 6.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
295
CMakePresets.json
Normal file
295
CMakePresets.json
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
{
|
||||||
|
"version": 8,
|
||||||
|
"cmakeMinimumRequired": {
|
||||||
|
"major": 3,
|
||||||
|
"minor": 29,
|
||||||
|
"patch": 0
|
||||||
|
},
|
||||||
|
"configurePresets": [
|
||||||
|
{
|
||||||
|
"name": "base",
|
||||||
|
"hidden": true,
|
||||||
|
"binaryDir": "${sourceDir}/out/build/${presetName}",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
|
"PP_BUILD_APP": "ON",
|
||||||
|
"PP_BUILD_TESTS": "ON",
|
||||||
|
"PP_BUILD_TOOLS": "ON",
|
||||||
|
"PP_ENABLE_OPENGL": "ON",
|
||||||
|
"PP_ENABLE_VULKAN_EXPERIMENTAL": "OFF",
|
||||||
|
"PP_ENABLE_VR": "ON",
|
||||||
|
"PP_ENABLE_CLOUD": "ON",
|
||||||
|
"PP_ENABLE_VIDEO": "ON"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "platform-headless-base",
|
||||||
|
"hidden": true,
|
||||||
|
"inherits": "base",
|
||||||
|
"cacheVariables": {
|
||||||
|
"PP_BUILD_APP": "OFF",
|
||||||
|
"PP_ENABLE_CLOUD": "OFF",
|
||||||
|
"PP_ENABLE_VIDEO": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "windows-vs2026-x64",
|
||||||
|
"inherits": "base",
|
||||||
|
"displayName": "Windows VS 2026 x64",
|
||||||
|
"generator": "Visual Studio 18 2026",
|
||||||
|
"architecture": "x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "windows-msvc-default",
|
||||||
|
"inherits": "base",
|
||||||
|
"displayName": "Windows MSVC default generator",
|
||||||
|
"architecture": "x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "windows-msvc-vcpkg-headless",
|
||||||
|
"inherits": "platform-headless-base",
|
||||||
|
"displayName": "Windows MSVC vcpkg headless",
|
||||||
|
"architecture": "x64",
|
||||||
|
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||||
|
"cacheVariables": {
|
||||||
|
"PP_USE_VCPKG_TINYXML2": "ON"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "windows-clangcl-asan",
|
||||||
|
"inherits": "platform-headless-base",
|
||||||
|
"displayName": "Windows clang-cl ASan",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_C_COMPILER": "clang-cl",
|
||||||
|
"CMAKE_CXX_COMPILER": "clang-cl",
|
||||||
|
"CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreadedDLL",
|
||||||
|
"PP_ENABLE_ASAN": "ON",
|
||||||
|
"PP_ENABLE_UBSAN": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linux-clang",
|
||||||
|
"inherits": "platform-headless-base",
|
||||||
|
"displayName": "Linux clang",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_C_COMPILER": "clang",
|
||||||
|
"CMAKE_CXX_COMPILER": "clang++"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-base",
|
||||||
|
"hidden": true,
|
||||||
|
"inherits": "platform-headless-base",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"toolchainFile": "$env{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake",
|
||||||
|
"cacheVariables": {
|
||||||
|
"ANDROID_PLATFORM": "android-26",
|
||||||
|
"ANDROID_STL": "c++_shared",
|
||||||
|
"PP_ENABLE_VR": "OFF",
|
||||||
|
"PP_ENABLE_OPENGL": "ON"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-arm64",
|
||||||
|
"inherits": "android-base",
|
||||||
|
"displayName": "Android arm64-v8a",
|
||||||
|
"cacheVariables": {
|
||||||
|
"ANDROID_ABI": "arm64-v8a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-x64",
|
||||||
|
"inherits": "android-base",
|
||||||
|
"displayName": "Android x86_64",
|
||||||
|
"cacheVariables": {
|
||||||
|
"ANDROID_ABI": "x86_64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-quest-arm64",
|
||||||
|
"inherits": "android-base",
|
||||||
|
"displayName": "Android Quest arm64-v8a",
|
||||||
|
"cacheVariables": {
|
||||||
|
"ANDROID_ABI": "arm64-v8a",
|
||||||
|
"PP_ENABLE_VR": "ON",
|
||||||
|
"PP_ANDROID_FLAVOR": "quest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-focus-arm64",
|
||||||
|
"inherits": "android-base",
|
||||||
|
"displayName": "Android Focus/Wave arm64-v8a",
|
||||||
|
"cacheVariables": {
|
||||||
|
"ANDROID_ABI": "arm64-v8a",
|
||||||
|
"PP_ENABLE_VR": "ON",
|
||||||
|
"PP_ANDROID_FLAVOR": "focus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "emscripten",
|
||||||
|
"inherits": "platform-headless-base",
|
||||||
|
"displayName": "Emscripten WebGL",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"cacheVariables": {
|
||||||
|
"PP_ENABLE_VR": "OFF",
|
||||||
|
"PP_ENABLE_VIDEO": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "macos",
|
||||||
|
"inherits": "platform-headless-base",
|
||||||
|
"displayName": "macOS",
|
||||||
|
"generator": "Ninja"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ios-device",
|
||||||
|
"inherits": "platform-headless-base",
|
||||||
|
"displayName": "iOS device",
|
||||||
|
"generator": "Xcode",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_SYSTEM_NAME": "iOS",
|
||||||
|
"CMAKE_OSX_SYSROOT": "iphoneos"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ios-simulator",
|
||||||
|
"inherits": "platform-headless-base",
|
||||||
|
"displayName": "iOS simulator",
|
||||||
|
"generator": "Xcode",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_SYSTEM_NAME": "iOS",
|
||||||
|
"CMAKE_OSX_SYSROOT": "iphonesimulator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildPresets": [
|
||||||
|
{
|
||||||
|
"name": "windows-vs2026-x64",
|
||||||
|
"configurePreset": "windows-vs2026-x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "windows-msvc-default",
|
||||||
|
"configurePreset": "windows-msvc-default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "windows-msvc-vcpkg-headless",
|
||||||
|
"configurePreset": "windows-msvc-vcpkg-headless"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "windows-clangcl-asan",
|
||||||
|
"configurePreset": "windows-clangcl-asan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linux-clang",
|
||||||
|
"configurePreset": "linux-clang"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-arm64",
|
||||||
|
"configurePreset": "android-arm64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-x64",
|
||||||
|
"configurePreset": "android-x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-quest-arm64",
|
||||||
|
"configurePreset": "android-quest-arm64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-focus-arm64",
|
||||||
|
"configurePreset": "android-focus-arm64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "emscripten",
|
||||||
|
"configurePreset": "emscripten"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "macos",
|
||||||
|
"configurePreset": "macos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ios-device",
|
||||||
|
"configurePreset": "ios-device"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ios-simulator",
|
||||||
|
"configurePreset": "ios-simulator"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"testPresets": [
|
||||||
|
{
|
||||||
|
"name": "desktop-fast",
|
||||||
|
"configurePreset": "windows-msvc-default",
|
||||||
|
"output": {
|
||||||
|
"outputOnFailure": true
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"exclude": {
|
||||||
|
"label": "gpu|slow|platform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "desktop-fast-vs2026",
|
||||||
|
"configurePreset": "windows-vs2026-x64",
|
||||||
|
"output": {
|
||||||
|
"outputOnFailure": true
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"exclude": {
|
||||||
|
"label": "gpu|slow|platform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "desktop-fast-vcpkg",
|
||||||
|
"configurePreset": "windows-msvc-vcpkg-headless",
|
||||||
|
"output": {
|
||||||
|
"outputOnFailure": true
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"exclude": {
|
||||||
|
"label": "gpu|slow|platform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "desktop-gpu",
|
||||||
|
"configurePreset": "windows-msvc-default",
|
||||||
|
"output": {
|
||||||
|
"outputOnFailure": true
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"include": {
|
||||||
|
"label": "gpu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fuzz",
|
||||||
|
"configurePreset": "windows-msvc-default",
|
||||||
|
"output": {
|
||||||
|
"outputOnFailure": true
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"include": {
|
||||||
|
"label": "fuzz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stress",
|
||||||
|
"configurePreset": "windows-msvc-default",
|
||||||
|
"output": {
|
||||||
|
"outputOnFailure": true
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"include": {
|
||||||
|
"label": "stress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio 15
|
|
||||||
VisualStudioVersion = 15.0.28010.2026
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PanoPainter", "PanoPainter.vcxproj", "{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}"
|
|
||||||
EndProject
|
|
||||||
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "PanoPainterPackage", "PanoPainterPackage\PanoPainterPackage.wapproj", "{3A716FB6-DE62-439F-83B6-3C40915D6678}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Debug|x64 = Debug|x64
|
|
||||||
Debug|x86 = Debug|x86
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
Release|x64 = Release|x64
|
|
||||||
Release|x86 = Release|x86
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|Any CPU.ActiveCfg = Debug|Win32
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.ActiveCfg = Debug|x64
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.Build.0 = Debug|x64
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.Deploy.0 = Debug|x64
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x86.ActiveCfg = Debug|Win32
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x86.Build.0 = Debug|Win32
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|Any CPU.ActiveCfg = Release|Win32
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.ActiveCfg = Release|x64
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.Build.0 = Release|x64
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.Deploy.0 = Release|x64
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x86.ActiveCfg = Release|Win32
|
|
||||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x86.Build.0 = Release|Win32
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.ActiveCfg = Debug|x64
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.Build.0 = Debug|x64
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.Deploy.0 = Debug|x64
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.ActiveCfg = Debug|x86
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.Build.0 = Debug|x86
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.Deploy.0 = Debug|x86
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.ActiveCfg = Release|x64
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.Build.0 = Release|x64
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.Deploy.0 = Release|x64
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.ActiveCfg = Release|x86
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.Build.0 = Release|x86
|
|
||||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.Deploy.0 = Release|x86
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {3E8EFC4B-CEA1-4408-8628-7D2C0F6C43C8}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -1,634 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<ItemGroup Label="ProjectConfigurations">
|
|
||||||
<ProjectConfiguration Include="Debug|Win32">
|
|
||||||
<Configuration>Debug</Configuration>
|
|
||||||
<Platform>Win32</Platform>
|
|
||||||
</ProjectConfiguration>
|
|
||||||
<ProjectConfiguration Include="Release|Win32">
|
|
||||||
<Configuration>Release</Configuration>
|
|
||||||
<Platform>Win32</Platform>
|
|
||||||
</ProjectConfiguration>
|
|
||||||
<ProjectConfiguration Include="Debug|x64">
|
|
||||||
<Configuration>Debug</Configuration>
|
|
||||||
<Platform>x64</Platform>
|
|
||||||
</ProjectConfiguration>
|
|
||||||
<ProjectConfiguration Include="Release|x64">
|
|
||||||
<Configuration>Release</Configuration>
|
|
||||||
<Platform>x64</Platform>
|
|
||||||
</ProjectConfiguration>
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup Label="Globals">
|
|
||||||
<ProjectGuid>{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}</ProjectGuid>
|
|
||||||
<Keyword>Win32Proj</Keyword>
|
|
||||||
<RootNamespace>PanoPainter</RootNamespace>
|
|
||||||
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
|
|
||||||
<ProjectName>PanoPainter</ProjectName>
|
|
||||||
</PropertyGroup>
|
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
|
||||||
<ConfigurationType>Application</ConfigurationType>
|
|
||||||
<UseDebugLibraries>true</UseDebugLibraries>
|
|
||||||
<PlatformToolset>v141</PlatformToolset>
|
|
||||||
<CharacterSet>Unicode</CharacterSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
|
||||||
<ConfigurationType>Application</ConfigurationType>
|
|
||||||
<UseDebugLibraries>false</UseDebugLibraries>
|
|
||||||
<PlatformToolset>v141</PlatformToolset>
|
|
||||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
|
||||||
<CharacterSet>Unicode</CharacterSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
|
||||||
<ConfigurationType>Application</ConfigurationType>
|
|
||||||
<UseDebugLibraries>true</UseDebugLibraries>
|
|
||||||
<PlatformToolset>v142</PlatformToolset>
|
|
||||||
<CharacterSet>Unicode</CharacterSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
|
||||||
<ConfigurationType>Application</ConfigurationType>
|
|
||||||
<UseDebugLibraries>false</UseDebugLibraries>
|
|
||||||
<PlatformToolset>v142</PlatformToolset>
|
|
||||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
|
||||||
<CharacterSet>Unicode</CharacterSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
|
||||||
<ImportGroup Label="ExtensionSettings">
|
|
||||||
</ImportGroup>
|
|
||||||
<ImportGroup Label="Shared">
|
|
||||||
</ImportGroup>
|
|
||||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
|
||||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
|
||||||
</ImportGroup>
|
|
||||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
|
||||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
|
||||||
</ImportGroup>
|
|
||||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
|
||||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
|
||||||
</ImportGroup>
|
|
||||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
|
||||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
|
||||||
</ImportGroup>
|
|
||||||
<PropertyGroup Label="UserMacros" />
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
|
||||||
<LinkIncremental>true</LinkIncremental>
|
|
||||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\source\Client;$(IncludePath)</IncludePath>
|
|
||||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\bin;$(LibraryPath)</LibraryPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
|
||||||
<LinkIncremental>true</LinkIncremental>
|
|
||||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;libs\bugtrap-client\include;libs\poly2tri\poly2tri;libs\base64;libs\sqlite3;libs\openvr\headers;libs\nanort;libs\hash-library;libs\fmt\include;libs\glad\include;libs\openh264\include;libs\mp4v2\include;libs\libyuv\include;C:\Program Files\RenderDoc;$(IncludePath)</IncludePath>
|
|
||||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);libs\bugtrap-client\lib;libs\openvr\lib\win64;libs\openh264\lib;libs\mp4v2\lib\win;libs\libyuv\lib\win;$(LibraryPath)</LibraryPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
|
||||||
<LinkIncremental>false</LinkIncremental>
|
|
||||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\source\Client;$(IncludePath)</IncludePath>
|
|
||||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\bin;$(LibraryPath)</LibraryPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
|
||||||
<LinkIncremental>false</LinkIncremental>
|
|
||||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;libs\bugtrap-client\include;libs\poly2tri\poly2tri;libs\base64;libs\sqlite3;libs\openvr\headers;libs\nanort;libs\hash-library;libs\fmt\include;libs\glad\include;libs\openh264\include;libs\mp4v2\include;libs\libyuv\include;C:\Program Files\RenderDoc;$(IncludePath)</IncludePath>
|
|
||||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);libs\bugtrap-client\lib;libs\openvr\lib\win64;libs\openh264\lib;libs\mp4v2\lib\win;libs\libyuv\lib\win;$(LibraryPath)</LibraryPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
|
||||||
<ClCompile>
|
|
||||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
|
||||||
<WarningLevel>Level3</WarningLevel>
|
|
||||||
<Optimization>Disabled</Optimization>
|
|
||||||
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
|
||||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
|
||||||
</ClCompile>
|
|
||||||
<Link>
|
|
||||||
<SubSystem>Console</SubSystem>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
|
||||||
</Link>
|
|
||||||
<PreBuildEvent>
|
|
||||||
<Command>
|
|
||||||
</Command>
|
|
||||||
</PreBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
|
||||||
<ClCompile>
|
|
||||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
|
||||||
<WarningLevel>Level3</WarningLevel>
|
|
||||||
<Optimization>Disabled</Optimization>
|
|
||||||
<PreprocessorDefinitions>ENUM_BITFIELDS_NOT_SUPPORTED;DEBUG;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
|
||||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
|
||||||
<ExceptionHandling>false</ExceptionHandling>
|
|
||||||
</ClCompile>
|
|
||||||
<Link>
|
|
||||||
<SubSystem>Console</SubSystem>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
|
||||||
</Link>
|
|
||||||
<PreBuildEvent>
|
|
||||||
<Command>python .\scripts\pre-build.py debug</Command>
|
|
||||||
</PreBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
|
||||||
<ClCompile>
|
|
||||||
<WarningLevel>Level3</WarningLevel>
|
|
||||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
|
||||||
<Optimization>MaxSpeed</Optimization>
|
|
||||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
|
||||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
|
||||||
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
|
||||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
|
||||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
|
||||||
</ClCompile>
|
|
||||||
<Link>
|
|
||||||
<SubSystem>Console</SubSystem>
|
|
||||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
|
||||||
<OptimizeReferences>true</OptimizeReferences>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
|
||||||
</Link>
|
|
||||||
<PreBuildEvent>
|
|
||||||
<Command>
|
|
||||||
</Command>
|
|
||||||
</PreBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
|
||||||
<ClCompile>
|
|
||||||
<WarningLevel>Level3</WarningLevel>
|
|
||||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
|
||||||
<Optimization>MaxSpeed</Optimization>
|
|
||||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
|
||||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
|
||||||
<PreprocessorDefinitions>ENUM_BITFIELDS_NOT_SUPPORTED;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
|
||||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
|
||||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
|
||||||
<ExceptionHandling>false</ExceptionHandling>
|
|
||||||
</ClCompile>
|
|
||||||
<Link>
|
|
||||||
<SubSystem>Windows</SubSystem>
|
|
||||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
|
||||||
<OptimizeReferences>true</OptimizeReferences>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
|
||||||
</Link>
|
|
||||||
<PreBuildEvent>
|
|
||||||
<Command>python .\scripts\pre-build.py release</Command>
|
|
||||||
</PreBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ClCompile Include="libs\fmt\src\format.cc">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\fmt\src\posix.cc">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\glad\src\glad.c">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\glad\src\glad_wgl.c">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\hash-library\md5.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\nanort\nanort.cc">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\sqlite3\sqlite3.c">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\event\event.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\internal\experiments.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\log.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\Utils.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGConfig.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGEnums.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGLayout.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGNode.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGNodePrint.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGStyle.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGValue.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\Yoga.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
|
||||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
|
||||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\abr.cpp" />
|
|
||||||
<ClCompile Include="src\action.cpp" />
|
|
||||||
<ClCompile Include="src\app.cpp" />
|
|
||||||
<ClCompile Include="src\app_cloud.cpp" />
|
|
||||||
<ClCompile Include="src\app_commands.cpp" />
|
|
||||||
<ClCompile Include="src\app_dialogs.cpp" />
|
|
||||||
<ClCompile Include="src\app_events.cpp" />
|
|
||||||
<ClCompile Include="src\app_layout.cpp" />
|
|
||||||
<ClCompile Include="src\app_shaders.cpp" />
|
|
||||||
<ClCompile Include="src\app_vr.cpp" />
|
|
||||||
<ClCompile Include="src\asset.cpp" />
|
|
||||||
<ClCompile Include="src\bezier.cpp" />
|
|
||||||
<ClCompile Include="src\binary_stream.cpp" />
|
|
||||||
<ClCompile Include="src\brush.cpp" />
|
|
||||||
<ClCompile Include="src\canvas.cpp" />
|
|
||||||
<ClCompile Include="src\canvas_actions.cpp" />
|
|
||||||
<ClCompile Include="src\canvas_layer.cpp" />
|
|
||||||
<ClCompile Include="src\canvas_modes.cpp" />
|
|
||||||
<ClCompile Include="src\event.cpp" />
|
|
||||||
<ClCompile Include="src\font.cpp" />
|
|
||||||
<ClCompile Include="src\hmd.cpp" />
|
|
||||||
<ClCompile Include="src\image.cpp" />
|
|
||||||
<ClCompile Include="src\layout.cpp" />
|
|
||||||
<ClCompile Include="src\log.cpp" />
|
|
||||||
<ClCompile Include="src\main.cpp" />
|
|
||||||
<ClCompile Include="src\mp4enc.cpp" />
|
|
||||||
<ClCompile Include="src\node.cpp" />
|
|
||||||
<ClCompile Include="src\node_about.cpp" />
|
|
||||||
<ClCompile Include="src\node_border.cpp" />
|
|
||||||
<ClCompile Include="src\node_button.cpp" />
|
|
||||||
<ClCompile Include="src\node_button_custom.cpp" />
|
|
||||||
<ClCompile Include="src\node_canvas.cpp" />
|
|
||||||
<ClCompile Include="src\node_changelog.cpp" />
|
|
||||||
<ClCompile Include="src\node_checkbox.cpp" />
|
|
||||||
<ClCompile Include="src\node_colorwheel.cpp" />
|
|
||||||
<ClCompile Include="src\node_color_quad.cpp" />
|
|
||||||
<ClCompile Include="src\node_combobox.cpp" />
|
|
||||||
<ClCompile Include="src\node_dialog_browse.cpp" />
|
|
||||||
<ClCompile Include="src\node_dialog_cloud.cpp" />
|
|
||||||
<ClCompile Include="src\node_dialog_export_ppbr.cpp" />
|
|
||||||
<ClCompile Include="src\node_dialog_layer_rename.cpp" />
|
|
||||||
<ClCompile Include="src\node_dialog_open.cpp" />
|
|
||||||
<ClCompile Include="src\node_dialog_picker.cpp" />
|
|
||||||
<ClCompile Include="src\node_dialog_resize.cpp" />
|
|
||||||
<ClCompile Include="src\node_icon.cpp" />
|
|
||||||
<ClCompile Include="src\node_image.cpp" />
|
|
||||||
<ClCompile Include="src\node_image_texture.cpp" />
|
|
||||||
<ClCompile Include="src\node_input_box.cpp" />
|
|
||||||
<ClCompile Include="src\node_message_box.cpp" />
|
|
||||||
<ClCompile Include="src\node_metadata.cpp" />
|
|
||||||
<ClCompile Include="src\node_panel_brush.cpp" />
|
|
||||||
<ClCompile Include="src\node_panel_color.cpp" />
|
|
||||||
<ClCompile Include="src\node_panel_floating.cpp" />
|
|
||||||
<ClCompile Include="src\node_panel_grid.cpp" />
|
|
||||||
<ClCompile Include="src\node_panel_layer.cpp" />
|
|
||||||
<ClCompile Include="src\node_panel_quick.cpp" />
|
|
||||||
<ClCompile Include="src\node_panel_stroke.cpp" />
|
|
||||||
<ClCompile Include="src\node_panel_animation.cpp" />
|
|
||||||
<ClCompile Include="src\node_popup_menu.cpp" />
|
|
||||||
<ClCompile Include="src\node_progress_bar.cpp" />
|
|
||||||
<ClCompile Include="src\node_remote_page.cpp" />
|
|
||||||
<ClCompile Include="src\node_scroll.cpp" />
|
|
||||||
<ClCompile Include="src\node_settings.cpp" />
|
|
||||||
<ClCompile Include="src\node_shorcuts.cpp" />
|
|
||||||
<ClCompile Include="src\node_slider.cpp" />
|
|
||||||
<ClCompile Include="src\node_stroke_preview.cpp" />
|
|
||||||
<ClCompile Include="src\node_text.cpp" />
|
|
||||||
<ClCompile Include="src\node_text_input.cpp" />
|
|
||||||
<ClCompile Include="src\node_tool_bucket.cpp" />
|
|
||||||
<ClCompile Include="src\node_usermanual.cpp" />
|
|
||||||
<ClCompile Include="src\node_viewport.cpp" />
|
|
||||||
<ClCompile Include="src\pch.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\rtt.cpp" />
|
|
||||||
<ClCompile Include="src\serializer.cpp" />
|
|
||||||
<ClCompile Include="src\settings.cpp" />
|
|
||||||
<ClCompile Include="src\shader.cpp" />
|
|
||||||
<ClCompile Include="src\shape.cpp" />
|
|
||||||
<ClCompile Include="src\texture.cpp" />
|
|
||||||
<ClCompile Include="src\util.cpp" />
|
|
||||||
<ClCompile Include="src\version.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\wacom.cpp" />
|
|
||||||
<ClCompile Include="libs\jpeg\jpgd.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\jpeg\jpge.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\common\shapes.cc">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\advancing_front.cc">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\cdt.cc">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep.cc">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep_context.cc">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\tinyxml2\tinyxml2.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\wacom\WinTab\Utils.cpp">
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Use</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
|
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
|
|
||||||
</ClCompile>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ClInclude Include="libs\hash-library\md5.h" />
|
|
||||||
<ClInclude Include="libs\nanort\nanort.h" />
|
|
||||||
<ClInclude Include="libs\sqlite3\sqlite3.h" />
|
|
||||||
<ClInclude Include="libs\sqlite3\sqlite3ext.h" />
|
|
||||||
<ClInclude Include="resource.h" />
|
|
||||||
<ClInclude Include="src\abr.h" />
|
|
||||||
<ClInclude Include="src\action.h" />
|
|
||||||
<ClInclude Include="src\app.h" />
|
|
||||||
<ClInclude Include="src\asset.h" />
|
|
||||||
<ClInclude Include="src\bezier.h" />
|
|
||||||
<ClInclude Include="src\binary_stream.h" />
|
|
||||||
<ClInclude Include="src\brush.h" />
|
|
||||||
<ClInclude Include="src\canvas.h" />
|
|
||||||
<ClInclude Include="src\canvas_actions.h" />
|
|
||||||
<ClInclude Include="src\canvas_layer.h" />
|
|
||||||
<ClInclude Include="src\canvas_modes.h" />
|
|
||||||
<ClInclude Include="src\event.h" />
|
|
||||||
<ClInclude Include="src\font.h" />
|
|
||||||
<ClInclude Include="src\hmd.h" />
|
|
||||||
<ClInclude Include="src\image.h" />
|
|
||||||
<ClInclude Include="src\keymap.h" />
|
|
||||||
<ClInclude Include="src\layout.h" />
|
|
||||||
<ClInclude Include="src\log.h" />
|
|
||||||
<ClInclude Include="src\mp4enc.h" />
|
|
||||||
<ClInclude Include="src\node.h" />
|
|
||||||
<ClInclude Include="src\node_about.h" />
|
|
||||||
<ClInclude Include="src\node_border.h" />
|
|
||||||
<ClInclude Include="src\node_button.h" />
|
|
||||||
<ClInclude Include="src\node_button_custom.h" />
|
|
||||||
<ClInclude Include="src\node_canvas.h" />
|
|
||||||
<ClInclude Include="src\node_changelog.h" />
|
|
||||||
<ClInclude Include="src\node_checkbox.h" />
|
|
||||||
<ClInclude Include="src\node_colorwheel.h" />
|
|
||||||
<ClInclude Include="src\node_color_quad.h" />
|
|
||||||
<ClInclude Include="src\node_combobox.h" />
|
|
||||||
<ClInclude Include="src\node_dialog_browse.h" />
|
|
||||||
<ClInclude Include="src\node_dialog_cloud.h" />
|
|
||||||
<ClInclude Include="src\node_dialog_export_ppbr.h" />
|
|
||||||
<ClInclude Include="src\node_dialog_layer_rename.h" />
|
|
||||||
<ClInclude Include="src\node_dialog_open.h" />
|
|
||||||
<ClInclude Include="src\node_dialog_picker.h" />
|
|
||||||
<ClInclude Include="src\node_dialog_resize.h" />
|
|
||||||
<ClInclude Include="src\node_icon.h" />
|
|
||||||
<ClInclude Include="src\node_image.h" />
|
|
||||||
<ClInclude Include="src\node_image_texture.h" />
|
|
||||||
<ClInclude Include="src\node_input_box.h" />
|
|
||||||
<ClInclude Include="src\node_message_box.h" />
|
|
||||||
<ClInclude Include="src\node_metadata.h" />
|
|
||||||
<ClInclude Include="src\node_panel_brush.h" />
|
|
||||||
<ClInclude Include="src\node_panel_color.h" />
|
|
||||||
<ClInclude Include="src\node_panel_floating.h" />
|
|
||||||
<ClInclude Include="src\node_panel_grid.h" />
|
|
||||||
<ClInclude Include="src\node_panel_layer.h" />
|
|
||||||
<ClInclude Include="src\node_panel_quick.h" />
|
|
||||||
<ClInclude Include="src\node_panel_stroke.h" />
|
|
||||||
<ClInclude Include="src\node_panel_animation.h" />
|
|
||||||
<ClInclude Include="src\node_popup_menu.h" />
|
|
||||||
<ClInclude Include="src\node_progress_bar.h" />
|
|
||||||
<ClInclude Include="src\node_remote_page.h" />
|
|
||||||
<ClInclude Include="src\node_scroll.h" />
|
|
||||||
<ClInclude Include="src\node_settings.h" />
|
|
||||||
<ClInclude Include="src\node_shorcuts.h" />
|
|
||||||
<ClInclude Include="src\node_slider.h" />
|
|
||||||
<ClInclude Include="src\node_stroke_preview.h" />
|
|
||||||
<ClInclude Include="src\node_text.h" />
|
|
||||||
<ClInclude Include="src\node_text_input.h" />
|
|
||||||
<ClInclude Include="src\node_tool_bucket.h" />
|
|
||||||
<ClInclude Include="src\node_usermanual.h" />
|
|
||||||
<ClInclude Include="src\node_viewport.h" />
|
|
||||||
<ClInclude Include="src\pch.h" />
|
|
||||||
<ClInclude Include="src\rtt.h" />
|
|
||||||
<ClInclude Include="src\serializer.h" />
|
|
||||||
<ClInclude Include="src\settings.h" />
|
|
||||||
<ClInclude Include="src\shader.h" />
|
|
||||||
<ClInclude Include="src\shape.h" />
|
|
||||||
<ClInclude Include="src\texture.h" />
|
|
||||||
<ClInclude Include="src\util.h" />
|
|
||||||
<ClInclude Include="src\version.gen.h" />
|
|
||||||
<ClInclude Include="src\version.h" />
|
|
||||||
<ClInclude Include="src\wacom.h" />
|
|
||||||
<ClInclude Include="libs\jpeg\jpgd.h" />
|
|
||||||
<ClInclude Include="libs\jpeg\jpge.h" />
|
|
||||||
<ClInclude Include="libs\tinyxml2\tinyxml2.h" />
|
|
||||||
<ClInclude Include="libs\wacom\WinTab\MSGPACK.H" />
|
|
||||||
<ClInclude Include="libs\wacom\WinTab\PKTDEF.H" />
|
|
||||||
<ClInclude Include="libs\wacom\WinTab\Utils.h" />
|
|
||||||
<ClInclude Include="libs\wacom\WinTab\WINTAB.H" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ResourceCompile Include="PanoPainter.rc" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Natvis Include="libs\glm\util\glm.natvis" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Xml Include="data\dialogs\about.xml" />
|
|
||||||
<Xml Include="data\dialogs\brush-export.xml" />
|
|
||||||
<Xml Include="data\dialogs\changelog.xml" />
|
|
||||||
<Xml Include="data\dialogs\cloud-browse.xml" />
|
|
||||||
<Xml Include="data\dialogs\color-picker.xml" />
|
|
||||||
<Xml Include="data\dialogs\doc-browse.xml" />
|
|
||||||
<Xml Include="data\dialogs\doc-new.xml" />
|
|
||||||
<Xml Include="data\dialogs\doc-open.xml" />
|
|
||||||
<Xml Include="data\dialogs\doc-resize.xml" />
|
|
||||||
<Xml Include="data\dialogs\doc-save.xml" />
|
|
||||||
<Xml Include="data\dialogs\input-box.xml" />
|
|
||||||
<Xml Include="data\dialogs\layer-rename.xml" />
|
|
||||||
<Xml Include="data\dialogs\message-box.xml" />
|
|
||||||
<Xml Include="data\dialogs\panel-animation.xml" />
|
|
||||||
<Xml Include="data\dialogs\panel-floating.xml" />
|
|
||||||
<Xml Include="data\dialogs\panel-grid.xml" />
|
|
||||||
<Xml Include="data\dialogs\panel-layers.xml" />
|
|
||||||
<Xml Include="data\dialogs\panel-brushes.xml" />
|
|
||||||
<Xml Include="data\dialogs\panel-presets.xml" />
|
|
||||||
<Xml Include="data\dialogs\panel-quick.xml" />
|
|
||||||
<Xml Include="data\dialogs\panel-stroke.xml" />
|
|
||||||
<Xml Include="data\dialogs\progress-bar.xml" />
|
|
||||||
<Xml Include="data\dialogs\remote-page.xml" />
|
|
||||||
<Xml Include="data\dialogs\settings.xml" />
|
|
||||||
<Xml Include="data\dialogs\shortcuts.xml" />
|
|
||||||
<Xml Include="data\dialogs\usermanual.xml" />
|
|
||||||
<Xml Include="data\layout.xml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
|
||||||
</DeploymentContent>
|
|
||||||
</Xml>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="data\shaders\atlas.glsl" />
|
|
||||||
<None Include="data\shaders\bake-uv.glsl" />
|
|
||||||
<None Include="data\shaders\checkerboard.glsl" />
|
|
||||||
<None Include="data\shaders\color-hue.glsl" />
|
|
||||||
<None Include="data\shaders\color-quad.glsl" />
|
|
||||||
<None Include="data\shaders\color-tri.glsl" />
|
|
||||||
<None Include="data\shaders\color.glsl" />
|
|
||||||
<None Include="data\shaders\comp-draw.glsl" />
|
|
||||||
<None Include="data\shaders\comp-erase.glsl" />
|
|
||||||
<None Include="data\shaders\equirect.glsl" />
|
|
||||||
<None Include="data\shaders\font.glsl" />
|
|
||||||
<None Include="data\shaders\include\blend-stroke.glsl" />
|
|
||||||
<None Include="data\shaders\include\blend.glsl" />
|
|
||||||
<None Include="data\shaders\include\blur.glsl" />
|
|
||||||
<None Include="data\shaders\include\color.glsl" />
|
|
||||||
<None Include="data\shaders\include\ext-fb-fetch.glsl" />
|
|
||||||
<None Include="data\shaders\include\hsv.glsl" />
|
|
||||||
<None Include="data\shaders\include\rand.glsl" />
|
|
||||||
<None Include="data\shaders\lambert.glsl" />
|
|
||||||
<None Include="data\shaders\lightmap.glsl" />
|
|
||||||
<None Include="data\shaders\stroke-dilate.glsl" />
|
|
||||||
<None Include="data\shaders\stroke-instanced.glsl" />
|
|
||||||
<None Include="data\shaders\stroke-pad.glsl" />
|
|
||||||
<None Include="data\shaders\stroke-preview.glsl" />
|
|
||||||
<None Include="data\shaders\stroke.glsl" />
|
|
||||||
<None Include="data\shaders\texture-alpha.glsl" />
|
|
||||||
<None Include="data\shaders\texture-blend.glsl" />
|
|
||||||
<None Include="data\shaders\texture-colorize.glsl" />
|
|
||||||
<None Include="data\shaders\texture-mask.glsl" />
|
|
||||||
<None Include="data\shaders\texture.glsl" />
|
|
||||||
<None Include="data\shaders\uvs.glsl" />
|
|
||||||
<None Include="data\shaders\vertex-color.glsl" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Image Include="icon.ico" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Xsd Include="extra\layout.xsd">
|
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
|
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
|
||||||
</Xsd>
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
|
||||||
<ImportGroup Label="ExtensionTargets">
|
|
||||||
</ImportGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,854 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<ItemGroup>
|
|
||||||
<Filter Include="Source Files">
|
|
||||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
|
||||||
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="Resource Files">
|
|
||||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
|
||||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="Source Files\ui">
|
|
||||||
<UniqueIdentifier>{600b8daa-4234-4c37-b4ba-c22cad7d1dc3}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs">
|
|
||||||
<UniqueIdentifier>{6d64b115-02d1-43e0-86c8-c8212f51162d}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\jpeg">
|
|
||||||
<UniqueIdentifier>{dc178d53-6a6d-4a18-a93c-d4994340515f}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\WinTab">
|
|
||||||
<UniqueIdentifier>{54dc9f46-d2e0-466c-90d2-eb5d72d5799d}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\yoga">
|
|
||||||
<UniqueIdentifier>{a4a12057-835e-47ff-be4d-ce58b36cecf5}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\tinyxml2">
|
|
||||||
<UniqueIdentifier>{6fe315aa-e2b9-4f01-8291-683a5fda123b}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\poly2tri">
|
|
||||||
<UniqueIdentifier>{bda6fa93-a186-41ca-9bd9-49b7e0fd1ca4}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="extras">
|
|
||||||
<UniqueIdentifier>{e631ac80-1b9b-424f-8adf-e2bab71a566d}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\sqlite3">
|
|
||||||
<UniqueIdentifier>{ef44d179-f28b-458c-b3df-be2895553149}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\nanort">
|
|
||||||
<UniqueIdentifier>{be0c0053-abd8-4e2d-a294-7c54511b05a6}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\hash">
|
|
||||||
<UniqueIdentifier>{2a784067-6741-47a3-b668-cc45f2224286}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\fmt">
|
|
||||||
<UniqueIdentifier>{7b4f5b47-7a8b-4e4c-9e82-399bb5047ffc}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="shaders">
|
|
||||||
<UniqueIdentifier>{b55fb692-a845-4ef2-9b0e-5b2dd8bd125f}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="shaders\include">
|
|
||||||
<UniqueIdentifier>{a2cacb13-2854-44ee-9511-6cb8ac587428}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="libs\glad">
|
|
||||||
<UniqueIdentifier>{ca37521b-213f-4bcf-acfd-eda1483a30b2}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
<Filter Include="extras\dialogs">
|
|
||||||
<UniqueIdentifier>{5ecb54ed-7c3d-46fd-9b5d-227abdbc5954}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ClCompile Include="src\app.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\image.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\main.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\shader.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\shape.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\texture.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\pch.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\font.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\util.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\asset.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\rtt.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\bezier.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\canvas.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\brush.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\log.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\action.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\event.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\canvas_modes.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_border.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_button.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_button_custom.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_canvas.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_checkbox.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_color_quad.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_dialog_open.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_icon.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_image.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_image_texture.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_message_box.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_panel_brush.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_panel_color.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_panel_layer.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_panel_stroke.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_popup_menu.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_settings.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_slider.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_stroke_preview.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_text.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_text_input.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_viewport.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\layout.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_scroll.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\app_shaders.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\app_layout.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\app_events.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\jpeg\jpgd.cpp">
|
|
||||||
<Filter>libs\jpeg</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\jpeg\jpge.cpp">
|
|
||||||
<Filter>libs\jpeg</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\tinyxml2\tinyxml2.cpp">
|
|
||||||
<Filter>libs\tinyxml2</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\wacom.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\wacom\WinTab\Utils.cpp">
|
|
||||||
<Filter>libs\WinTab</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_dialog_layer_rename.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\app_dialogs.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\common\shapes.cc">
|
|
||||||
<Filter>libs\poly2tri</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\advancing_front.cc">
|
|
||||||
<Filter>libs\poly2tri</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\cdt.cc">
|
|
||||||
<Filter>libs\poly2tri</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep.cc">
|
|
||||||
<Filter>libs\poly2tri</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep_context.cc">
|
|
||||||
<Filter>libs\poly2tri</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_progress_bar.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_dialog_browse.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\app_commands.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_dialog_cloud.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\app_cloud.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_combobox.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_dialog_picker.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_colorwheel.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_panel_grid.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\version.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_about.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_changelog.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_usermanual.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_dialog_resize.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\sqlite3\sqlite3.c">
|
|
||||||
<Filter>libs\sqlite3</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\hmd.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\app_vr.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\nanort\nanort.cc">
|
|
||||||
<Filter>libs\nanort</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\hash-library\md5.cpp">
|
|
||||||
<Filter>libs\hash</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\abr.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\fmt\src\format.cc">
|
|
||||||
<Filter>libs\fmt</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\fmt\src\posix.cc">
|
|
||||||
<Filter>libs\fmt</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_panel_quick.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\binary_stream.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\serializer.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGConfig.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGEnums.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGLayout.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGNode.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGNodePrint.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGStyle.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\YGValue.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\Yoga.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\log.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\Utils.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_panel_floating.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\settings.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\canvas_actions.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\canvas_layer.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_tool_bucket.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\glad\src\glad.c">
|
|
||||||
<Filter>libs\glad</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\glad\src\glad_wgl.c">
|
|
||||||
<Filter>libs\glad</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_dialog_export_ppbr.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_input_box.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_panel_animation.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\internal\experiments.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="libs\yoga\yoga\event\event.cpp">
|
|
||||||
<Filter>libs\yoga</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\mp4enc.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_remote_page.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_metadata.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="src\node_shorcuts.cpp">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ClInclude Include="libs\jpeg\jpgd.h">
|
|
||||||
<Filter>libs\jpeg</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\jpeg\jpge.h">
|
|
||||||
<Filter>libs\jpeg</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\tinyxml2\tinyxml2.h">
|
|
||||||
<Filter>libs\tinyxml2</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\wacom\WinTab\PKTDEF.H">
|
|
||||||
<Filter>libs\WinTab</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\wacom\WinTab\Utils.h">
|
|
||||||
<Filter>libs\WinTab</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\wacom\WinTab\WINTAB.H">
|
|
||||||
<Filter>libs\WinTab</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\wacom\WinTab\MSGPACK.H">
|
|
||||||
<Filter>libs\WinTab</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\sqlite3\sqlite3.h">
|
|
||||||
<Filter>libs\sqlite3</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\sqlite3\sqlite3ext.h">
|
|
||||||
<Filter>libs\sqlite3</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\nanort\nanort.h">
|
|
||||||
<Filter>libs\nanort</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="libs\hash-library\md5.h">
|
|
||||||
<Filter>libs\hash</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\abr.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\action.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\app.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\asset.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\bezier.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\binary_stream.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\brush.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\canvas.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\canvas_actions.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\canvas_layer.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\canvas_modes.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\event.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\font.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\hmd.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\image.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\keymap.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\log.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\pch.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="resource.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\rtt.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\serializer.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\settings.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\shader.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\shape.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\texture.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\util.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\version.gen.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\version.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\wacom.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\layout.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_about.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_border.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_button.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_button_custom.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_canvas.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_changelog.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_checkbox.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_color_quad.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_colorwheel.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_combobox.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_dialog_browse.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_dialog_cloud.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_dialog_export_ppbr.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_dialog_layer_rename.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_dialog_open.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_dialog_picker.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_dialog_resize.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_icon.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_image.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_image_texture.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_input_box.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_message_box.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_panel_brush.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_panel_color.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_panel_floating.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_panel_grid.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_panel_layer.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_panel_quick.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_panel_stroke.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_popup_menu.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_progress_bar.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_scroll.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_settings.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_slider.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_stroke_preview.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_text.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_text_input.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_tool_bucket.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_usermanual.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_viewport.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_panel_animation.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_remote_page.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_metadata.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\node_shorcuts.h">
|
|
||||||
<Filter>Source Files\ui</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
<ClInclude Include="src\mp4enc.h">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ResourceCompile Include="PanoPainter.rc">
|
|
||||||
<Filter>Resource Files</Filter>
|
|
||||||
</ResourceCompile>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Natvis Include="libs\glm\util\glm.natvis">
|
|
||||||
<Filter>extras</Filter>
|
|
||||||
</Natvis>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Xml Include="data\layout.xml">
|
|
||||||
<Filter>extras</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\changelog.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\about.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\usermanual.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\brush-export.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\panel-layers.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\panel-brushes.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\panel-stroke.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\panel-grid.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\panel-quick.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\color-picker.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\input-box.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\message-box.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\progress-bar.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\layer-rename.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\doc-resize.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\doc-browse.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\doc-new.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\doc-save.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\cloud-browse.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\settings.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\doc-open.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\panel-floating.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\panel-presets.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\panel-animation.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\remote-page.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
<Xml Include="data\dialogs\shortcuts.xml">
|
|
||||||
<Filter>extras\dialogs</Filter>
|
|
||||||
</Xml>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="data\shaders\texture.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\include\blend-stroke.glsl">
|
|
||||||
<Filter>shaders\include</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\include\blur.glsl">
|
|
||||||
<Filter>shaders\include</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\include\color.glsl">
|
|
||||||
<Filter>shaders\include</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\include\ext-fb-fetch.glsl">
|
|
||||||
<Filter>shaders\include</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\include\hsv.glsl">
|
|
||||||
<Filter>shaders\include</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\include\rand.glsl">
|
|
||||||
<Filter>shaders\include</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\include\blend.glsl">
|
|
||||||
<Filter>shaders\include</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\comp-draw.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\comp-erase.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\equirect.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\font.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\lambert.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\lightmap.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\stroke.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\stroke-instanced.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\stroke-preview.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\texture-alpha.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\texture-blend.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\uvs.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\vertex-color.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\atlas.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\bake-uv.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\checkerboard.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\color.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\color-hue.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\color-quad.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\color-tri.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\texture-colorize.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\texture-mask.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\stroke-dilate.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
<None Include="data\shaders\stroke-pad.glsl">
|
|
||||||
<Filter>shaders</Filter>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Image Include="icon.ico">
|
|
||||||
<Filter>Resource Files</Filter>
|
|
||||||
</Image>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Xsd Include="extra\layout.xsd">
|
|
||||||
<Filter>extras</Filter>
|
|
||||||
</Xsd>
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
20
cmake/PanoPainterOptions.cmake
Normal file
20
cmake/PanoPainterOptions.cmake
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
option(PP_BUILD_APP "Build the PanoPainter application target from root CMake." ON)
|
||||||
|
option(PP_BUILD_TESTS "Build PanoPainter tests." ON)
|
||||||
|
option(PP_BUILD_TOOLS "Build PanoPainter automation tools." ON)
|
||||||
|
|
||||||
|
option(PP_ENABLE_OPENGL "Enable the OpenGL renderer backend." ON)
|
||||||
|
option(PP_ENABLE_VULKAN_EXPERIMENTAL "Enable non-production Vulkan experiments." OFF)
|
||||||
|
option(PP_ENABLE_VR "Enable VR support." ON)
|
||||||
|
option(PP_ENABLE_CLOUD "Enable cloud/network features." ON)
|
||||||
|
option(PP_ENABLE_VIDEO "Enable MP4/timelapse video features." ON)
|
||||||
|
|
||||||
|
option(PP_ENABLE_ASAN "Enable AddressSanitizer where supported." OFF)
|
||||||
|
option(PP_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer where supported." OFF)
|
||||||
|
option(PP_ENABLE_TSAN "Enable ThreadSanitizer for headless targets where supported." OFF)
|
||||||
|
option(PP_ENABLE_MSVC_ANALYZE "Enable MSVC static analysis." OFF)
|
||||||
|
option(PP_ENABLE_CLANG_TIDY "Enable clang-tidy integration." OFF)
|
||||||
|
option(PP_ENABLE_CPPCHECK "Enable cppcheck integration." OFF)
|
||||||
|
option(PP_USE_VCPKG_TINYXML2 "Use the vcpkg tinyxml2 package for component targets." OFF)
|
||||||
|
|
||||||
|
set(PP_ANDROID_FLAVOR "standard" CACHE STRING "Android package flavor: standard, quest, or focus.")
|
||||||
|
set_property(CACHE PP_ANDROID_FLAVOR PROPERTY STRINGS standard quest focus)
|
||||||
26
cmake/PanoPainterRuntime.cmake
Normal file
26
cmake/PanoPainterRuntime.cmake
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
function(pp_configure_windows_runtime_payloads target_name)
|
||||||
|
if(NOT TARGET "${target_name}")
|
||||||
|
message(FATAL_ERROR "pp_configure_windows_runtime_payloads target does not exist: ${target_name}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_custom_command(TARGET "${target_name}" POST_BUILD
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E copy_directory
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||||
|
"$<TARGET_FILE_DIR:${target_name}>/data"
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.dll"
|
||||||
|
"$<TARGET_FILE_DIR:${target_name}>/BugTrapU-x64.dll"
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||||
|
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.dll>$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.dll>"
|
||||||
|
"$<TARGET_FILE_DIR:${target_name}>/"
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/libyuv.dll"
|
||||||
|
"$<TARGET_FILE_DIR:${target_name}>/libyuv.dll"
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.dll"
|
||||||
|
"$<TARGET_FILE_DIR:${target_name}>/libmp4v2.dll"
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.dll"
|
||||||
|
"$<TARGET_FILE_DIR:${target_name}>/openh264-2.0.0-win64.dll"
|
||||||
|
VERBATIM)
|
||||||
|
endfunction()
|
||||||
170
cmake/PanoPainterSources.cmake
Normal file
170
cmake/PanoPainterSources.cmake
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
set(PP_LEGACY_ENGINE_SOURCES
|
||||||
|
src/hmd.cpp
|
||||||
|
src/log.cpp
|
||||||
|
src/mp4enc.cpp
|
||||||
|
src/util.cpp
|
||||||
|
src/wacom.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_LEGACY_ASSETS_IO_SOURCES
|
||||||
|
src/abr.cpp
|
||||||
|
src/asset.cpp
|
||||||
|
src/binary_stream.cpp
|
||||||
|
src/image.cpp
|
||||||
|
src/serializer.cpp
|
||||||
|
src/settings.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_LEGACY_PAINT_DOCUMENT_SOURCES
|
||||||
|
src/action.cpp
|
||||||
|
src/bezier.cpp
|
||||||
|
src/brush.cpp
|
||||||
|
src/canvas.cpp
|
||||||
|
src/canvas_actions.cpp
|
||||||
|
src/canvas_layer.cpp
|
||||||
|
src/event.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_LEGACY_RENDERER_GL_SOURCES
|
||||||
|
src/font.cpp
|
||||||
|
src/rtt.cpp
|
||||||
|
src/shader.cpp
|
||||||
|
src/shape.cpp
|
||||||
|
src/texture.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_LEGACY_UI_CORE_SOURCES
|
||||||
|
src/layout.cpp
|
||||||
|
src/node.cpp
|
||||||
|
src/node_border.cpp
|
||||||
|
src/node_button.cpp
|
||||||
|
src/node_button_custom.cpp
|
||||||
|
src/node_checkbox.cpp
|
||||||
|
src/node_combobox.cpp
|
||||||
|
src/node_icon.cpp
|
||||||
|
src/node_image.cpp
|
||||||
|
src/node_image_texture.cpp
|
||||||
|
src/node_input_box.cpp
|
||||||
|
src/node_popup_menu.cpp
|
||||||
|
src/node_progress_bar.cpp
|
||||||
|
src/node_remote_page.cpp
|
||||||
|
src/node_scroll.cpp
|
||||||
|
src/node_settings.cpp
|
||||||
|
src/node_shorcuts.cpp
|
||||||
|
src/node_slider.cpp
|
||||||
|
src/node_text.cpp
|
||||||
|
src/node_text_input.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_LEGACY_APP_SOURCES
|
||||||
|
src/canvas_modes.cpp
|
||||||
|
src/pch.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_PANOPAINTER_APP_SOURCES
|
||||||
|
src/app.cpp
|
||||||
|
src/app_cloud.cpp
|
||||||
|
src/app_commands.cpp
|
||||||
|
src/app_dialogs.cpp
|
||||||
|
src/app_events.cpp
|
||||||
|
src/app_layout.cpp
|
||||||
|
src/app_shaders.cpp
|
||||||
|
src/app_vr.cpp
|
||||||
|
src/platform_legacy/legacy_platform_services.cpp
|
||||||
|
src/platform_legacy/legacy_platform_services.h
|
||||||
|
src/version.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_PANOPAINTER_UI_SOURCES
|
||||||
|
src/node_about.cpp
|
||||||
|
src/node_canvas.cpp
|
||||||
|
src/node_changelog.cpp
|
||||||
|
src/node_color_quad.cpp
|
||||||
|
src/node_colorwheel.cpp
|
||||||
|
src/node_dialog_browse.cpp
|
||||||
|
src/node_dialog_cloud.cpp
|
||||||
|
src/node_dialog_export_ppbr.cpp
|
||||||
|
src/node_dialog_layer_rename.cpp
|
||||||
|
src/node_dialog_open.cpp
|
||||||
|
src/node_dialog_picker.cpp
|
||||||
|
src/node_dialog_resize.cpp
|
||||||
|
src/node_message_box.cpp
|
||||||
|
src/node_metadata.cpp
|
||||||
|
src/node_panel_animation.cpp
|
||||||
|
src/node_panel_brush.cpp
|
||||||
|
src/node_panel_color.cpp
|
||||||
|
src/node_panel_floating.cpp
|
||||||
|
src/node_panel_grid.cpp
|
||||||
|
src/node_panel_layer.cpp
|
||||||
|
src/node_panel_quick.cpp
|
||||||
|
src/node_panel_stroke.cpp
|
||||||
|
src/node_stroke_preview.cpp
|
||||||
|
src/node_tool_bucket.cpp
|
||||||
|
src/node_usermanual.cpp
|
||||||
|
src/node_viewport.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_WINDOWS_PLATFORM_SOURCES
|
||||||
|
src/main.cpp
|
||||||
|
src/platform_windows/windows_platform_services.cpp
|
||||||
|
src/platform_windows/windows_platform_services.h
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_WINDOWS_APP_SOURCES
|
||||||
|
PanoPainter.rc
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_VENDOR_SOURCES
|
||||||
|
libs/fmt/src/format.cc
|
||||||
|
libs/fmt/src/posix.cc
|
||||||
|
libs/glad/src/glad.c
|
||||||
|
libs/glad/src/glad_wgl.c
|
||||||
|
libs/hash-library/md5.cpp
|
||||||
|
libs/jpeg/jpgd.cpp
|
||||||
|
libs/jpeg/jpge.cpp
|
||||||
|
libs/nanort/nanort.cc
|
||||||
|
libs/poly2tri/poly2tri/common/shapes.cc
|
||||||
|
libs/poly2tri/poly2tri/sweep/advancing_front.cc
|
||||||
|
libs/poly2tri/poly2tri/sweep/cdt.cc
|
||||||
|
libs/poly2tri/poly2tri/sweep/sweep.cc
|
||||||
|
libs/poly2tri/poly2tri/sweep/sweep_context.cc
|
||||||
|
libs/sqlite3/sqlite3.c
|
||||||
|
libs/tinyxml2/tinyxml2.cpp
|
||||||
|
libs/wacom/WinTab/Utils.cpp
|
||||||
|
libs/yoga/yoga/event/event.cpp
|
||||||
|
libs/yoga/yoga/internal/experiments.cpp
|
||||||
|
libs/yoga/yoga/log.cpp
|
||||||
|
libs/yoga/yoga/Utils.cpp
|
||||||
|
libs/yoga/yoga/YGConfig.cpp
|
||||||
|
libs/yoga/yoga/YGEnums.cpp
|
||||||
|
libs/yoga/yoga/YGLayout.cpp
|
||||||
|
libs/yoga/yoga/YGNode.cpp
|
||||||
|
libs/yoga/yoga/YGNodePrint.cpp
|
||||||
|
libs/yoga/yoga/YGStyle.cpp
|
||||||
|
libs/yoga/yoga/YGValue.cpp
|
||||||
|
libs/yoga/yoga/Yoga.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PP_LEGACY_INCLUDE_DIRS
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/base64"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/include"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/include"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/fmt/include"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/glad/include"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/glm"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/hash-library"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/jpeg"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/include"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/include"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/nanort"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/include"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/headers"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/poly2tri/poly2tri"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/sqlite3"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/stb"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/tinyxml2"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/wacom"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/libs/yoga"
|
||||||
|
)
|
||||||
17
cmake/PanoPainterVersion.cmake
Normal file
17
cmake/PanoPainterVersion.cmake
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
function(pp_add_version_generation target config_name)
|
||||||
|
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/version.gen.h"
|
||||||
|
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pre-build.py" "${config_name}"
|
||||||
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pre-build.py"
|
||||||
|
COMMENT "Generating src/version.gen.h"
|
||||||
|
VERBATIM)
|
||||||
|
|
||||||
|
add_custom_target(pp_generate_version
|
||||||
|
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/version.gen.h")
|
||||||
|
|
||||||
|
add_dependencies(${target} pp_generate_version)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
44
cmake/PanoPainterWarnings.cmake
Normal file
44
cmake/PanoPainterWarnings.cmake
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
function(pp_configure_project_warnings target)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(${target} INTERFACE
|
||||||
|
/W4
|
||||||
|
/permissive-
|
||||||
|
/Zc:__cplusplus
|
||||||
|
/Zc:preprocessor
|
||||||
|
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
|
||||||
|
/wd4100)
|
||||||
|
if(PP_ENABLE_MSVC_ANALYZE)
|
||||||
|
target_compile_options(${target} INTERFACE /analyze)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
target_compile_options(${target} INTERFACE
|
||||||
|
-Wall
|
||||||
|
-Wextra
|
||||||
|
-Wpedantic
|
||||||
|
-Wconversion
|
||||||
|
-Wshadow
|
||||||
|
-Wnull-dereference
|
||||||
|
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
|
||||||
|
-Wno-unused-parameter)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(PP_ENABLE_ASAN)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(${target} INTERFACE /fsanitize=address)
|
||||||
|
target_link_options(${target} INTERFACE /fsanitize=address)
|
||||||
|
else()
|
||||||
|
target_compile_options(${target} INTERFACE -fsanitize=address)
|
||||||
|
target_link_options(${target} INTERFACE -fsanitize=address)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(PP_ENABLE_UBSAN AND NOT MSVC)
|
||||||
|
target_compile_options(${target} INTERFACE -fsanitize=undefined)
|
||||||
|
target_link_options(${target} INTERFACE -fsanitize=undefined)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(PP_ENABLE_TSAN AND NOT MSVC)
|
||||||
|
target_compile_options(${target} INTERFACE -fsanitize=thread)
|
||||||
|
target_link_options(${target} INTERFACE -fsanitize=thread)
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
75
cmake/ValidatePanoPainterShaders.cmake
Normal file
75
cmake/ValidatePanoPainterShaders.cmake
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
if(NOT DEFINED PP_SHADER_DIR)
|
||||||
|
message(FATAL_ERROR "PP_SHADER_DIR is required")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
file(REAL_PATH "${PP_SHADER_DIR}" pp_shader_dir)
|
||||||
|
if(NOT IS_DIRECTORY "${pp_shader_dir}")
|
||||||
|
message(FATAL_ERROR "Shader directory does not exist: ${pp_shader_dir}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
file(GLOB_RECURSE pp_shader_files
|
||||||
|
"${pp_shader_dir}/*.glsl")
|
||||||
|
|
||||||
|
if(NOT pp_shader_files)
|
||||||
|
message(FATAL_ERROR "No shader files found under: ${pp_shader_dir}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(pp_shader_errors "")
|
||||||
|
set(pp_top_level_count 0)
|
||||||
|
set(pp_include_count 0)
|
||||||
|
|
||||||
|
foreach(pp_shader_file IN LISTS pp_shader_files)
|
||||||
|
file(RELATIVE_PATH pp_shader_rel "${pp_shader_dir}" "${pp_shader_file}")
|
||||||
|
file(READ "${pp_shader_file}" pp_shader_contents)
|
||||||
|
|
||||||
|
string(REGEX MATCHALL "#[ \t]*include[ \t]+\"[^\"]+\"" pp_include_lines "${pp_shader_contents}")
|
||||||
|
foreach(pp_include_line IN LISTS pp_include_lines)
|
||||||
|
string(REGEX REPLACE ".*\"([^\"]+)\".*" "\\1" pp_include_path "${pp_include_line}")
|
||||||
|
if(pp_include_path MATCHES "^/")
|
||||||
|
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must be relative: ${pp_include_path}")
|
||||||
|
endif()
|
||||||
|
if(pp_include_path MATCHES "^[A-Za-z]:")
|
||||||
|
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must not be drive-absolute: ${pp_include_path}")
|
||||||
|
endif()
|
||||||
|
if(pp_include_path MATCHES "\\.\\.")
|
||||||
|
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must not traverse parent directories: ${pp_include_path}")
|
||||||
|
endif()
|
||||||
|
if(NOT EXISTS "${pp_shader_dir}/${pp_include_path}")
|
||||||
|
list(APPEND pp_shader_errors "${pp_shader_rel}: missing include: ${pp_include_path}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(pp_shader_rel MATCHES "^include/")
|
||||||
|
math(EXPR pp_include_count "${pp_include_count} + 1")
|
||||||
|
if(pp_shader_contents MATCHES "\\[\\[(vertex|fragment)\\]\\]")
|
||||||
|
list(APPEND pp_shader_errors "${pp_shader_rel}: include shaders must not declare stage markers")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
math(EXPR pp_top_level_count "${pp_top_level_count} + 1")
|
||||||
|
|
||||||
|
string(REGEX MATCHALL "\\[\\[vertex\\]\\]" pp_vertex_markers "${pp_shader_contents}")
|
||||||
|
string(REGEX MATCHALL "\\[\\[fragment\\]\\]" pp_fragment_markers "${pp_shader_contents}")
|
||||||
|
list(LENGTH pp_vertex_markers pp_vertex_count)
|
||||||
|
list(LENGTH pp_fragment_markers pp_fragment_count)
|
||||||
|
|
||||||
|
if(NOT pp_vertex_count EQUAL 1)
|
||||||
|
list(APPEND pp_shader_errors "${pp_shader_rel}: expected exactly one [[vertex]] marker")
|
||||||
|
endif()
|
||||||
|
if(NOT pp_fragment_count EQUAL 1)
|
||||||
|
list(APPEND pp_shader_errors "${pp_shader_rel}: expected exactly one [[fragment]] marker")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(FIND "${pp_shader_contents}" "[[vertex]]" pp_vertex_pos)
|
||||||
|
string(FIND "${pp_shader_contents}" "[[fragment]]" pp_fragment_pos)
|
||||||
|
if(pp_vertex_pos GREATER_EQUAL 0 AND pp_fragment_pos GREATER_EQUAL 0 AND NOT pp_vertex_pos LESS pp_fragment_pos)
|
||||||
|
list(APPEND pp_shader_errors "${pp_shader_rel}: [[vertex]] marker must appear before [[fragment]]")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(pp_shader_errors)
|
||||||
|
list(JOIN pp_shader_errors "\n" pp_shader_error_text)
|
||||||
|
message(FATAL_ERROR "Shader validation failed:\n${pp_shader_error_text}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "Validated ${pp_top_level_count} shader programs and ${pp_include_count} shader includes under ${pp_shader_dir}")
|
||||||
49
docs/adr/0001-modernization-boundaries.md
Normal file
49
docs/adr/0001-modernization-boundaries.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ADR 0001: Incremental Component Boundaries
|
||||||
|
|
||||||
|
Status: accepted
|
||||||
|
Date: 2026-05-31
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
PanoPainter currently has a flat `src/` layout with broad dependencies through
|
||||||
|
`pch.h`, global singletons such as `App::I` and `Canvas::I`, OpenGL types in
|
||||||
|
high-level painting/document headers, and duplicated platform source lists.
|
||||||
|
The modernization work must retain existing behavior across Windows desktop
|
||||||
|
and AppX, macOS, iOS, Android standard, Quest, Focus/Wave, Linux, and WebGL.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Modernization will proceed incrementally. OpenGL remains the production
|
||||||
|
renderer while component boundaries and tests are introduced. Vulkan, Metal,
|
||||||
|
and WebGPU-related work must stay out of the production path until OpenGL
|
||||||
|
parity tests exist.
|
||||||
|
|
||||||
|
The target dependency direction is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
pp_foundation
|
||||||
|
-> pp_assets
|
||||||
|
-> pp_paint
|
||||||
|
-> pp_document
|
||||||
|
-> pp_renderer_api
|
||||||
|
-> pp_renderer_gl
|
||||||
|
-> pp_paint_renderer
|
||||||
|
-> pp_ui_core
|
||||||
|
-> pp_panopainter_ui
|
||||||
|
-> pp_platform_*
|
||||||
|
-> panopainter_app
|
||||||
|
```
|
||||||
|
|
||||||
|
Pure component headers must not include platform SDK headers or graphics API
|
||||||
|
headers. Temporary shims are allowed only when recorded in
|
||||||
|
`docs/modernization/debt.md`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The first implementation steps are documentation, inventory, CMake skeleton,
|
||||||
|
diagnostics, and tests, not a renderer rewrite.
|
||||||
|
- Existing project files remain until the shared CMake targets are validated.
|
||||||
|
- Refactors should prefer additive compatibility layers before moving behavior.
|
||||||
|
- Every extracted component must gain its own tests before the next component
|
||||||
|
boundary is extracted.
|
||||||
|
|
||||||
584
docs/modernization/build-inventory.md
Normal file
584
docs/modernization/build-inventory.md
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
# Build And Platform Inventory
|
||||||
|
|
||||||
|
Status: live
|
||||||
|
Last updated: 2026-06-03
|
||||||
|
|
||||||
|
This inventory records the known build surfaces during the CMake migration.
|
||||||
|
Keep it updated as platform paths move to shared CMake targets.
|
||||||
|
|
||||||
|
## Existing Build Entrypoints
|
||||||
|
|
||||||
|
| Platform/Target | Current Entrypoint | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing plus open/close/save session decisions owned by `pp_app_core`, SDK-free clipboard/cursor/virtual-keyboard/display/share/picker service contracts owned by `pp_platform_api`, and injected `WindowsPlatformServices` now isolated in `src/platform_windows/windows_platform_services.*`; retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` |
|
||||||
|
| Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging |
|
||||||
|
| macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today |
|
||||||
|
| iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES today |
|
||||||
|
| Android standard | `android/android/build.gradle`, `android/android/CMakeLists.txt` | Native library target `native-lib` |
|
||||||
|
| Android Quest | `android/quest/build.gradle`, `android/quest/CMakeLists.txt` | OVR SDK imported libraries |
|
||||||
|
| Android Focus/Wave | `android/focus/build.gradle`, `android/focus/CMakeLists.txt` | Wave SDK imported libraries |
|
||||||
|
| Linux | `linux/CMakeLists.txt` | Old CMake 3.4, C++14 flag |
|
||||||
|
| WebGL/Emscripten | `webgl/CMakeLists.txt` | Old CMake 3.4, WebGL2 flags |
|
||||||
|
|
||||||
|
## Existing Version Generation
|
||||||
|
|
||||||
|
- Script: `scripts/pre-build.py`
|
||||||
|
- Output: `src/version.gen.h`
|
||||||
|
- Current behavior: derives version from git branch, latest tag, short hash,
|
||||||
|
commit count, and configuration argument.
|
||||||
|
- Migration requirement: root CMake should call this script through a custom
|
||||||
|
command and avoid unnecessary tracked-file churn where possible.
|
||||||
|
|
||||||
|
## Existing Dependency Sources
|
||||||
|
|
||||||
|
Hybrid policy: migrate reliable packages to vcpkg and retain SDK/patched
|
||||||
|
dependencies until each platform triplet is proven.
|
||||||
|
|
||||||
|
| Dependency | Current Source | Initial Policy |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| fmt | `libs/fmt` | Move to vcpkg |
|
||||||
|
| GLM | `libs/glm` | Move to vcpkg |
|
||||||
|
| tinyxml2 | `libs/tinyxml2` | Move to vcpkg |
|
||||||
|
| stb | `libs/stb` | Move to vcpkg or interface target if package friction |
|
||||||
|
| CURL | `libs/curl-win`, `libs/curl-android-ios` | Move to vcpkg where triplets work |
|
||||||
|
| SQLite | `libs/sqlite3` | Move to vcpkg |
|
||||||
|
| GLAD | `libs/glad` | Move to vcpkg or generated backend target |
|
||||||
|
| Catch2 | none yet | Add through vcpkg |
|
||||||
|
| OpenVR | `libs/openvr` | Retain initially |
|
||||||
|
| OVR Platform/Mobile | `libs/ovr_platform`, `libs/ovr_mobile` | Retain initially |
|
||||||
|
| Wave SDK | `libs/wave_sdk` | Retain initially |
|
||||||
|
| Wacom WinTab | `libs/wacom` | Retain initially |
|
||||||
|
| AppCenter Apple | `libs/appcenter-apple` | Retain initially |
|
||||||
|
| openh264/mp4v2/libyuv | `libs/openh264`, `libs/mp4v2`, `libs/libyuv` | Retain initially |
|
||||||
|
| jpeg helpers | `libs/jpeg` | Evaluate after image tests exist |
|
||||||
|
| poly2tri/nanort/base64/hash-library | `libs/*` | Evaluate after component split |
|
||||||
|
|
||||||
|
## Current Validation Commands
|
||||||
|
|
||||||
|
These commands are the current local baseline.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake --preset windows-msvc-default
|
||||||
|
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
|
||||||
|
ctest --preset desktop-fast --build-config Debug
|
||||||
|
ctest --preset fuzz --build-config Debug
|
||||||
|
ctest --preset stress --build-config Debug
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
|
||||||
|
cmake --build --preset windows-msvc-default --target panopainter_validate_shaders
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts\automation\analyze.ps1 -Preset windows-msvc-default -NoApp
|
||||||
|
$env:VCPKG_ROOT = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"
|
||||||
|
cmake --preset windows-msvc-vcpkg-headless
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets windows-msvc-vcpkg-headless
|
||||||
|
ctest --preset desktop-fast-vcpkg --build-config Debug
|
||||||
|
cmake --preset android-arm64
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug
|
||||||
|
cmake --fresh --preset windows-clangcl-asan
|
||||||
|
```
|
||||||
|
|
||||||
|
Known local toolchain state:
|
||||||
|
|
||||||
|
- CMake: 4.0.0-rc4
|
||||||
|
- Local Visual Studio generator selected by CMake: Visual Studio 17 2022
|
||||||
|
- Bundled vcpkg: `C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg`
|
||||||
|
(`vcpkg version` reports 2025-11-19)
|
||||||
|
- Android SDK: `C:\Users\omara\AppData\Local\Android\Sdk`
|
||||||
|
- Android NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865`
|
||||||
|
- clang-cl: `C:\Program Files\LLVM\bin\clang-cl.exe` reports 18.1.8, but the
|
||||||
|
selected VS 2026-preview STL expects Clang 20 or newer; see DEBT-0014 before
|
||||||
|
treating `windows-clangcl-asan` as a passing sanitizer gate.
|
||||||
|
- Android arm64 headless configure/build passes through root CMake and the
|
||||||
|
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
|
||||||
|
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_renderer_gl`,
|
||||||
|
`pp_paint_renderer`,
|
||||||
|
`pp_ui_core`, `pano_cli`, and their current headless test binaries,
|
||||||
|
including foundation binary-stream/event/logging/task queue coverage, PNG metadata and
|
||||||
|
decode, PPI header/layout/non-finite opacity and blend-mode rejection, settings document, document
|
||||||
|
snapshot/per-layer-frame/move/duration/face-pixel/PPI export coverage,
|
||||||
|
snapshot-embedded duplicate/invalid face-payload and selection-mask rejection, paint brush/final-blend/
|
||||||
|
stroke-alpha-blend/stroke spacing/stroke stress/stroke-script coverage,
|
||||||
|
renderer shader descriptor and OpenGL capability coverage, UI
|
||||||
|
color parsing, and layout XML parse coverage.
|
||||||
|
- Root CMake exposes named `fuzz` and `stress` CTest presets. `fuzz` currently
|
||||||
|
runs deterministic parser/serializer edge tests for binary streams, image
|
||||||
|
metadata, PPI, stroke scripts, and layout XML; `stress` currently runs the
|
||||||
|
stroke sampler stress coverage.
|
||||||
|
- `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by
|
||||||
|
`pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture.
|
||||||
|
- `pp_assets_image_pixels_tests` decodes PNG payloads, encodes RGBA8 pixels to
|
||||||
|
PNG, round-trips encoded pixels back through the decoder, and rejects corrupt
|
||||||
|
or malformed image payloads.
|
||||||
|
- `pano_cli import-image` accepts a PNG path, decodes RGBA8 pixels through
|
||||||
|
`pp_assets`, attaches them to a pure `pp_document` face payload, and is
|
||||||
|
covered for checked-in decodable PNG import by `pano_cli_import_image_smoke`
|
||||||
|
and metadata-valid truncated PNG rejection by
|
||||||
|
`pano_cli_import_image_rejects_truncated_png`.
|
||||||
|
- `pano_cli export-image` writes a deterministic RGBA8 PNG through `pp_assets`
|
||||||
|
and is covered by `pano_cli_export_image_roundtrip_smoke`, which imports the
|
||||||
|
generated file back through `pano_cli import-image`.
|
||||||
|
- `pano_cli inspect-project` reports validated PPI thumbnail/body byte layout,
|
||||||
|
body summary fields, layer/frame descriptors, and dirty-face PNG payload
|
||||||
|
metadata, and is covered by `pano_cli_inspect_project_layout_smoke` with a
|
||||||
|
minimal PPI fixture.
|
||||||
|
- `pp_document_ppi_import_tests` attaches decoded PPI dirty-face payloads to
|
||||||
|
`pp_document` layer/frame storage and rejects payloads outside document
|
||||||
|
layers.
|
||||||
|
- `pp_document_ppi_export_tests` exports pure `pp_document` metadata,
|
||||||
|
per-layer frame durations, and RGBA8 face payloads to PPI bytes through
|
||||||
|
`pp_assets`, then decodes and reimports them for round-trip coverage.
|
||||||
|
- `pano_cli simulate-document-export` exposes the same pure document-to-PPI
|
||||||
|
export, asset-level decode, and document reimport path through JSON
|
||||||
|
automation and is covered by `pano_cli_simulate_document_export_smoke`.
|
||||||
|
- `pano_cli save-document-project` writes that pure document export to a PPI
|
||||||
|
file and is covered by `pano_cli_save_document_project_roundtrip_smoke`,
|
||||||
|
which inspects and loads the generated file.
|
||||||
|
- `pano_cli load-project` creates a `pp_document` projection with per-layer
|
||||||
|
frame counts, durations, and decoded face-pixel payloads when present; the
|
||||||
|
metadata-only minimal fixture remains covered by
|
||||||
|
`pano_cli_load_project_metadata_smoke`.
|
||||||
|
- `pp_assets::create_ppi_project` writes generated multi-layer, multi-frame
|
||||||
|
PPI files with explicit per-layer names, opacity, blend mode, alpha lock,
|
||||||
|
visibility, per-layer frame durations, and targeted dirty-face layer/frame
|
||||||
|
payloads. `pano_cli save-project` exposes that path for automation and is
|
||||||
|
covered by `pano_cli_save_project_roundtrip_smoke` and
|
||||||
|
`pano_cli_save_project_payload_roundtrip_smoke`, which reload generated
|
||||||
|
metadata-only and targeted dirty-face-payload projects through
|
||||||
|
`pano_cli load-project`, plus
|
||||||
|
`pano_cli_save_project_rejects_non_finite_opacity`, which verifies rejected
|
||||||
|
automation floats do not create output files.
|
||||||
|
- `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and
|
||||||
|
is covered by `pano_cli_create_animation_document_smoke`.
|
||||||
|
- `pano_cli simulate-document-edits` exercises pure document layer/frame edit
|
||||||
|
operations, renderer-free face payloads, and renderer-free selection masks,
|
||||||
|
and is covered by `pano_cli_simulate_document_edits_smoke`.
|
||||||
|
- `pano_cli simulate-document-history` exercises pure document history
|
||||||
|
apply/undo/redo behavior and is covered by
|
||||||
|
`pano_cli_simulate_document_history_smoke`.
|
||||||
|
- `pano_cli simulate-image-import` decodes an embedded tiny PNG through
|
||||||
|
`pp_assets`, attaches it to `pp_document`, and is covered by
|
||||||
|
`pano_cli_simulate_image_import_smoke`.
|
||||||
|
- `pano_cli simulate-blend` exposes deterministic final RGBA and stroke-alpha
|
||||||
|
blend reference vectors through JSON automation and is covered by
|
||||||
|
`pano_cli_simulate_blend_smoke`.
|
||||||
|
- `pano_cli simulate-stroke` exposes the pure stroke sampler for scripted
|
||||||
|
automation and is covered by `pano_cli_simulate_stroke_smoke`.
|
||||||
|
- `pano_cli simulate-stroke-script` loads a text stroke script fixture and is
|
||||||
|
covered by `pano_cli_simulate_stroke_script_smoke`.
|
||||||
|
- `pano_cli apply-stroke-script` parses a text stroke script fixture, samples
|
||||||
|
every stroke through `pp_paint`, maps the samples into a bounded
|
||||||
|
`pp_document` RGBA8 face payload, writes a PPI file, and is covered by
|
||||||
|
`pano_cli_apply_stroke_script_roundtrip_smoke`, which inspects the dirty-face
|
||||||
|
box and loads the generated file back as decoded document pixel data, plus
|
||||||
|
`pano_cli_apply_stroke_script_rejects_tiny_canvas` for invalid dimension
|
||||||
|
rejection.
|
||||||
|
- `panopainter_validate_shaders` validates the current combined GLSL shader
|
||||||
|
files for one vertex stage marker, one fragment stage marker, valid marker
|
||||||
|
order, and existing relative includes.
|
||||||
|
- `pp_renderer_api` owns the canonical PanoPainter shader catalog consumed by
|
||||||
|
the legacy OpenGL app initialization path; `pp_renderer_api_tests` validates
|
||||||
|
catalog size, key entries, duplicate rejection, and bad path rejection.
|
||||||
|
- `pp_renderer_gl` owns headless OpenGL runtime capability detection consumed
|
||||||
|
by the legacy app initialization path; `pp_renderer_gl_capabilities_tests`
|
||||||
|
validates framebuffer fetch, map-buffer alignment, desktop GL float support,
|
||||||
|
GLES float/half-float extensions, WebGL exclusion behavior, and the
|
||||||
|
upload-type mapping used by legacy `Texture2D` and `RTT` creation, plus the
|
||||||
|
RGBA pixel-format mapping used by `RTT` texture allocation. It also validates
|
||||||
|
image channel-count to OpenGL texture format mapping, including
|
||||||
|
invalid channel counts rejected by `Texture2D::create(Image)`, renderer API
|
||||||
|
texture-format to OpenGL internal/pixel/component token mapping including
|
||||||
|
depth-stencil formats, RGBA8/RGBA32F
|
||||||
|
readback formats, checked byte-count math, and PBO pixel-buffer target/usage/access
|
||||||
|
mapping used by `RTT` and `PBO` readbacks, and framebuffer status naming
|
||||||
|
used by `RTT` and `Texture2D` diagnostics. It also owns the 2D texture target,
|
||||||
|
framebuffer setup, readback format, mipmap target, and update component-type
|
||||||
|
tokens used by `Texture2D`, plus cube-map binding and allocation face targets
|
||||||
|
used by `TextureCube`. It also owns and
|
||||||
|
validates framebuffer blit color mask and linear/nearest filters used by
|
||||||
|
`RTT::resize` and `RTT::copy`, renderer API blit-filter to OpenGL token
|
||||||
|
mapping, plus the default linear clamp-to-edge
|
||||||
|
render-target texture parameters, texture/renderbuffer targets, depth format,
|
||||||
|
framebuffer targets, binding queries, attachment points, and completion
|
||||||
|
status used by `RTT::create` and framebuffer bind/restore paths, plus RTT
|
||||||
|
clear color/depth masks, renderer API render-pass color/depth/stencil
|
||||||
|
clear-mask and clear-value mapping, and color-write-mask query tokens. `RTT` no longer
|
||||||
|
spells GL enum names directly. It also
|
||||||
|
validates renderer API primitive-topology to OpenGL draw-mode mapping, Shape
|
||||||
|
index-type, fill/stroke primitive-mode, buffer target, static upload usage,
|
||||||
|
and vertex attribute component/normalization mapping used by
|
||||||
|
the legacy mesh draw path. Legacy `Shape` mesh buffer/VAO creation, zero-byte
|
||||||
|
dynamic-buffer creation, dynamic vertex/index uploads, fill/stroke draw
|
||||||
|
calls, and buffer/VAO deletion now consume tested dispatch contracts here,
|
||||||
|
plus the PanoPainter cube-face to OpenGL
|
||||||
|
texture-target mapping used by `TextureCube`.
|
||||||
|
It also owns and validates sampler wrap S/T/R, min/mag filter, and desktop
|
||||||
|
border-color parameter mapping used by legacy `Sampler`, plus renderer API
|
||||||
|
sampler filter/address-mode to OpenGL token mapping including mirrored-repeat
|
||||||
|
and aggregate renderer API sampler-state to OpenGL min/mag/wrap mapping.
|
||||||
|
Legacy `TextureCube` allocation/bind/delete and legacy `Sampler`
|
||||||
|
create/configure/border/bind/unbind calls now consume those resource dispatch
|
||||||
|
contracts directly from the retained app utilities.
|
||||||
|
The PanoPainter
|
||||||
|
shader attribute binding catalog, shader stage tokens, compile/link status
|
||||||
|
queries, active-uniform count query, and matrix-uniform transpose token used
|
||||||
|
by legacy `Shader` creation also live here. Renderer API blend factor/op to
|
||||||
|
OpenGL token mapping is tested here with explicit support flags so `GL_ZERO`
|
||||||
|
stays distinguishable from unsupported enum values. Aggregate renderer API
|
||||||
|
blend-state to OpenGL enable/factor/equation/color-mask mapping, depth
|
||||||
|
compare-op to OpenGL depth-function mapping, and aggregate renderer API
|
||||||
|
depth-state to OpenGL enable/write/compare mapping are tested here too.
|
||||||
|
`Shader` no longer spells GL enum
|
||||||
|
names directly. It also owns the PanoPainter shader uniform catalog and legacy hash
|
||||||
|
mapping used by `Shader` active-uniform discovery and the uniform uniqueness
|
||||||
|
check. Legacy `Shader` program use/delete, uniform writes, and
|
||||||
|
attribute-location lookup now consume tested dispatch contracts here.
|
||||||
|
Legacy shader source compilation, shader deletion, program attach/link,
|
||||||
|
attribute rebinding, active-uniform count/enumeration, and uniform-location
|
||||||
|
discovery now consume tested dispatch contracts as well, leaving the retained
|
||||||
|
shader utility with thin GL adapter functions.
|
||||||
|
App OpenGL initialization debug severity, debug output, GL info string,
|
||||||
|
renderer API viewport/scissor rect conversion, default depth/program-point/
|
||||||
|
line-smooth state, blend factor/equation, and UI render-target RGBA8 format
|
||||||
|
tokens are cataloged and tested here too, including the legacy convert command
|
||||||
|
and resize path. App clear color-buffer masks, default framebuffer binding,
|
||||||
|
scissor state, and sampler filter/wrap tokens also consume the backend mapping.
|
||||||
|
OpenGL extension enumeration query tokens
|
||||||
|
used before runtime capability detection are cataloged here. Legacy font
|
||||||
|
atlas texture formats, text mesh buffer targets, attribute component and
|
||||||
|
normalization tokens, draw primitive/index type, upload usage, and active
|
||||||
|
texture unit selection also consume the backend mapping. Text mesh
|
||||||
|
buffer/VAO creation, deferred index/vertex uploads, and indexed draw calls
|
||||||
|
now consume tested `pp_renderer_gl` mesh dispatch contracts too. Canvas undo/redo
|
||||||
|
dirty-region texture updates and readbacks also consume the backend-owned 2D
|
||||||
|
texture target, RGBA pixel format, and unsigned-byte component mapping.
|
||||||
|
`NodeViewport` preview rendering also consumes backend-owned viewport query,
|
||||||
|
clear-color query, color-buffer clear mask, and blend-state tokens.
|
||||||
|
`NodeImageTexture` preview drawing also consumes backend-owned fallback 2D
|
||||||
|
texture bind and blend-state tokens.
|
||||||
|
`NodeImage` drawing and remote-image texture creation also consume
|
||||||
|
backend-owned mipmapped sampler filters, blend-state tokens, and RGBA8/RGBA
|
||||||
|
texture format mapping.
|
||||||
|
`NodeColorWheel` triangle-buffer setup and draw-state handling also consume
|
||||||
|
backend-owned array-buffer, static-upload, vertex-attribute, primitive-mode,
|
||||||
|
and blend-state tokens.
|
||||||
|
Simple UI text, text-input, border, scroll, and animation timeline draw
|
||||||
|
paths also consume backend-owned blend-state tokens.
|
||||||
|
Canvas layer cube/equirect generation, clear, restore, and snapshot paths
|
||||||
|
also consume backend-owned cube/2D texture targets, active texture units,
|
||||||
|
blend/clear state, and RGBA8 read/write pixel mapping.
|
||||||
|
`NodePanelGrid` heightmap preview and lightmap baking also consume
|
||||||
|
backend-owned texture readback formats, sampler filters, depth/blend state,
|
||||||
|
depth clears, viewport queries, color-mask booleans, active texture units,
|
||||||
|
and float render-target formats.
|
||||||
|
Legacy `util.cpp` OpenGL error naming and `gl_state` save/restore also
|
||||||
|
consume backend-owned error codes, state queries, framebuffer targets,
|
||||||
|
texture binding targets, and active texture units.
|
||||||
|
`NodeStrokePreview` brush preview rendering also consumes backend-owned
|
||||||
|
depth/scissor/blend state, viewport/clear-color queries, active texture
|
||||||
|
units, 2D texture targets, copy targets, and sampler filters/wraps.
|
||||||
|
Legacy `Texture2D`, `TextureManager`, `Sampler`, and `RTT` public headers no
|
||||||
|
longer expose raw OpenGL enum defaults; default texture formats, sampler
|
||||||
|
filters/wraps, and render-target formats resolve through backend-owned
|
||||||
|
overloads.
|
||||||
|
The Windows entrypoint also consumes backend-owned generic OpenGL
|
||||||
|
error-code/info-string tokens and WGL core-context/pixel-format attribute
|
||||||
|
catalogs.
|
||||||
|
The headless OpenGL command planner consumes `pp_renderer_api` recorded
|
||||||
|
commands and maps render-pass clear masks/values, viewport/scissor state,
|
||||||
|
blend/depth/sampler state, texture formats, primitive modes, draw counts, and
|
||||||
|
blit filters into GL-facing planned command data while rejecting unsupported
|
||||||
|
enum tokens before a real GL context is needed. It also plans whole recorded
|
||||||
|
command streams, preserving per-command planned data while counting render
|
||||||
|
passes, draws, shader binds, shader uniforms, texture/sampler binds, texture
|
||||||
|
uploads, mipmap generation, texture transitions, texture copies, texture
|
||||||
|
readbacks, frame captures, passthrough commands, trace commands, unsupported
|
||||||
|
commands, and render-pass ordering errors such as state changes outside a
|
||||||
|
pass, nested passes, texture I/O or blits inside a pass, and unclosed passes.
|
||||||
|
It also validates executable command dependencies, including
|
||||||
|
shader-before-uniform and shader-plus-mesh before draw within each render
|
||||||
|
pass, and rejects invalid texture/sampler bind slots in malformed recorded
|
||||||
|
streams. `pano_cli record-render` emits the OpenGL plan texture/sampler bind
|
||||||
|
counts so automation can assert backend interpretation without an OpenGL
|
||||||
|
context.
|
||||||
|
Desktop VR drawing also consumes backend-owned scissor/depth/blend state,
|
||||||
|
depth clear masks, active texture units, and fallback 2D texture unbind
|
||||||
|
targets while retaining the existing VR SDK/platform bridge shape.
|
||||||
|
Canvas mode overlay, mask, and transform paths also consume backend-owned
|
||||||
|
blend/depth state, active texture units, 2D texture copy targets, and RGBA8
|
||||||
|
readback format tokens.
|
||||||
|
`NodeCanvas` panorama UI rendering also consumes backend-owned sampler
|
||||||
|
defaults, viewport/clear-state queries, blend/depth/scissor state, color
|
||||||
|
clear masks, active texture units, fallback 2D texture unbind targets, copy
|
||||||
|
targets, and RGBA8 render-target formats.
|
||||||
|
Canvas resource setup also consumes backend-owned stroke-buffer
|
||||||
|
RGBA8/RGBA16F/RGBA32F formats, flood-fill texture upload format/type,
|
||||||
|
brush/stencil/mix sampler filters and wraps, and image channel-count texture
|
||||||
|
formats for cube-strip imports. Clamp-to-border sampler wrap is now part of
|
||||||
|
the backend capability catalog and test coverage.
|
||||||
|
Early canvas draw helpers also consume backend-owned pick readback
|
||||||
|
format/type, stroke mixer depth/scissor/blend state, saved viewport and
|
||||||
|
clear-state queries, active texture units, fallback 2D texture unbind
|
||||||
|
targets, and stroke background copy targets.
|
||||||
|
Canvas stroke commit also consumes backend-owned saved viewport/clear/blend
|
||||||
|
state, history readback format/type, active texture units, fallback 2D
|
||||||
|
texture unbind targets, and layer compositing copy targets.
|
||||||
|
Canvas layer merge rendering and explicit layer-merge compositing also consume
|
||||||
|
backend-owned depth/blend state, active texture units, fallback 2D texture
|
||||||
|
unbind targets, and merge framebuffer copy targets.
|
||||||
|
Canvas equirectangular import drawing and depth export rendering also consume
|
||||||
|
backend-owned depth/blend state and active texture units.
|
||||||
|
Canvas thumbnail generation and object-drawing helpers also consume
|
||||||
|
backend-owned saved viewport/clear/blend state, active texture units,
|
||||||
|
readback format/type, framebuffer copy targets, and renderbuffer/depth
|
||||||
|
attachment parameters; `src/canvas.cpp` no longer contains raw `GL_*`
|
||||||
|
constants.
|
||||||
|
Windows desktop OpenGL context creation now consumes a tested
|
||||||
|
`windows_wgl_core_context_3_3_config()` catalog from `pp_renderer_gl` instead
|
||||||
|
of owning active WGL context/pixel-format attribute literals in `main.cpp`.
|
||||||
|
- `windows-msvc-vcpkg-headless` validates manifest install/configure/build/test
|
||||||
|
for the current headless component matrix; see DEBT-0007 for remaining app
|
||||||
|
and platform triplet migration.
|
||||||
|
- `scripts/automation/analyze.*` runs shader validation plus a
|
||||||
|
renderer-boundary guard that reports JSON and fails if active non-backend
|
||||||
|
source code reintroduces raw `GL_*`/`WGL_*` constants outside the allowed
|
||||||
|
legacy OpenGL implementation files.
|
||||||
|
- `pp_renderer_api` exposes a headless `RecordingRenderDevice` that reports
|
||||||
|
renderer feature flags and validates backend-owned resource creation,
|
||||||
|
explicit texture usage flags, command order,
|
||||||
|
render-pass color/depth/stencil clear intent, scissor state, depth state,
|
||||||
|
blend state, texture-slot binding, sampler-state binding, texture-upload byte
|
||||||
|
counts, texture mip-level counts, texture/mesh/shader resource debug labels, mipmap-generation commands,
|
||||||
|
texture-state transitions, shader-uniform writes, explicit draw descriptor ranges, texture-copy regions,
|
||||||
|
readback/frame-capture/blit descriptor validation, readback bounds, destination buffer sizes, and
|
||||||
|
render-target blit regions, records
|
||||||
|
render-pass-clear/scissor/depth/blend/shader-uniform/texture-bind/
|
||||||
|
sampler-bind/draw/upload/mipmap-generation/texture-transition/texture-copy/readback/
|
||||||
|
frame-capture/blit commands, draw mesh inputs, explicit draw ranges, and
|
||||||
|
records trace markers and scopes without a window or GL context. Recorder
|
||||||
|
`clear()` also resets active render-pass and trace-scope state so automation
|
||||||
|
can reuse the same recording device after an interrupted frame.
|
||||||
|
- `pano_cli record-render` exposes the recording renderer through JSON
|
||||||
|
automation, including backend feature flags, render-pass/depth-clear counts, scissor/depth/blend/
|
||||||
|
shader-uniform/texture-bind/sampler-bind/upload/mipmap-generation/texture-transition/texture-copy/readback/
|
||||||
|
frame-capture/blit command and byte totals, trace marker/scope counts,
|
||||||
|
labeled descriptor counts, backend resource creation counts, plus draw
|
||||||
|
descriptor vertex/index totals. When `pp_renderer_gl` is available, it also
|
||||||
|
emits an `openGlPlan` JSON object with the planned command count, support
|
||||||
|
status, render-pass/draw/shader-bind/uniform/texture-upload/mipmap/
|
||||||
|
transition/copy/readback/capture/passthrough/trace counts, unsupported
|
||||||
|
command count, render-pass order error count, dependency error count, and
|
||||||
|
unclosed-pass state. Its
|
||||||
|
`--exercise-clear` mode verifies
|
||||||
|
interrupted-frame recorder clear/reuse behavior and reports the result in
|
||||||
|
JSON, and is covered by `pano_cli_record_render_smoke`,
|
||||||
|
`pano_cli_record_render_exercises_clear_reset`, plus
|
||||||
|
`pano_cli_record_render_rejects_oversized_target`.
|
||||||
|
- `pano_cli simulate-document-history` exposes `pp_document::DocumentHistory`
|
||||||
|
apply/undo/redo state through JSON automation and is covered by
|
||||||
|
`pano_cli_simulate_document_history_smoke`.
|
||||||
|
- `pano_cli simulate-document-edits` exposes `pp_document` layer metadata,
|
||||||
|
frame order, active-index, tiny face-payload state, and selection-mask state
|
||||||
|
through JSON automation and is covered by
|
||||||
|
`pano_cli_simulate_document_edits_smoke`.
|
||||||
|
- `pano_cli simulate-image-import` exposes embedded PNG decode and document
|
||||||
|
face-payload attachment through JSON automation and is covered by
|
||||||
|
`pano_cli_simulate_image_import_smoke`.
|
||||||
|
- `pano_cli import-image` exposes file-driven PNG decode and document
|
||||||
|
face-payload attachment through JSON automation and is covered by
|
||||||
|
`pano_cli_import_image_smoke` and
|
||||||
|
`pano_cli_import_image_rejects_truncated_png`.
|
||||||
|
- `pano_cli export-image` exposes deterministic RGBA8 PNG writing through JSON
|
||||||
|
automation and is covered by `pano_cli_export_image_roundtrip_smoke`; full
|
||||||
|
legacy canvas export remains a future CLI task.
|
||||||
|
- `pano_cli save-project` exposes generated multi-layer, multi-frame PPI
|
||||||
|
writing with layer metadata and targeted dirty-face layer/frame payloads
|
||||||
|
through JSON automation and is covered by metadata-only and
|
||||||
|
dirty-face-payload round-trip smoke tests; full legacy canvas save parity
|
||||||
|
remains tracked by DEBT-0013.
|
||||||
|
- `pp_document::export_ppi_project_document` exposes pure document-to-PPI byte
|
||||||
|
export through CTest coverage; legacy Canvas save integration remains a
|
||||||
|
future DEBT-0010/DEBT-0013 task.
|
||||||
|
- `pano_cli simulate-document-export` exposes document export round-trip state
|
||||||
|
through JSON automation for agent-driven checks.
|
||||||
|
- `pano_cli save-document-project` exposes file-writing document export
|
||||||
|
automation for inspect/load round trips.
|
||||||
|
- `pano_cli apply-stroke-script` exposes file-driven stroke-script application
|
||||||
|
to a pure document face payload and writes a PPI artifact for inspect/load
|
||||||
|
round-trip automation.
|
||||||
|
- `pano_cli classify-open` exposes the `pp_app_core` document-open route
|
||||||
|
contract as JSON and is covered for project files, ABR imports, PPBR
|
||||||
|
imports, and malformed path rejection.
|
||||||
|
- `pano_cli plan-open-route` exposes `pp_app_core` document-open action
|
||||||
|
planning as JSON and is covered for clean project open, dirty project
|
||||||
|
discard-prompt, and ABR import-prompt states.
|
||||||
|
- `pano_cli plan-new-document` exposes `pp_app_core` new-document target,
|
||||||
|
legacy resolution-index mapping, and overwrite-prompt planning as JSON and is
|
||||||
|
covered for save-now, existing-target overwrite, and invalid-resolution
|
||||||
|
states.
|
||||||
|
- `pano_cli plan-document-file` exposes `pp_app_core` document-name
|
||||||
|
validation, legacy `.ppi` path construction, and overwrite-prompt decisions
|
||||||
|
as JSON through the same combined save-file plan consumed by the live save-as
|
||||||
|
dialog; it is covered for save-now and existing-target overwrite states.
|
||||||
|
- `pano_cli plan-document-version` exposes `pp_app_core` save-version suffix
|
||||||
|
parsing, candidate path generation, collision skipping, and no-slot failure
|
||||||
|
behavior as JSON and is covered for first-version and existing-path skip
|
||||||
|
states.
|
||||||
|
- `pano_cli plan-export-target` exposes `pp_app_core` export target planning
|
||||||
|
for image file exports, layer/frame collection directories, picked-directory
|
||||||
|
stems, and MP4 suggested names as JSON and is covered for file, collection,
|
||||||
|
and suggested-name states.
|
||||||
|
- `pano_cli plan-export-start` exposes `pp_app_core` export availability
|
||||||
|
planning for license-gated, demo-blocked, and missing-canvas states as JSON;
|
||||||
|
the live image, layer, animation-frame, depth, and cube-face export dialogs
|
||||||
|
plus MP4 animation and timelapse export dialogs consume the same start
|
||||||
|
contract before reaching legacy canvas/recording export execution.
|
||||||
|
- `pano_cli plan-recording-session` exposes `pp_app_core` recording start,
|
||||||
|
stop, clear, platform cleanup, frame-count reset, and export progress-total
|
||||||
|
planning as JSON; the live recording controls consume those contracts before
|
||||||
|
reaching legacy recording threads, PBO readback, and MP4 encoder execution.
|
||||||
|
- `pano_cli plan-share-file` exposes `pp_app_core` share availability planning
|
||||||
|
as JSON for unsaved and saved document paths; the live platform share command
|
||||||
|
consumes the same contract before reaching iOS/macOS sharing bridges or
|
||||||
|
retained no-op platform branches.
|
||||||
|
- `pano_cli plan-picked-path` exposes `pp_app_core` selected-path planning as
|
||||||
|
JSON for empty and non-empty file picker results; live image/file/save/
|
||||||
|
directory picker branches consume the same contract before invoking retained
|
||||||
|
platform callbacks or legacy picker bridges.
|
||||||
|
- `pano_cli plan-display-file` exposes `pp_app_core` external file presentation
|
||||||
|
planning as JSON for empty and non-empty paths; the live display-file command
|
||||||
|
consumes the same contract before retained platform open-file bridges.
|
||||||
|
- `pano_cli plan-keyboard-visibility` exposes `pp_app_core` virtual keyboard
|
||||||
|
visibility planning as JSON for hidden and visible states; live show/hide
|
||||||
|
keyboard requests consume the same contract before retained mobile platform
|
||||||
|
keyboard bridges.
|
||||||
|
- `pano_cli plan-cursor-visibility` exposes `pp_app_core` cursor visibility
|
||||||
|
planning as JSON for hidden and visible states; live canvas cursor requests
|
||||||
|
consume the same contract before retained desktop platform cursor bridges.
|
||||||
|
- `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write` expose
|
||||||
|
`pp_app_core` clipboard text planning as JSON, including empty text writes;
|
||||||
|
live clipboard get/set requests consume the same contracts before retained
|
||||||
|
platform clipboard bridges.
|
||||||
|
- `pp_platform_api` exposes the SDK-free `PlatformServices` interface for
|
||||||
|
startup storage path preparation, clipboard text, cursor visibility,
|
||||||
|
virtual-keyboard visibility, external file display, file sharing, native
|
||||||
|
app/window close, UI-thread lifecycle hooks, render-context lifecycle hooks,
|
||||||
|
render-target binding hooks, render platform hint hooks, render-capture frame
|
||||||
|
hooks, render debug callback hooks, per-frame platform hooks, picker
|
||||||
|
callbacks, and recording cleanup, live asset/layout reload policy, diagnostic
|
||||||
|
stacktrace/crash hooks, prepared-file save/download handoff;
|
||||||
|
Windows
|
||||||
|
live app execution now uses injected
|
||||||
|
`WindowsPlatformServices` from
|
||||||
|
`src/platform_windows/windows_platform_services.*` in `pp_platform_windows`,
|
||||||
|
while non-Windows platforms still reach retained platform bridges through
|
||||||
|
the debt-tracked adapter isolated in
|
||||||
|
`src/platform_legacy/legacy_platform_services.*`.
|
||||||
|
- `pp_renderer_gl` owns the tested `OpenGlInitialState` startup depth/blend
|
||||||
|
policy and dispatch application consumed by `App::init`, tested runtime
|
||||||
|
version/vendor/renderer/GLSL string query dispatch, tested default clear
|
||||||
|
color/buffer dispatch consumed by `App::clear`, tested app UI
|
||||||
|
viewport/scissor dispatch consumed by `App::draw` and `App::vr_draw_ui`,
|
||||||
|
tested generic capability/buffer-clear dispatch consumed by VR draw state
|
||||||
|
setup, tested saved-state snapshot/restore dispatch consumed by the retained
|
||||||
|
`gl_state` utility, tested texture lifecycle/readback dispatch consumed by
|
||||||
|
the retained `Texture2D` utility, tested framebuffer blit/readback dispatch
|
||||||
|
consumed by retained `RTT` resize/copy/readback paths, tested framebuffer
|
||||||
|
bind/restore dispatch consumed by retained `RTT` render-target pass entry
|
||||||
|
and exit paths, plus renderer API to OpenGL token mapping and command-planning
|
||||||
|
contracts used by the OpenGL parity work.
|
||||||
|
- `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability,
|
||||||
|
new-document warning, publish prompt, and save-before-upload planning as JSON;
|
||||||
|
the live cloud upload command consumes the same start contract before
|
||||||
|
reaching legacy UI, canvas save, and network upload execution.
|
||||||
|
- `pano_cli plan-cloud-upload-all` exposes bulk cloud upload file-count,
|
||||||
|
progress UI availability, and progress-total clamping as JSON; the live
|
||||||
|
upload-all command consumes the same contract before reaching legacy asset
|
||||||
|
file listing, OpenGL context guard, progress UI, and network upload
|
||||||
|
execution.
|
||||||
|
- `pano_cli plan-cloud-browse` exposes `pp_app_core` cloud browse availability
|
||||||
|
and selected-file download planning as JSON; the live cloud browse command
|
||||||
|
consumes those contracts before reaching legacy dialog, network download,
|
||||||
|
canvas project-open, layer UI, and action-history execution.
|
||||||
|
- `pano_cli simulate-app-session` exposes `pp_app_core` project-open,
|
||||||
|
app-close, save, save-as, save-version, and save-before-workflow decisions
|
||||||
|
as JSON and is covered for clean, dirty, already-prompting, missing-canvas,
|
||||||
|
new-document, save-as, save-version, and dirty-save-version states.
|
||||||
|
- `pp_app_core_document_route_tests` covers the app document-open route
|
||||||
|
contract for PPI/project files, ABR imports, PPBR imports, inner-dot names,
|
||||||
|
and malformed paths before the live `App::open_document` performs UI or
|
||||||
|
legacy canvas work.
|
||||||
|
- `pp_app_core_document_export_tests` covers export file targets, collection
|
||||||
|
directory/stem targets, picked-directory stems, MP4 suggested names, and
|
||||||
|
invalid export naming inputs, plus export-start license/canvas availability
|
||||||
|
decisions.
|
||||||
|
- `pp_app_core_document_recording_tests` covers recording start/stop, clear,
|
||||||
|
platform recorded-file cleanup, frame-count reset, export progress totals,
|
||||||
|
and oversized progress-total clamping.
|
||||||
|
- `pp_app_core_document_sharing_tests` covers saved-path gating before platform
|
||||||
|
share execution.
|
||||||
|
- `pp_app_core_document_platform_io_tests` covers empty selected-path filtering
|
||||||
|
and non-empty picked-path callback planning, plus empty/non-empty display-file
|
||||||
|
planning before platform picker/display callbacks, plus virtual keyboard
|
||||||
|
show/hide planning before platform keyboard callbacks, plus cursor visibility
|
||||||
|
planning before platform cursor callbacks, plus clipboard read/write
|
||||||
|
planning before platform clipboard callbacks.
|
||||||
|
- `pp_platform_api_tests` covers service dispatch for clipboard read/write,
|
||||||
|
empty clipboard writes, cursor visibility, virtual-keyboard visibility,
|
||||||
|
external file display, file sharing, and picker callbacks without platform
|
||||||
|
SDK headers or a window.
|
||||||
|
- `pp_app_core_document_cloud_tests` covers cloud upload no-canvas,
|
||||||
|
new-document warning, clean publish prompt, and dirty save-before-upload
|
||||||
|
decisions, plus cloud browse no-canvas/show-browser and selected-download
|
||||||
|
decisions, plus bulk upload progress visibility, zero-file, and clamped
|
||||||
|
progress-total decisions.
|
||||||
|
- `pp_app_core_document_session_tests` covers clean and dirty app session,
|
||||||
|
document-open action planning, save-request, save-before-workflow,
|
||||||
|
new-document target/resolution/overwrite planning, document file target,
|
||||||
|
combined save-file overwrite planning, and save-version target decisions
|
||||||
|
without requiring a window, canvas, or message box.
|
||||||
|
- `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON`
|
||||||
|
through the vcpkg preset; default and Android validation still use the
|
||||||
|
retained vendored fallback tracked by DEBT-0012.
|
||||||
|
|
||||||
|
Known warnings after the current CMake app build:
|
||||||
|
|
||||||
|
- Legacy code/vendor warnings under `/W4`.
|
||||||
|
- `pp_legacy_vendor` intentionally owns retained third-party source builds for
|
||||||
|
now, including JPEG, SQLite, Yoga, poly2tri, GLAD, fmt, Wacom utilities, and
|
||||||
|
other patched/embedded sources. Each dependency should either move to vcpkg,
|
||||||
|
an SDK import target, or a documented permanent vendored target.
|
||||||
|
- `pp_legacy_assets_io` is an object-library containment boundary for retained
|
||||||
|
ABR, asset/file, binary stream, image, serializer, and settings code. It
|
||||||
|
should shrink as app I/O consumes `pp_assets` directly.
|
||||||
|
- `pp_legacy_paint_document` is an object-library containment boundary for
|
||||||
|
retained action, bezier, brush, canvas, canvas-layer, and event code. It
|
||||||
|
should shrink as app painting and document behavior consume `pp_paint` and
|
||||||
|
`pp_document` directly.
|
||||||
|
- `pp_legacy_engine` intentionally contains retained legacy runtime shell
|
||||||
|
sources for now, so it concentrates existing legacy tablet, video, HMD, log,
|
||||||
|
and low-level utility warnings until those paths move to cleaner component
|
||||||
|
ownership.
|
||||||
|
- `pp_legacy_renderer_gl` is an object-library containment boundary because
|
||||||
|
the retained OpenGL runtime classes still include legacy app/engine headers
|
||||||
|
and are still consumed directly by canvas and UI classes. It should become a
|
||||||
|
normal backend library once those call sites depend on `pp_renderer_api`.
|
||||||
|
- `pp_legacy_ui_core` is an object-library containment boundary because the
|
||||||
|
retained base `Node` controls still depend on legacy renderer and app
|
||||||
|
headers. It should shrink as layout parsing, colors, generic controls, and
|
||||||
|
text/image primitives move to `pp_ui_core`.
|
||||||
|
- `pp_panopainter_ui` currently surfaces existing legacy `Node`/`Serializer`
|
||||||
|
header and static-analysis warnings while it still depends on
|
||||||
|
`pp_legacy_app`; these should be reduced as the UI core/app UI boundary is
|
||||||
|
tightened instead of suppressed globally.
|
||||||
|
- `pp_app_core` is the first pure app-engine target consumed by
|
||||||
|
`panopainter_app`; it should grow only with UI-free command routing,
|
||||||
|
validation, and app service contracts that can be tested without a window.
|
||||||
|
- `panopainter_app` currently surfaces existing app orchestration, GLM,
|
||||||
|
base64, VR, and serializer warnings now that app sources live in the
|
||||||
|
composition target; warning cleanup should follow component ownership rather
|
||||||
|
than be hidden with target-wide suppressions.
|
||||||
|
- Visual Studio vcpkg manifest warning because manifest mode is not enabled.
|
||||||
|
- `LNK4099` missing `yuv.pdb` for retained libyuv binaries.
|
||||||
|
- `LNK4098` runtime library conflict from retained vendor binaries.
|
||||||
|
|
||||||
|
Platform-specific commands should be added here when verified locally.
|
||||||
84
docs/modernization/capability-map.md
Normal file
84
docs/modernization/capability-map.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# PanoPainter Capability Map
|
||||||
|
|
||||||
|
Status: live
|
||||||
|
Last updated: 2026-06-03
|
||||||
|
|
||||||
|
This map is the preservation checklist for the modernization. When a component
|
||||||
|
is extracted, update the relevant rows with the owning component, test label,
|
||||||
|
and validation command.
|
||||||
|
|
||||||
|
## Project And Documents
|
||||||
|
|
||||||
|
| Capability | Current Area | Target Owner | Required Tests |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture |
|
||||||
|
| Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, open-action plan tests, CLI route/action smoke, app open smoke |
|
||||||
|
| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/new-document resolution/overwrite/version-target decision tests, CLI session, new-document, document-file, and document-version smoke, app close/open/save/new/browse smoke |
|
||||||
|
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior |
|
||||||
|
| Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input |
|
||||||
|
| Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke |
|
||||||
|
|
||||||
|
## Image And Export
|
||||||
|
|
||||||
|
| Capability | Current Area | Target Owner | Required Tests |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
|
||||||
|
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests |
|
||||||
|
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests |
|
||||||
|
| Cube face export | `Canvas` | `pp_paint_renderer` | Six-face golden set |
|
||||||
|
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |
|
||||||
|
|
||||||
|
## Brush And Painting
|
||||||
|
|
||||||
|
| Capability | Current Area | Target Owner | Required Tests |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Brush settings serialization and stroke-panel controls | `Brush`, `Serializer`, `NodePanelStroke` | `pp_paint`, `pp_assets`, `pp_app_core`, `pp_panopainter_ui` | Round-trip and boundary values; stroke slider/toggle/blend/reset planning and invalid setting tests |
|
||||||
|
| ABR import | `ABR`, `Brush` | `pp_assets`, `pp_paint` | Sample ABR and malformed ABR |
|
||||||
|
| PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture |
|
||||||
|
| Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
|
||||||
|
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference, dual/pattern feedback planning, GPU golden |
|
||||||
|
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors, fixed-function/framebuffer-fetch/ping-pong stroke composite planning, live `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke-feedback destination-copy coverage, and GPU parity |
|
||||||
|
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
|
||||||
|
|
||||||
|
## Layers And Animation
|
||||||
|
|
||||||
|
| Capability | Current Area | Target Owner | Required Tests |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Layer add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document` | Undo/redo invariant tests |
|
||||||
|
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_paint_renderer` | CPU model and render golden |
|
||||||
|
| Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases |
|
||||||
|
| Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek |
|
||||||
|
| MP4/timelapse export | `MP4Encoder`, recording thread, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core`, app | Recording lifecycle/progress decision tests, smoke export, cancellation, suggested-name tests |
|
||||||
|
|
||||||
|
## UI And Workflow
|
||||||
|
|
||||||
|
| Capability | Current Area | Target Owner | Required Tests |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| XML layout parsing | `LayoutManager`, `Node` | `pp_ui_core` | Layout fixtures and malformed XML |
|
||||||
|
| Yoga layout | `Node` | `pp_ui_core` | Deterministic geometry fixtures |
|
||||||
|
| Generic controls | `NodeButton`, sliders, text, images | `pp_ui_core` | Event dispatch and layout tests |
|
||||||
|
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui` | UI automation scripts |
|
||||||
|
| Canvas viewport UI | `NodeCanvas` | `pp_panopainter_ui`, `pp_paint_renderer` | Input-to-command automation |
|
||||||
|
| Settings UI | `Settings`, `NodeSettings` | `pp_assets`, `pp_panopainter_ui` | Round-trip settings |
|
||||||
|
|
||||||
|
## Input, Platform, And Devices
|
||||||
|
|
||||||
|
| Capability | Current Area | Target Owner | Required Tests |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*`, app | Cursor visibility decision tests, platform service dispatch tests, synthetic event playback |
|
||||||
|
| Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback |
|
||||||
|
| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform service display/share/picker dispatch tests, platform smoke or mocked service |
|
||||||
|
| Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Keyboard visibility decision tests, platform service dispatch tests, platform smoke |
|
||||||
|
| OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests |
|
||||||
|
| Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate |
|
||||||
|
| Focus/Wave | Android Focus files | `pp_platform_android_wave` | Compile/package gate |
|
||||||
|
|
||||||
|
## Cloud, Logging, And Automation
|
||||||
|
|
||||||
|
| Capability | Current Area | Target Owner | Required Tests |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Upload/download/browse | `app_cloud`, CURL helpers | `pp_app_core`, app service, `pp_platform_*` | Upload prompt/new-doc/no-canvas decision tests, bulk-upload progress decision tests, browse/selection decision tests, mocked HTTP and timeout tests |
|
||||||
|
| License/check flows | app/cloud code | app service | Mocked response tests |
|
||||||
|
| Logging/crash reporting | `log`, BugTrap/AppCenter | `pp_foundation`, platform wrappers | Log formatting and platform compile |
|
||||||
|
| Headless automation | none yet | `tools/pano_cli` | JSON command fixtures |
|
||||||
|
| Tracing | none yet | `pp_foundation` | Span nesting/timing tests |
|
||||||
63
docs/modernization/debt.md
Normal file
63
docs/modernization/debt.md
Normal file
File diff suppressed because one or more lines are too long
1686
docs/modernization/roadmap.md
Normal file
1686
docs/modernization/roadmap.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ BOOL LoadWintab( void )
|
|||||||
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wacom_Tablet.dll" );
|
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wacom_Tablet.dll" );
|
||||||
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wintab32.dll" );
|
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wintab32.dll" );
|
||||||
LOG("calling LoadLibrary");
|
LOG("calling LoadLibrary");
|
||||||
ghWintab = LoadLibrary(L"Wintab32.dll");
|
ghWintab = LoadLibraryW(L"Wintab32.dll");
|
||||||
LOG("LoadLibrary called");
|
LOG("LoadLibrary called");
|
||||||
|
|
||||||
if ( !ghWintab )
|
if ( !ghWintab )
|
||||||
|
|||||||
65
scripts/automation/analyze.ps1
Normal file
65
scripts/automation/analyze.ps1
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Preset = "windows-msvc-default",
|
||||||
|
[switch]$NoApp
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$started = Get-Date
|
||||||
|
$argsList = @(
|
||||||
|
"--preset", $Preset,
|
||||||
|
"-DPP_ENABLE_MSVC_ANALYZE=ON",
|
||||||
|
"-DPP_ENABLE_CLANG_TIDY=ON",
|
||||||
|
"-DPP_ENABLE_CPPCHECK=ON"
|
||||||
|
)
|
||||||
|
if ($NoApp) {
|
||||||
|
$argsList += "-DPP_BUILD_APP=OFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
& cmake @argsList
|
||||||
|
$configureExitCode = $LASTEXITCODE
|
||||||
|
$shaderExitCode = 0
|
||||||
|
$rendererBoundaryExitCode = 0
|
||||||
|
|
||||||
|
if ($configureExitCode -eq 0) {
|
||||||
|
& cmake --build --preset $Preset --target panopainter_validate_shaders
|
||||||
|
$shaderExitCode = $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($configureExitCode -eq 0) {
|
||||||
|
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "check-renderer-boundary.ps1")
|
||||||
|
$rendererBoundaryExitCode = $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
$exitCode = $configureExitCode
|
||||||
|
if ($exitCode -eq 0 -and $shaderExitCode -ne 0) {
|
||||||
|
$exitCode = $shaderExitCode
|
||||||
|
}
|
||||||
|
if ($exitCode -eq 0 -and $rendererBoundaryExitCode -ne 0) {
|
||||||
|
$exitCode = $rendererBoundaryExitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
command = "analyze"
|
||||||
|
preset = $Preset
|
||||||
|
exitCode = $exitCode
|
||||||
|
checks = @(
|
||||||
|
[ordered]@{
|
||||||
|
name = "configure"
|
||||||
|
exitCode = $configureExitCode
|
||||||
|
},
|
||||||
|
[ordered]@{
|
||||||
|
name = "shader-validation"
|
||||||
|
exitCode = $shaderExitCode
|
||||||
|
},
|
||||||
|
[ordered]@{
|
||||||
|
name = "renderer-boundary"
|
||||||
|
exitCode = $rendererBoundaryExitCode
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elapsedMs = $elapsed
|
||||||
|
} | ConvertTo-Json -Compress -Depth 4
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
29
scripts/automation/analyze.sh
Normal file
29
scripts/automation/analyze.sh
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||||
|
preset="${1:-linux-clang}"
|
||||||
|
start="$(date +%s)"
|
||||||
|
cmake --preset "$preset" -DPP_ENABLE_CLANG_TIDY=ON -DPP_ENABLE_CPPCHECK=ON
|
||||||
|
configure_exit_code="$?"
|
||||||
|
shader_exit_code="0"
|
||||||
|
renderer_boundary_exit_code="0"
|
||||||
|
if [ "$configure_exit_code" -eq 0 ]; then
|
||||||
|
cmake --build --preset "$preset" --target panopainter_validate_shaders
|
||||||
|
shader_exit_code="$?"
|
||||||
|
fi
|
||||||
|
if [ "$configure_exit_code" -eq 0 ]; then
|
||||||
|
"$script_dir/check-renderer-boundary.sh"
|
||||||
|
renderer_boundary_exit_code="$?"
|
||||||
|
fi
|
||||||
|
exit_code="$configure_exit_code"
|
||||||
|
if [ "$exit_code" -eq 0 ] && [ "$shader_exit_code" -ne 0 ]; then
|
||||||
|
exit_code="$shader_exit_code"
|
||||||
|
fi
|
||||||
|
if [ "$exit_code" -eq 0 ] && [ "$renderer_boundary_exit_code" -ne 0 ]; then
|
||||||
|
exit_code="$renderer_boundary_exit_code"
|
||||||
|
fi
|
||||||
|
end="$(date +%s)"
|
||||||
|
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||||
|
printf '{"command":"analyze","preset":"%s","exitCode":%s,"checks":[{"name":"configure","exitCode":%s},{"name":"shader-validation","exitCode":%s},{"name":"renderer-boundary","exitCode":%s}],"elapsedMs":%s}\n' "$preset" "$exit_code" "$configure_exit_code" "$shader_exit_code" "$renderer_boundary_exit_code" "$elapsed_ms"
|
||||||
|
exit "$exit_code"
|
||||||
28
scripts/automation/build.ps1
Normal file
28
scripts/automation/build.ps1
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Preset = "windows-msvc-default",
|
||||||
|
[string]$Configuration = "Debug",
|
||||||
|
[string]$Target = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$started = Get-Date
|
||||||
|
$argsList = @("--build", "--preset", $Preset, "--config", $Configuration)
|
||||||
|
if ($Target.Length -gt 0) {
|
||||||
|
$argsList += @("--target", $Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
& cmake @argsList
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
command = "build"
|
||||||
|
preset = $Preset
|
||||||
|
configuration = $Configuration
|
||||||
|
target = $Target
|
||||||
|
exitCode = $exitCode
|
||||||
|
elapsedMs = $elapsed
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
17
scripts/automation/build.sh
Normal file
17
scripts/automation/build.sh
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
preset="${1:-linux-clang}"
|
||||||
|
configuration="${2:-Debug}"
|
||||||
|
target="${3:-}"
|
||||||
|
start="$(date +%s)"
|
||||||
|
if [ -n "$target" ]; then
|
||||||
|
cmake --build --preset "$preset" --config "$configuration" --target "$target"
|
||||||
|
else
|
||||||
|
cmake --build --preset "$preset" --config "$configuration"
|
||||||
|
fi
|
||||||
|
exit_code="$?"
|
||||||
|
end="$(date +%s)"
|
||||||
|
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||||
|
printf '{"command":"build","preset":"%s","configuration":"%s","target":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$exit_code" "$elapsed_ms"
|
||||||
|
exit "$exit_code"
|
||||||
62
scripts/automation/check-renderer-boundary.ps1
Normal file
62
scripts/automation/check-renderer-boundary.ps1
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Root = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Root)) {
|
||||||
|
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||||
|
}
|
||||||
|
$started = Get-Date
|
||||||
|
$pattern = '\b(?:GL|WGL)_[A-Z0-9_]+\b'
|
||||||
|
$allowed = @(
|
||||||
|
"src/renderer_gl/",
|
||||||
|
"src/rtt.cpp",
|
||||||
|
"src/texture.cpp"
|
||||||
|
)
|
||||||
|
$violations = @()
|
||||||
|
$files = Get-ChildItem -Path (Join-Path $Root "src") -Recurse -File -Include *.c,*.cc,*.cpp,*.h,*.hpp
|
||||||
|
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$relative = $file.FullName.Substring($Root.Length).TrimStart('\', '/').Replace('\', '/')
|
||||||
|
$isAllowed = $false
|
||||||
|
foreach ($prefix in $allowed) {
|
||||||
|
if ($relative.StartsWith($prefix)) {
|
||||||
|
$isAllowed = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isAllowed) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineNumber = 0
|
||||||
|
foreach ($line in Get-Content -LiteralPath $file.FullName) {
|
||||||
|
$lineNumber += 1
|
||||||
|
$trimmed = $line.TrimStart()
|
||||||
|
if ($trimmed.StartsWith("//")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$matches = [regex]::Matches($line, $pattern)
|
||||||
|
foreach ($match in $matches) {
|
||||||
|
$violations += [ordered]@{
|
||||||
|
file = $relative
|
||||||
|
line = $lineNumber
|
||||||
|
token = $match.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$exitCode = if ($violations.Count -eq 0) { 0 } else { 1 }
|
||||||
|
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
command = "check-renderer-boundary"
|
||||||
|
exitCode = $exitCode
|
||||||
|
violationCount = $violations.Count
|
||||||
|
violations = @($violations | Select-Object -First 50)
|
||||||
|
elapsedMs = $elapsed
|
||||||
|
} | ConvertTo-Json -Compress -Depth 4
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
34
scripts/automation/check-renderer-boundary.sh
Normal file
34
scripts/automation/check-renderer-boundary.sh
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||||
|
root="${1:-$(CDPATH= cd -- "$script_dir/../.." && pwd)}"
|
||||||
|
start="$(date +%s)"
|
||||||
|
tmp="${TMPDIR:-/tmp}/panopainter-renderer-boundary-$$.txt"
|
||||||
|
|
||||||
|
find "$root/src" -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) | while IFS= read -r file; do
|
||||||
|
rel="${file#"$root"/}"
|
||||||
|
case "$rel" in
|
||||||
|
src/renderer_gl/*|src/rtt.cpp|src/texture.cpp)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
awk -v rel="$rel" '
|
||||||
|
/^[[:space:]]*\/\// { next }
|
||||||
|
match($0, /\<(GL|WGL)_[A-Z0-9_]+\>/) {
|
||||||
|
print rel ":" FNR ":" substr($0, RSTART, RLENGTH)
|
||||||
|
}
|
||||||
|
' "$file"
|
||||||
|
done > "$tmp"
|
||||||
|
|
||||||
|
count="$(wc -l < "$tmp" | tr -d '[:space:]')"
|
||||||
|
end="$(date +%s)"
|
||||||
|
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||||
|
exit_code="0"
|
||||||
|
if [ "$count" -ne 0 ]; then
|
||||||
|
exit_code="1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '{"command":"check-renderer-boundary","exitCode":%s,"violationCount":%s,"elapsedMs":%s}\n' "$exit_code" "$count" "$elapsed_ms"
|
||||||
|
rm -f "$tmp"
|
||||||
|
exit "$exit_code"
|
||||||
25
scripts/automation/configure.ps1
Normal file
25
scripts/automation/configure.ps1
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Preset = "windows-msvc-default",
|
||||||
|
[switch]$NoApp
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$started = Get-Date
|
||||||
|
$argsList = @("--preset", $Preset)
|
||||||
|
if ($NoApp) {
|
||||||
|
$argsList += "-DPP_BUILD_APP=OFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
& cmake @argsList
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
command = "configure"
|
||||||
|
preset = $Preset
|
||||||
|
exitCode = $exitCode
|
||||||
|
elapsedMs = $elapsed
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
11
scripts/automation/configure.sh
Normal file
11
scripts/automation/configure.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
preset="${1:-linux-clang}"
|
||||||
|
start="$(date +%s)"
|
||||||
|
cmake --preset "$preset"
|
||||||
|
exit_code="$?"
|
||||||
|
end="$(date +%s)"
|
||||||
|
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||||
|
printf '{"command":"configure","preset":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$exit_code" "$elapsed_ms"
|
||||||
|
exit "$exit_code"
|
||||||
249
scripts/automation/package-smoke.ps1
Normal file
249
scripts/automation/package-smoke.ps1
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Preset = "windows-msvc-default",
|
||||||
|
[string]$Configuration = "Debug",
|
||||||
|
[string]$Target = "PanoPainter",
|
||||||
|
[string[]]$PackageKinds = @(
|
||||||
|
"windows-appx",
|
||||||
|
"android-standard-apk",
|
||||||
|
"android-quest-apk",
|
||||||
|
"android-focus-apk",
|
||||||
|
"apple-bundle",
|
||||||
|
"webgl"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$started = Get-Date
|
||||||
|
$root = (Get-Location).Path
|
||||||
|
|
||||||
|
function Test-CommandAvailable {
|
||||||
|
param([string]$Name)
|
||||||
|
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ArtifactCheck {
|
||||||
|
param(
|
||||||
|
[string]$Name,
|
||||||
|
[string]$Path,
|
||||||
|
[string]$PathType = "Any"
|
||||||
|
)
|
||||||
|
|
||||||
|
$exists = if ($PathType -eq "Container") {
|
||||||
|
Test-Path -LiteralPath $Path -PathType Container
|
||||||
|
} elseif ($PathType -eq "Leaf") {
|
||||||
|
Test-Path -LiteralPath $Path -PathType Leaf
|
||||||
|
} else {
|
||||||
|
Test-Path -LiteralPath $Path
|
||||||
|
}
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
name = $Name
|
||||||
|
path = $Path
|
||||||
|
pathType = $PathType
|
||||||
|
exists = $exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-Prerequisite {
|
||||||
|
param(
|
||||||
|
[string]$Name,
|
||||||
|
[bool]$Available,
|
||||||
|
[string]$Detail
|
||||||
|
)
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
name = $Name
|
||||||
|
available = $Available
|
||||||
|
detail = $Detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-PackageReadiness {
|
||||||
|
param(
|
||||||
|
[string]$Kind,
|
||||||
|
[string]$Status,
|
||||||
|
[string]$Reason,
|
||||||
|
[object[]]$Prerequisites,
|
||||||
|
[object[]]$Artifacts,
|
||||||
|
[string]$ValidationCommand
|
||||||
|
)
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
kind = $Kind
|
||||||
|
status = $Status
|
||||||
|
reason = $Reason
|
||||||
|
debt = "DEBT-0011"
|
||||||
|
validationCommand = $ValidationCommand
|
||||||
|
prerequisites = $Prerequisites
|
||||||
|
artifacts = $Artifacts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PackageReadiness {
|
||||||
|
param([string[]]$Kinds)
|
||||||
|
|
||||||
|
$readiness = @()
|
||||||
|
foreach ($kind in $Kinds) {
|
||||||
|
switch ($kind) {
|
||||||
|
"windows-appx" {
|
||||||
|
$wapproj = Join-Path $root "PanoPainterPackage/PanoPainterPackage.wapproj"
|
||||||
|
$manifest = Join-Path $root "PanoPainterPackage/Package.appxmanifest"
|
||||||
|
$appPackages = Join-Path $root "PanoPainterPackage/AppPackages"
|
||||||
|
$readiness += New-PackageReadiness `
|
||||||
|
-Kind $kind `
|
||||||
|
-Status "blocked" `
|
||||||
|
-Reason "legacy-wapproj-present-but-root-cmake-package-target-missing" `
|
||||||
|
-ValidationCommand "msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$Configuration /p:Platform=x64" `
|
||||||
|
-Prerequisites @(
|
||||||
|
(New-Prerequisite -Name "legacy-wapproj" -Available (Test-Path -LiteralPath $wapproj -PathType Leaf) -Detail $wapproj),
|
||||||
|
(New-Prerequisite -Name "appx-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||||
|
(New-Prerequisite -Name "makeappx" -Available (Test-CommandAvailable "makeappx") -Detail "Windows SDK packaging tool"),
|
||||||
|
(New-Prerequisite -Name "signtool" -Available (Test-CommandAvailable "signtool") -Detail "Windows SDK signing tool"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||||
|
) `
|
||||||
|
-Artifacts @(
|
||||||
|
(New-ArtifactCheck -Name "app-packages" -Path $appPackages -PathType "Container")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"android-standard-apk" {
|
||||||
|
$gradle = Join-Path $root "android/android/build.gradle"
|
||||||
|
$manifest = Join-Path $root "android/android/src/main/AndroidManifest.xml"
|
||||||
|
$apkDir = Join-Path $root "android/android/build/outputs/apk"
|
||||||
|
$readiness += New-PackageReadiness `
|
||||||
|
-Kind $kind `
|
||||||
|
-Status "blocked" `
|
||||||
|
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||||
|
-ValidationCommand "gradle -p android/android assembleDebug" `
|
||||||
|
-Prerequisites @(
|
||||||
|
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||||
|
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||||
|
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-arm64/android-x64"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||||
|
) `
|
||||||
|
-Artifacts @(
|
||||||
|
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"android-quest-apk" {
|
||||||
|
$gradle = Join-Path $root "android/quest/build.gradle"
|
||||||
|
$manifest = Join-Path $root "android/quest/src/main/AndroidManifest.xml"
|
||||||
|
$apkDir = Join-Path $root "android/quest/build/outputs/apk"
|
||||||
|
$readiness += New-PackageReadiness `
|
||||||
|
-Kind $kind `
|
||||||
|
-Status "blocked" `
|
||||||
|
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||||
|
-ValidationCommand "gradle -p android/quest assembleDebug" `
|
||||||
|
-Prerequisites @(
|
||||||
|
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||||
|
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||||
|
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-quest-arm64"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||||
|
) `
|
||||||
|
-Artifacts @(
|
||||||
|
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"android-focus-apk" {
|
||||||
|
$gradle = Join-Path $root "android/focus/build.gradle"
|
||||||
|
$manifest = Join-Path $root "android/focus/src/main/AndroidManifest.xml"
|
||||||
|
$apkDir = Join-Path $root "android/focus/build/outputs/apk"
|
||||||
|
$readiness += New-PackageReadiness `
|
||||||
|
-Kind $kind `
|
||||||
|
-Status "blocked" `
|
||||||
|
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||||
|
-ValidationCommand "gradle -p android/focus assembleDebug" `
|
||||||
|
-Prerequisites @(
|
||||||
|
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||||
|
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||||
|
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-focus-arm64"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||||
|
) `
|
||||||
|
-Artifacts @(
|
||||||
|
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"apple-bundle" {
|
||||||
|
$xcodeProject = Join-Path $root "PanoPainter.xcodeproj/project.pbxproj"
|
||||||
|
$bundleDir = Join-Path $root "out/package/apple"
|
||||||
|
$readiness += New-PackageReadiness `
|
||||||
|
-Kind $kind `
|
||||||
|
-Status "blocked" `
|
||||||
|
-Reason "legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target" `
|
||||||
|
-ValidationCommand "xcodebuild -project PanoPainter.xcodeproj -configuration $Configuration" `
|
||||||
|
-Prerequisites @(
|
||||||
|
(New-Prerequisite -Name "legacy-xcode-project" -Available (Test-Path -LiteralPath $xcodeProject -PathType Leaf) -Detail $xcodeProject),
|
||||||
|
(New-Prerequisite -Name "xcodebuild" -Available (Test-CommandAvailable "xcodebuild") -Detail "Apple package builder"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "macos/ios-device/ios-simulator"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||||
|
) `
|
||||||
|
-Artifacts @(
|
||||||
|
(New-ArtifactCheck -Name "apple-package-output" -Path $bundleDir -PathType "Container")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"webgl" {
|
||||||
|
$webDir = Join-Path $root "out/package/webgl"
|
||||||
|
$readiness += New-PackageReadiness `
|
||||||
|
-Kind $kind `
|
||||||
|
-Status "blocked" `
|
||||||
|
-Reason "emscripten-preset-exists-but-webgl-package-target-missing" `
|
||||||
|
-ValidationCommand "cmake --build --preset emscripten --target PanoPainter" `
|
||||||
|
-Prerequisites @(
|
||||||
|
(New-Prerequisite -Name "emcc" -Available (Test-CommandAvailable "emcc") -Detail "Emscripten compiler"),
|
||||||
|
(New-Prerequisite -Name "emcmake" -Available (Test-CommandAvailable "emcmake") -Detail "Emscripten CMake wrapper"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "emscripten"),
|
||||||
|
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||||
|
) `
|
||||||
|
-Artifacts @(
|
||||||
|
(New-ArtifactCheck -Name "webgl-output" -Path $webDir -PathType "Container")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $readiness
|
||||||
|
}
|
||||||
|
|
||||||
|
& cmake --build --preset $Preset --config $Configuration --target $Target
|
||||||
|
$buildExitCode = $LASTEXITCODE
|
||||||
|
if ($buildExitCode -ne 0) {
|
||||||
|
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||||
|
[ordered]@{
|
||||||
|
command = "package-smoke"
|
||||||
|
preset = $Preset
|
||||||
|
configuration = $Configuration
|
||||||
|
target = $Target
|
||||||
|
stage = "build"
|
||||||
|
exitCode = $buildExitCode
|
||||||
|
elapsedMs = $elapsed
|
||||||
|
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
exit $buildExitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
$binaryDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "$Target.exe"
|
||||||
|
$dataDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "data"
|
||||||
|
$checks = @(
|
||||||
|
[ordered]@{ name = "executable"; path = $binaryDir; exists = Test-Path -LiteralPath $binaryDir -PathType Leaf },
|
||||||
|
[ordered]@{ name = "data"; path = $dataDir; exists = Test-Path -LiteralPath $dataDir -PathType Container }
|
||||||
|
)
|
||||||
|
|
||||||
|
$failed = @($checks | Where-Object { -not $_.exists })
|
||||||
|
$exitCode = if ($failed.Count -eq 0) { 0 } else { 2 }
|
||||||
|
$elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
command = "package-smoke"
|
||||||
|
preset = $Preset
|
||||||
|
configuration = $Configuration
|
||||||
|
target = $Target
|
||||||
|
exitCode = $exitCode
|
||||||
|
elapsedMs = $elapsedMs
|
||||||
|
checks = $checks
|
||||||
|
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
|
||||||
|
} | ConvertTo-Json -Compress -Depth 5
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
107
scripts/automation/package-smoke.sh
Normal file
107
scripts/automation/package-smoke.sh
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
preset="${1:-linux-clang}"
|
||||||
|
configuration="${2:-Debug}"
|
||||||
|
target="${3:-PanoPainter}"
|
||||||
|
artifact="${4:-out/build/$preset/$target}"
|
||||||
|
start="$(date +%s)"
|
||||||
|
root="$(pwd)"
|
||||||
|
|
||||||
|
json_string() {
|
||||||
|
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||||
|
}
|
||||||
|
|
||||||
|
json_bool() {
|
||||||
|
if [ "$1" = "1" ]; then
|
||||||
|
printf true
|
||||||
|
else
|
||||||
|
printf false
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
command_available() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
file_available() {
|
||||||
|
[ -f "$1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
dir_available() {
|
||||||
|
[ -d "$1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
package_readiness_json() {
|
||||||
|
windows_wapproj="$root/PanoPainterPackage/PanoPainterPackage.wapproj"
|
||||||
|
windows_manifest="$root/PanoPainterPackage/Package.appxmanifest"
|
||||||
|
windows_output="$root/PanoPainterPackage/AppPackages"
|
||||||
|
android_standard_gradle="$root/android/android/build.gradle"
|
||||||
|
android_standard_manifest="$root/android/android/src/main/AndroidManifest.xml"
|
||||||
|
android_standard_output="$root/android/android/build/outputs/apk"
|
||||||
|
android_quest_gradle="$root/android/quest/build.gradle"
|
||||||
|
android_quest_manifest="$root/android/quest/src/main/AndroidManifest.xml"
|
||||||
|
android_quest_output="$root/android/quest/build/outputs/apk"
|
||||||
|
android_focus_gradle="$root/android/focus/build.gradle"
|
||||||
|
android_focus_manifest="$root/android/focus/src/main/AndroidManifest.xml"
|
||||||
|
android_focus_output="$root/android/focus/build/outputs/apk"
|
||||||
|
apple_project="$root/PanoPainter.xcodeproj/project.pbxproj"
|
||||||
|
apple_output="$root/out/package/apple"
|
||||||
|
webgl_output="$root/out/package/webgl"
|
||||||
|
|
||||||
|
file_available "$windows_wapproj"; windows_wapproj_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
file_available "$windows_manifest"; windows_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
command_available makeappx; makeappx_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
command_available signtool; signtool_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
dir_available "$windows_output"; windows_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
|
||||||
|
file_available "$android_standard_gradle"; android_standard_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
file_available "$android_standard_manifest"; android_standard_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
file_available "$android_quest_gradle"; android_quest_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
file_available "$android_quest_manifest"; android_quest_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
file_available "$android_focus_gradle"; android_focus_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
file_available "$android_focus_manifest"; android_focus_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
command_available gradle; gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
dir_available "$android_standard_output"; android_standard_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
dir_available "$android_quest_output"; android_quest_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
dir_available "$android_focus_output"; android_focus_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
|
||||||
|
file_available "$apple_project"; apple_project_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
command_available xcodebuild; xcodebuild_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
dir_available "$apple_output"; apple_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
|
||||||
|
command_available emcc; emcc_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
command_available emcmake; emcmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
dir_available "$webgl_output"; webgl_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||||
|
|
||||||
|
printf '['
|
||||||
|
printf '{"kind":"windows-appx","status":"blocked","reason":"legacy-wapproj-present-but-root-cmake-package-target-missing","debt":"DEBT-0011","validationCommand":"msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=%s /p:Platform=x64","prerequisites":[{"name":"legacy-wapproj","available":%s,"detail":%s},{"name":"appx-manifest","available":%s,"detail":%s},{"name":"makeappx","available":%s,"detail":"Windows SDK packaging tool"},{"name":"signtool","available":%s,"detail":"Windows SDK signing tool"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"app-packages","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$windows_wapproj_exists")" "$(json_string "$windows_wapproj")" "$(json_bool "$windows_manifest_exists")" "$(json_string "$windows_manifest")" "$(json_bool "$makeappx_exists")" "$(json_bool "$signtool_exists")" "$(json_string "$windows_output")" "$(json_bool "$windows_output_exists")"
|
||||||
|
printf ',{"kind":"android-standard-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/android assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-arm64/android-x64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_standard_gradle_exists")" "$(json_string "$android_standard_gradle")" "$(json_bool "$android_standard_manifest_exists")" "$(json_string "$android_standard_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_standard_output")" "$(json_bool "$android_standard_output_exists")"
|
||||||
|
printf ',{"kind":"android-quest-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/quest assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-quest-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_quest_gradle_exists")" "$(json_string "$android_quest_gradle")" "$(json_bool "$android_quest_manifest_exists")" "$(json_string "$android_quest_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_quest_output")" "$(json_bool "$android_quest_output_exists")"
|
||||||
|
printf ',{"kind":"android-focus-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/focus assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-focus-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_focus_gradle_exists")" "$(json_string "$android_focus_gradle")" "$(json_bool "$android_focus_manifest_exists")" "$(json_string "$android_focus_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_focus_output")" "$(json_bool "$android_focus_output_exists")"
|
||||||
|
printf ',{"kind":"apple-bundle","status":"blocked","reason":"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target","debt":"DEBT-0011","validationCommand":"xcodebuild -project PanoPainter.xcodeproj -configuration %s","prerequisites":[{"name":"legacy-xcode-project","available":%s,"detail":%s},{"name":"xcodebuild","available":%s,"detail":"Apple package builder"},{"name":"root-cmake-preset","available":true,"detail":"macos/ios-device/ios-simulator"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apple-package-output","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$apple_project_exists")" "$(json_string "$apple_project")" "$(json_bool "$xcodebuild_exists")" "$(json_string "$apple_output")" "$(json_bool "$apple_output_exists")"
|
||||||
|
printf ',{"kind":"webgl","status":"blocked","reason":"emscripten-preset-exists-but-webgl-package-target-missing","debt":"DEBT-0011","validationCommand":"cmake --build --preset emscripten --target PanoPainter","prerequisites":[{"name":"emcc","available":%s,"detail":"Emscripten compiler"},{"name":"emcmake","available":%s,"detail":"Emscripten CMake wrapper"},{"name":"root-cmake-preset","available":true,"detail":"emscripten"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"webgl-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$emcc_exists")" "$(json_bool "$emcmake_exists")" "$(json_string "$webgl_output")" "$(json_bool "$webgl_output_exists")"
|
||||||
|
printf ']'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmake --build --preset "$preset" --config "$configuration" --target "$target"
|
||||||
|
build_exit="$?"
|
||||||
|
if [ "$build_exit" -ne 0 ]; then
|
||||||
|
end="$(date +%s)"
|
||||||
|
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||||
|
readiness="$(package_readiness_json)"
|
||||||
|
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"build","exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$build_exit" "$elapsed_ms" "$readiness"
|
||||||
|
exit "$build_exit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -e "$artifact" ]; then
|
||||||
|
exit_code=0
|
||||||
|
else
|
||||||
|
exit_code=2
|
||||||
|
fi
|
||||||
|
|
||||||
|
end="$(date +%s)"
|
||||||
|
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||||
|
readiness="$(package_readiness_json)"
|
||||||
|
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","artifact":"%s","exists":%s,"exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$artifact" "$([ "$exit_code" -eq 0 ] && printf true || printf false)" "$exit_code" "$elapsed_ms" "$readiness"
|
||||||
|
exit "$exit_code"
|
||||||
99
scripts/automation/platform-build.ps1
Normal file
99
scripts/automation/platform-build.ps1
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string[]]$Presets = @("android-arm64"),
|
||||||
|
[string[]]$Targets = @(
|
||||||
|
"pp_foundation",
|
||||||
|
"pp_assets",
|
||||||
|
"pp_paint",
|
||||||
|
"pp_document",
|
||||||
|
"pp_renderer_api",
|
||||||
|
"pp_renderer_gl",
|
||||||
|
"pp_paint_renderer",
|
||||||
|
"pp_ui_core",
|
||||||
|
"pp_platform_api",
|
||||||
|
"pp_app_core",
|
||||||
|
"pano_cli",
|
||||||
|
"pp_foundation_binary_stream_tests",
|
||||||
|
"pp_foundation_event_tests",
|
||||||
|
"pp_foundation_log_tests",
|
||||||
|
"pp_foundation_parse_tests",
|
||||||
|
"pp_foundation_task_queue_tests",
|
||||||
|
"pp_foundation_trace_tests",
|
||||||
|
"pp_assets_image_format_tests",
|
||||||
|
"pp_assets_image_metadata_tests",
|
||||||
|
"pp_assets_image_pixels_tests",
|
||||||
|
"pp_assets_ppi_header_tests",
|
||||||
|
"pp_assets_settings_document_tests",
|
||||||
|
"pp_paint_brush_tests",
|
||||||
|
"pp_paint_blend_tests",
|
||||||
|
"pp_paint_stroke_tests",
|
||||||
|
"pp_paint_stroke_script_tests",
|
||||||
|
"pp_document_tests",
|
||||||
|
"pp_document_ppi_import_tests",
|
||||||
|
"pp_document_ppi_export_tests",
|
||||||
|
"pp_renderer_api_tests",
|
||||||
|
"pp_renderer_gl_capabilities_tests",
|
||||||
|
"pp_renderer_gl_command_plan_tests",
|
||||||
|
"pp_paint_renderer_compositor_tests",
|
||||||
|
"pp_platform_api_tests",
|
||||||
|
"pp_ui_core_color_tests",
|
||||||
|
"pp_ui_core_layout_value_tests",
|
||||||
|
"pp_ui_core_layout_xml_tests",
|
||||||
|
"pp_app_core_document_route_tests",
|
||||||
|
"pp_app_core_document_export_tests",
|
||||||
|
"pp_app_core_document_cloud_tests",
|
||||||
|
"pp_app_core_document_platform_io_tests",
|
||||||
|
"pp_app_core_document_recording_tests",
|
||||||
|
"pp_app_core_app_preferences_tests",
|
||||||
|
"pp_app_core_app_status_tests",
|
||||||
|
"pp_app_core_document_sharing_tests",
|
||||||
|
"pp_app_core_document_session_tests"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$started = Get-Date
|
||||||
|
$results = @()
|
||||||
|
$overallExitCode = 0
|
||||||
|
|
||||||
|
foreach ($preset in $Presets) {
|
||||||
|
& cmake --preset $preset
|
||||||
|
$configureExitCode = $LASTEXITCODE
|
||||||
|
if ($configureExitCode -ne 0) {
|
||||||
|
$overallExitCode = $configureExitCode
|
||||||
|
$results += [ordered]@{
|
||||||
|
preset = $preset
|
||||||
|
stage = "configure"
|
||||||
|
exitCode = $configureExitCode
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$buildArgs = @("--build", "--preset", $preset)
|
||||||
|
foreach ($target in $Targets) {
|
||||||
|
$buildArgs += @("--target", $target)
|
||||||
|
}
|
||||||
|
|
||||||
|
& cmake @buildArgs
|
||||||
|
$buildExitCode = $LASTEXITCODE
|
||||||
|
if ($buildExitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||||
|
$overallExitCode = $buildExitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
$results += [ordered]@{
|
||||||
|
preset = $preset
|
||||||
|
stage = "build"
|
||||||
|
targets = $Targets
|
||||||
|
exitCode = $buildExitCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||||
|
[ordered]@{
|
||||||
|
command = "platform-build"
|
||||||
|
exitCode = $overallExitCode
|
||||||
|
elapsedMs = $elapsed
|
||||||
|
results = $results
|
||||||
|
} | ConvertTo-Json -Compress -Depth 6
|
||||||
|
|
||||||
|
exit $overallExitCode
|
||||||
29
scripts/automation/platform-build.sh
Normal file
29
scripts/automation/platform-build.sh
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
preset="${1:-android-arm64}"
|
||||||
|
shift || true
|
||||||
|
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_document_route_tests pp_app_core_document_export_tests pp_app_core_document_cloud_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_app_preferences_tests pp_app_core_app_status_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests}"
|
||||||
|
start="$(date +%s)"
|
||||||
|
|
||||||
|
cmake --preset "$preset"
|
||||||
|
configure_exit="$?"
|
||||||
|
if [ "$configure_exit" -ne 0 ]; then
|
||||||
|
end="$(date +%s)"
|
||||||
|
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||||
|
printf '{"command":"platform-build","preset":"%s","stage":"configure","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configure_exit" "$elapsed_ms"
|
||||||
|
exit "$configure_exit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
build_args=""
|
||||||
|
for target in $targets; do
|
||||||
|
build_args="$build_args --target $target"
|
||||||
|
done
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
cmake --build --preset "$preset" $build_args
|
||||||
|
build_exit="$?"
|
||||||
|
end="$(date +%s)"
|
||||||
|
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||||
|
printf '{"command":"platform-build","preset":"%s","targets":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$targets" "$build_exit" "$elapsed_ms"
|
||||||
|
exit "$build_exit"
|
||||||
22
scripts/automation/test.ps1
Normal file
22
scripts/automation/test.ps1
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Preset = "desktop-fast",
|
||||||
|
[string]$Configuration = "Debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$started = Get-Date
|
||||||
|
|
||||||
|
& ctest --preset $Preset --build-config $Configuration
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
command = "test"
|
||||||
|
preset = $Preset
|
||||||
|
configuration = $Configuration
|
||||||
|
exitCode = $exitCode
|
||||||
|
elapsedMs = $elapsed
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
12
scripts/automation/test.sh
Normal file
12
scripts/automation/test.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
preset="${1:-desktop-fast}"
|
||||||
|
configuration="${2:-Debug}"
|
||||||
|
start="$(date +%s)"
|
||||||
|
ctest --preset "$preset" --build-config "$configuration"
|
||||||
|
exit_code="$?"
|
||||||
|
end="$(date +%s)"
|
||||||
|
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||||
|
printf '{"command":"test","preset":"%s","configuration":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$exit_code" "$elapsed_ms"
|
||||||
|
exit "$exit_code"
|
||||||
621
src/app.cpp
621
src/app.cpp
@@ -5,6 +5,13 @@
|
|||||||
#include "node_dialog_open.h"
|
#include "node_dialog_open.h"
|
||||||
#include "node_progress_bar.h"
|
#include "node_progress_bar.h"
|
||||||
#include "mp4enc.h"
|
#include "mp4enc.h"
|
||||||
|
#include "app_core/app_status.h"
|
||||||
|
#include "app_core/canvas_tool_ui.h"
|
||||||
|
#include "app_core/document_recording.h"
|
||||||
|
#include "app_core/document_route.h"
|
||||||
|
#include "app_core/document_session.h"
|
||||||
|
#include "platform_api/platform_services.h"
|
||||||
|
#include "renderer_gl/opengl_capabilities.h"
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
#include <Foundation/Foundation.h>
|
#include <Foundation/Foundation.h>
|
||||||
@@ -12,32 +19,156 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
|
||||||
#ifdef __ANDROID__
|
|
||||||
void android_async_lock();
|
|
||||||
void android_async_swap();
|
|
||||||
void android_async_unlock();
|
|
||||||
void android_attach_jni();
|
|
||||||
void android_detach_jni();
|
|
||||||
#elif _WIN32
|
|
||||||
bool async_lock_try();
|
|
||||||
void async_lock();
|
|
||||||
void win32_async_swap();
|
|
||||||
void async_unlock();
|
|
||||||
void destroy_window();
|
|
||||||
void win32_renderdoc_frame_start();
|
|
||||||
void win32_renderdoc_frame_end();
|
|
||||||
#elif __LINUX__
|
|
||||||
std::string linux_home_path();
|
|
||||||
int mkpath(const std::string& dir, mode_t mode = DEFFILEMODE);
|
|
||||||
#elif __WEB__
|
|
||||||
#include <unistd.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
App* App::I = nullptr; // singleton
|
App* App::I = nullptr; // singleton
|
||||||
|
|
||||||
std::deque<AppTask> App::render_tasklist;
|
std::deque<AppTask> App::render_tasklist;
|
||||||
std::mutex App::render_task_mutex;
|
std::mutex App::render_task_mutex;
|
||||||
std::condition_variable App::render_cv;
|
std::condition_variable App::render_cv;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] const char* query_opengl_string(std::uint32_t name) noexcept
|
||||||
|
{
|
||||||
|
return reinterpret_cast<const char*>(glGetString(static_cast<GLenum>(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void enable_opengl_state(std::uint32_t state) noexcept
|
||||||
|
{
|
||||||
|
glEnable(static_cast<GLenum>(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept
|
||||||
|
{
|
||||||
|
switch (mode) {
|
||||||
|
case kCanvasMode::Draw:
|
||||||
|
return pp::app::CanvasToolMode::draw;
|
||||||
|
case kCanvasMode::Erase:
|
||||||
|
return pp::app::CanvasToolMode::erase;
|
||||||
|
case kCanvasMode::Line:
|
||||||
|
return pp::app::CanvasToolMode::line;
|
||||||
|
case kCanvasMode::Camera:
|
||||||
|
return pp::app::CanvasToolMode::camera;
|
||||||
|
case kCanvasMode::Grid:
|
||||||
|
return pp::app::CanvasToolMode::grid;
|
||||||
|
case kCanvasMode::Copy:
|
||||||
|
return pp::app::CanvasToolMode::copy;
|
||||||
|
case kCanvasMode::Cut:
|
||||||
|
return pp::app::CanvasToolMode::cut;
|
||||||
|
case kCanvasMode::Fill:
|
||||||
|
return pp::app::CanvasToolMode::fill;
|
||||||
|
case kCanvasMode::MaskFree:
|
||||||
|
return pp::app::CanvasToolMode::mask_free;
|
||||||
|
case kCanvasMode::MaskLine:
|
||||||
|
return pp::app::CanvasToolMode::mask_line;
|
||||||
|
case kCanvasMode::FloodFill:
|
||||||
|
return pp::app::CanvasToolMode::flood_fill;
|
||||||
|
default:
|
||||||
|
return pp::app::CanvasToolMode::draw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable_opengl_state(std::uint32_t state) noexcept
|
||||||
|
{
|
||||||
|
glDisable(static_cast<GLenum>(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_opengl_blend_func(std::uint32_t source_factor, std::uint32_t destination_factor) noexcept
|
||||||
|
{
|
||||||
|
glBlendFunc(static_cast<GLenum>(source_factor), static_cast<GLenum>(destination_factor));
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_opengl_blend_equation_separate(std::uint32_t color_equation, std::uint32_t alpha_equation) noexcept
|
||||||
|
{
|
||||||
|
glBlendEquationSeparate(static_cast<GLenum>(color_equation), static_cast<GLenum>(alpha_equation));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_opengl_color(float r, float g, float b, float a) noexcept
|
||||||
|
{
|
||||||
|
glClearColor(r, g, b, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_opengl_buffers(std::uint32_t mask) noexcept
|
||||||
|
{
|
||||||
|
glClear(static_cast<GLbitfield>(mask));
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
|
||||||
|
{
|
||||||
|
glViewport(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_opengl_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
|
||||||
|
{
|
||||||
|
glScissor(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_app_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
|
||||||
|
{
|
||||||
|
const auto status = pp::renderer::gl::apply_opengl_viewport(
|
||||||
|
viewport,
|
||||||
|
pp::renderer::gl::OpenGlViewportDispatch {
|
||||||
|
.viewport = set_opengl_viewport,
|
||||||
|
});
|
||||||
|
if (!status.ok())
|
||||||
|
LOG("OpenGL viewport failed: %s", status.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_app_scissor(pp::renderer::gl::OpenGlScissorRect scissor)
|
||||||
|
{
|
||||||
|
const auto status = pp::renderer::gl::apply_opengl_scissor_rect(
|
||||||
|
scissor,
|
||||||
|
pp::renderer::gl::OpenGlScissorDispatch {
|
||||||
|
.enable = enable_opengl_state,
|
||||||
|
.disable = disable_opengl_state,
|
||||||
|
.scissor = set_opengl_scissor,
|
||||||
|
});
|
||||||
|
if (!status.ok())
|
||||||
|
LOG("OpenGL scissor failed: %s", status.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_app_scissor_test(bool enabled)
|
||||||
|
{
|
||||||
|
const auto status = pp::renderer::gl::apply_opengl_scissor_test(
|
||||||
|
enabled,
|
||||||
|
pp::renderer::gl::OpenGlScissorTestDispatch {
|
||||||
|
.enable = enable_opengl_state,
|
||||||
|
.disable = disable_opengl_state,
|
||||||
|
});
|
||||||
|
if (!status.ok())
|
||||||
|
LOG("OpenGL scissor test failed: %s", status.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLint rgba8_internal_format() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum linear_texture_filter() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::linear_texture_filter());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum nearest_texture_filter() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::nearest_texture_filter());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum repeat_texture_wrap() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::repeat_texture_wrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum framebuffer_target() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::framebuffer_target());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLuint default_framebuffer_id() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLuint>(pp::renderer::gl::default_framebuffer_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
std::thread App::render_thread;
|
std::thread App::render_thread;
|
||||||
std::thread::id App::render_thread_id;
|
std::thread::id App::render_thread_id;
|
||||||
bool App::render_running = false;
|
bool App::render_running = false;
|
||||||
@@ -57,15 +188,14 @@ void App::create()
|
|||||||
|
|
||||||
void App::open_document(std::string path)
|
void App::open_document(std::string path)
|
||||||
{
|
{
|
||||||
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
|
const auto route = pp::app::classify_document_open_path(path);
|
||||||
std::smatch m;
|
if (!route)
|
||||||
if (!std::regex_search(path, m, r))
|
|
||||||
return;
|
return;
|
||||||
std::string base = m[1].str();
|
|
||||||
std::string name = m[2].str();
|
|
||||||
std::string ext = m[3].str();
|
|
||||||
|
|
||||||
if (str_iequals(ext, "abr"))
|
const bool has_unsaved_project =
|
||||||
|
route.value().kind == pp::app::DocumentOpenKind::open_project && Canvas::I->m_unsaved;
|
||||||
|
const auto open_plan = pp::app::plan_document_open(route.value().kind, has_unsaved_project);
|
||||||
|
if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_abr)
|
||||||
{
|
{
|
||||||
auto mb = message_box("Import ABR", "Would you like to import the brushes?", true);
|
auto mb = message_box("Import ABR", "Would you like to import the brushes?", true);
|
||||||
mb->on_submit = [this, path] (Node* target) {
|
mb->on_submit = [this, path] (Node* target) {
|
||||||
@@ -73,7 +203,7 @@ void App::open_document(std::string path)
|
|||||||
target->destroy();
|
target->destroy();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (str_iequals(ext, "ppbr"))
|
else if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_ppbr)
|
||||||
{
|
{
|
||||||
auto mb = message_box("Import PPBR", "Would you like to import the brushes?", true);
|
auto mb = message_box("Import PPBR", "Would you like to import the brushes?", true);
|
||||||
mb->on_submit = [this, path] (Node* target) {
|
mb->on_submit = [this, path] (Node* target) {
|
||||||
@@ -83,6 +213,8 @@ void App::open_document(std::string path)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
const std::string base = route.value().directory;
|
||||||
|
const std::string name = route.value().name;
|
||||||
auto open_action = [this, path, base, name] {
|
auto open_action = [this, path, base, name] {
|
||||||
doc_name = name;
|
doc_name = name;
|
||||||
doc_dir = base;
|
doc_dir = base;
|
||||||
@@ -109,7 +241,7 @@ void App::open_document(std::string path)
|
|||||||
});
|
});
|
||||||
ActionManager::clear();
|
ActionManager::clear();
|
||||||
};
|
};
|
||||||
if (!Canvas::I->m_unsaved)
|
if (open_plan == pp::app::DocumentOpenPlanAction::open_project_now)
|
||||||
{
|
{
|
||||||
open_action();
|
open_action();
|
||||||
}
|
}
|
||||||
@@ -127,25 +259,19 @@ void App::open_document(std::string path)
|
|||||||
bool App::request_close()
|
bool App::request_close()
|
||||||
{
|
{
|
||||||
static bool dialog_already_opened = false;
|
static bool dialog_already_opened = false;
|
||||||
if (!Canvas::I->m_unsaved)
|
const auto close_decision = pp::app::plan_close_request(
|
||||||
|
Canvas::I->m_unsaved,
|
||||||
|
dialog_already_opened);
|
||||||
|
if (close_decision == pp::app::CloseRequestDecision::close_now)
|
||||||
return true;
|
return true;
|
||||||
if (!dialog_already_opened)
|
if (close_decision == pp::app::CloseRequestDecision::show_unsaved_prompt)
|
||||||
{
|
{
|
||||||
auto* m = layout[main_id]->add_child<NodeMessageBox>();
|
auto* m = layout[main_id]->add_child<NodeMessageBox>();
|
||||||
m->m_title->set_text("Unsaved document");
|
m->m_title->set_text("Unsaved document");
|
||||||
m->m_message->set_text("Do you want to close without saving?");
|
m->m_message->set_text("Do you want to close without saving?");
|
||||||
m->btn_ok->m_text->set_text("Yes");
|
m->btn_ok->m_text->set_text("Yes");
|
||||||
m->btn_ok->on_click = [this](Node*) {
|
m->btn_ok->on_click = [this](Node*) {
|
||||||
#ifdef _WIN32
|
request_app_close();
|
||||||
destroy_window();
|
|
||||||
//PostQuitMessage(0);
|
|
||||||
#elif __OSX__
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[osx_view close];
|
|
||||||
});
|
|
||||||
#elif __LINUX__
|
|
||||||
glfwSetWindowShouldClose(glfw_window, GLFW_TRUE);
|
|
||||||
#endif
|
|
||||||
Canvas::I->m_unsaved = false;
|
Canvas::I->m_unsaved = false;
|
||||||
};
|
};
|
||||||
m->btn_cancel->m_text->set_text("No");
|
m->btn_cancel->m_text->set_text("No");
|
||||||
@@ -160,8 +286,13 @@ bool App::request_close()
|
|||||||
|
|
||||||
void App::clear()
|
void App::clear()
|
||||||
{
|
{
|
||||||
glClearColor(.1f, .1f, .1f, 1.f);
|
const auto status = pp::renderer::gl::clear_panopainter_default_target(
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
pp::renderer::gl::OpenGlClearDispatch {
|
||||||
|
.clear_color = clear_opengl_color,
|
||||||
|
.clear = clear_opengl_buffers,
|
||||||
|
});
|
||||||
|
if (!status.ok())
|
||||||
|
LOG("OpenGL clear failed: %s", status.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::initAssets()
|
void App::initAssets()
|
||||||
@@ -170,9 +301,9 @@ void App::initAssets()
|
|||||||
FontManager::init();
|
FontManager::init();
|
||||||
|
|
||||||
LOG("initializing assets create sampler");
|
LOG("initializing assets create sampler");
|
||||||
sampler.create(GL_NEAREST);
|
sampler.create(nearest_texture_filter());
|
||||||
sampler_stencil.create(GL_LINEAR, GL_REPEAT);
|
sampler_stencil.create(linear_texture_filter(), repeat_texture_wrap());
|
||||||
sampler_linear.create(GL_LINEAR);
|
sampler_linear.create(linear_texture_filter());
|
||||||
m_face_plane.create<1>(2, 2);
|
m_face_plane.create<1>(2, 2);
|
||||||
sphere.create<8, 8>(1);
|
sphere.create<8, 8>(1);
|
||||||
LOG("initializing assets load uvs texture");
|
LOG("initializing assets load uvs texture");
|
||||||
@@ -181,78 +312,16 @@ void App::initAssets()
|
|||||||
|
|
||||||
void App::initLog()
|
void App::initLog()
|
||||||
{
|
{
|
||||||
#if defined(__IOS__)
|
const auto paths = prepare_storage_paths();
|
||||||
[ios_view init_dirs];
|
if (!paths.data_path.empty())
|
||||||
#elif defined(__OSX__)
|
data_path = paths.data_path;
|
||||||
[osx_app init_dirs];
|
if (!paths.recording_path.empty())
|
||||||
#elif defined(_WIN32)
|
rec_path = paths.recording_path;
|
||||||
//CHAR my_documents[MAX_PATH];
|
if (!paths.temporary_path.empty())
|
||||||
//HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
|
tmp_path = paths.temporary_path;
|
||||||
|
|
||||||
//HMODULE hModule = GetModuleHandle(NULL);
|
|
||||||
//CHAR path[MAX_PATH];
|
|
||||||
//GetModuleFileNameA(hModule, path, MAX_PATH);
|
|
||||||
//CHAR out_drive[MAX_PATH];
|
|
||||||
//CHAR out_path[MAX_PATH];
|
|
||||||
//_splitpath(path, out_drive, out_path, nullptr, nullptr);
|
|
||||||
//sprintf_s(path, "%s%s", out_drive, out_path);
|
|
||||||
//data_path = path;
|
|
||||||
|
|
||||||
|
|
||||||
CHAR my_documents[MAX_PATH];
|
|
||||||
HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
|
|
||||||
if (SUCCEEDED(result))
|
|
||||||
{
|
|
||||||
std::string path = std::string(my_documents) + "\\PanoPainter";
|
|
||||||
if (!PathFileExistsA(path.c_str()))
|
|
||||||
CreateDirectoryA(path.c_str(), NULL);
|
|
||||||
data_path = path;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CHAR path[MAX_PATH];
|
|
||||||
GetCurrentDirectoryA(sizeof(path), path);
|
|
||||||
data_path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
rec_path = data_path + "\\frames";
|
|
||||||
if (!PathFileExistsA(rec_path.c_str()))
|
|
||||||
CreateDirectoryA(rec_path.c_str(), NULL);
|
|
||||||
|
|
||||||
if (!PathFileExistsA((data_path + "\\brushes").c_str()))
|
|
||||||
CreateDirectoryA((data_path + "\\brushes").c_str(), NULL);
|
|
||||||
if (!PathFileExistsA((data_path + "\\brushes\\thumbs").c_str()))
|
|
||||||
CreateDirectoryA((data_path + "\\brushes\\thumbs").c_str(), NULL);
|
|
||||||
|
|
||||||
if (!PathFileExistsA((data_path + "\\patterns").c_str()))
|
|
||||||
CreateDirectoryA((data_path + "\\patterns").c_str(), NULL);
|
|
||||||
if (!PathFileExistsA((data_path + "\\patterns\\thumbs").c_str()))
|
|
||||||
CreateDirectoryA((data_path + "\\patterns\\thumbs").c_str(), NULL);
|
|
||||||
|
|
||||||
if (!PathFileExistsA((data_path + "\\settings").c_str()))
|
|
||||||
CreateDirectoryA((data_path + "\\settings").c_str(), NULL);
|
|
||||||
|
|
||||||
#elif __LINUX__
|
|
||||||
data_path = linux_home_path() + "/PanoPainter";
|
|
||||||
mkpath(data_path + "/brushes");
|
|
||||||
mkpath(data_path + "/brushes/thumbs");
|
|
||||||
mkpath(data_path + "/patterns");
|
|
||||||
mkpath(data_path + "/patterns/thumbs");
|
|
||||||
mkpath(data_path + "/settings");
|
|
||||||
mkpath(data_path + "/frames");
|
|
||||||
#elif __WEB__
|
|
||||||
data_path = "/PanoPainter";
|
|
||||||
mkdir(data_path.c_str(), 0777);
|
|
||||||
mkdir((data_path + "/brushes").c_str(), 0777);
|
|
||||||
mkdir((data_path + "/brushes/thumbs").c_str(), 0777);
|
|
||||||
mkdir((data_path + "/patterns").c_str(), 0777);
|
|
||||||
mkdir((data_path + "/patterns/thumbs").c_str(), 0777);
|
|
||||||
mkdir((data_path + "/settings").c_str(), 0777);
|
|
||||||
mkdir((data_path + "/frames").c_str(), 0777);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// TODO: save this path somewhere in the settings, don't overwrite every start
|
// TODO: save this path somewhere in the settings, don't overwrite every start
|
||||||
work_path = data_path;
|
work_path = paths.work_path.empty() ? data_path : paths.work_path;
|
||||||
|
|
||||||
//LogRemote::I.start();
|
//LogRemote::I.start();
|
||||||
LogRemote::I.file_init();
|
LogRemote::I.file_init();
|
||||||
@@ -385,56 +454,29 @@ void App::upload(std::string filename, std::string name, std::function<void(floa
|
|||||||
#endif //CURL
|
#endif //CURL
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
static CONSOLE_SCREEN_BUFFER_INFO info;
|
|
||||||
void handle_gl_callback(GLenum source, GLenum type, GLuint id,
|
|
||||||
GLenum severity, GLsizei length, const GLchar* message, const void* userParam)
|
|
||||||
{
|
|
||||||
static std::map<GLenum, int> colors = {
|
|
||||||
{ GL_DEBUG_SEVERITY_NOTIFICATION, 8 },
|
|
||||||
{ GL_DEBUG_SEVERITY_LOW, 8 },
|
|
||||||
{ GL_DEBUG_SEVERITY_MEDIUM, FOREGROUND_GREEN | FOREGROUND_INTENSITY },
|
|
||||||
{ GL_DEBUG_SEVERITY_HIGH, FOREGROUND_RED | FOREGROUND_INTENSITY },
|
|
||||||
};
|
|
||||||
if (severity == GL_DEBUG_SEVERITY_HIGH || severity == GL_DEBUG_SEVERITY_MEDIUM || severity == GL_DEBUG_SEVERITY_LOW)
|
|
||||||
{
|
|
||||||
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colors[severity]);
|
|
||||||
LOG("OPENGL: %.*s", length, message);
|
|
||||||
FlushConsoleInputBuffer(GetStdHandle(STD_OUTPUT_HANDLE));
|
|
||||||
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), info.wAttributes);
|
|
||||||
#ifdef _DEBUG
|
|
||||||
if (severity == GL_DEBUG_SEVERITY_HIGH)
|
|
||||||
__debugbreak();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void App::init()
|
void App::init()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
|
||||||
if (glDebugMessageCallback)
|
|
||||||
{
|
|
||||||
// colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c
|
|
||||||
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
|
|
||||||
|
|
||||||
render_task([]
|
|
||||||
{
|
|
||||||
glDebugMessageCallback(handle_gl_callback, nullptr);
|
|
||||||
glEnable(GL_DEBUG_OUTPUT);
|
|
||||||
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
LOG("Screen Resolution: %dx%d", (int)width, (int)height);
|
LOG("Screen Resolution: %dx%d", (int)width, (int)height);
|
||||||
|
|
||||||
render_task([]
|
render_task([]
|
||||||
{
|
{
|
||||||
LOG("GL version: %s", glGetString(GL_VERSION));
|
App::I->install_render_debug_callback();
|
||||||
LOG("GL vendor: %s", glGetString(GL_VENDOR));
|
const auto runtime_info_result = pp::renderer::gl::query_opengl_runtime_info(
|
||||||
LOG("GL renderer: %s", glGetString(GL_RENDERER));
|
pp::renderer::gl::OpenGlRuntimeInfoDispatch {
|
||||||
LOG("GLSL version: %s", glGetString(GL_SHADING_LANGUAGE_VERSION));
|
.get_string = query_opengl_string,
|
||||||
|
});
|
||||||
|
if (runtime_info_result.ok())
|
||||||
|
{
|
||||||
|
const auto& runtime_info = runtime_info_result.value();
|
||||||
|
LOG("GL version: %s", runtime_info.version);
|
||||||
|
LOG("GL vendor: %s", runtime_info.vendor);
|
||||||
|
LOG("GL renderer: %s", runtime_info.renderer);
|
||||||
|
LOG("GLSL version: %s", runtime_info.shading_language_version);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG("OpenGL runtime info failed: %s", runtime_info_result.status().message);
|
||||||
|
}
|
||||||
|
|
||||||
//GLint n_exts;
|
//GLint n_exts;
|
||||||
//glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
|
//glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
|
||||||
@@ -447,13 +489,16 @@ void App::init()
|
|||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
App::I->apply_render_platform_hints();
|
||||||
#if defined(_WIN32) || defined(__OSX__)
|
const auto startup_state_status = pp::renderer::gl::apply_panopainter_initial_state(
|
||||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
pp::renderer::gl::OpenGlStateDispatch {
|
||||||
glEnable(GL_LINE_SMOOTH);
|
.enable = enable_opengl_state,
|
||||||
#endif
|
.disable = disable_opengl_state,
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
.blend_func = set_opengl_blend_func,
|
||||||
glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX);
|
.blend_equation_separate = set_opengl_blend_equation_separate,
|
||||||
|
});
|
||||||
|
if (!startup_state_status.ok())
|
||||||
|
LOG("OpenGL startup state failed: %s", startup_state_status.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
int run_counter = Settings::value<Serializer::Integer>("run_counter") + 1;
|
int run_counter = Settings::value<Serializer::Integer>("run_counter") + 1;
|
||||||
@@ -468,7 +513,7 @@ void App::init()
|
|||||||
initLayout();
|
initLayout();
|
||||||
title_update();
|
title_update();
|
||||||
|
|
||||||
uirtt.create(width, height, -1, GL_RGBA8, true);
|
uirtt.create(width, height, -1, rgba8_internal_format(), true);
|
||||||
|
|
||||||
if (Settings::value_or<Serializer::Boolean>("auto-timelapse", true))
|
if (Settings::value_or<Serializer::Boolean>("auto-timelapse", true))
|
||||||
rec_start();
|
rec_start();
|
||||||
@@ -482,18 +527,7 @@ void App::init()
|
|||||||
|
|
||||||
void App::async_start()
|
void App::async_start()
|
||||||
{
|
{
|
||||||
#if __OSX__
|
acquire_render_context();
|
||||||
[osx_view async_lock];
|
|
||||||
#elif __IOS__
|
|
||||||
[ios_view async_lock];
|
|
||||||
#elif __ANDROID__
|
|
||||||
android_async_lock();
|
|
||||||
#elif _WIN32
|
|
||||||
async_lock();
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
#elif __LINUX__ || __WEB__
|
|
||||||
glfwMakeContextCurrent(glfw_window);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::async_redraw()
|
void App::async_redraw()
|
||||||
@@ -504,30 +538,12 @@ void App::async_redraw()
|
|||||||
|
|
||||||
void App::async_end()
|
void App::async_end()
|
||||||
{
|
{
|
||||||
#if __OSX__
|
release_render_context();
|
||||||
[osx_view async_unlock];
|
|
||||||
#elif __IOS__
|
|
||||||
[ios_view async_unlock];
|
|
||||||
#elif __ANDROID__
|
|
||||||
android_async_unlock();
|
|
||||||
#elif _WIN32
|
|
||||||
async_unlock();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::async_swap()
|
void App::async_swap()
|
||||||
{
|
{
|
||||||
#if __OSX__
|
present_render_context();
|
||||||
[osx_view async_swap];
|
|
||||||
#elif __IOS__
|
|
||||||
[ios_view async_swap];
|
|
||||||
#elif __ANDROID__
|
|
||||||
android_async_swap();
|
|
||||||
#elif _WIN32
|
|
||||||
win32_async_swap();
|
|
||||||
#elif __LINUX__ || __WEB__
|
|
||||||
glfwSwapBuffers(glfw_window);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool App::update_ui_observer(Node *n)
|
bool App::update_ui_observer(Node *n)
|
||||||
@@ -569,7 +585,13 @@ bool App::update_ui_observer(Node *n)
|
|||||||
n->m_on_screen = true;
|
n->m_on_screen = true;
|
||||||
}
|
}
|
||||||
glm::ivec4 c = glm::vec4(box.x - 1, (height / zoom - box.y - box.w - 1), box.z + 2, box.w + 2) * zoom;
|
glm::ivec4 c = glm::vec4(box.x - 1, (height / zoom - box.y - box.w - 1), box.z + 2, box.w + 2) * zoom;
|
||||||
glScissor(floorf(c.x + off_x), floorf(c.y + off_y), ceilf(c.z), ceilf(c.w));
|
apply_app_scissor(pp::renderer::gl::OpenGlScissorRect {
|
||||||
|
.enabled = 1U,
|
||||||
|
.x = static_cast<std::int32_t>(floorf(c.x + off_x)),
|
||||||
|
.y = static_cast<std::int32_t>(floorf(c.y + off_y)),
|
||||||
|
.width = static_cast<std::int32_t>(ceilf(c.z)),
|
||||||
|
.height = static_cast<std::int32_t>(ceilf(c.w)),
|
||||||
|
});
|
||||||
n->draw();
|
n->draw();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -588,32 +610,36 @@ void App::draw(float dt)
|
|||||||
{
|
{
|
||||||
uirtt.bindFramebuffer();
|
uirtt.bindFramebuffer();
|
||||||
uirtt.clear();
|
uirtt.clear();
|
||||||
glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight());
|
apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
|
||||||
glEnable(GL_SCISSOR_TEST);
|
.width = static_cast<std::int32_t>(uirtt.getWidth()),
|
||||||
|
.height = static_cast<std::int32_t>(uirtt.getHeight()),
|
||||||
|
});
|
||||||
|
apply_app_scissor_test(true);
|
||||||
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
|
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
|
||||||
layout[main_id]->m_children[i]->watch(observer);
|
layout[main_id]->m_children[i]->watch(observer);
|
||||||
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
|
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
|
||||||
layout_designer[main_id]->m_children[i]->watch(observer);
|
layout_designer[main_id]->m_children[i]->watch(observer);
|
||||||
//msgbox->watch(observer);
|
//msgbox->watch(observer);
|
||||||
glDisable(GL_SCISSOR_TEST);
|
apply_app_scissor_test(false);
|
||||||
uirtt.unbindFramebuffer();
|
uirtt.unbindFramebuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vr_only)
|
if (!vr_only)
|
||||||
{
|
{
|
||||||
#if __IOS__
|
bind_main_render_target();
|
||||||
[ios_view->glview bindDrawable];
|
apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
|
||||||
#else
|
.x = static_cast<std::int32_t>(off_x),
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
.y = static_cast<std::int32_t>(off_y),
|
||||||
#endif
|
.width = static_cast<std::int32_t>(width),
|
||||||
glViewport(off_x, off_y, (GLsizei)width, (GLsizei)height);
|
.height = static_cast<std::int32_t>(height),
|
||||||
glEnable(GL_SCISSOR_TEST);
|
});
|
||||||
|
apply_app_scissor_test(true);
|
||||||
for (int i = 0; i < layout[main_id]->m_children.size(); i++)
|
for (int i = 0; i < layout[main_id]->m_children.size(); i++)
|
||||||
layout[main_id]->m_children[i]->watch(observer);
|
layout[main_id]->m_children[i]->watch(observer);
|
||||||
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
|
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
|
||||||
layout_designer[main_id]->m_children[i]->watch(observer);
|
layout_designer[main_id]->m_children[i]->watch(observer);
|
||||||
//msgbox->watch(observer);
|
//msgbox->watch(observer);
|
||||||
glDisable(GL_SCISSOR_TEST);
|
apply_app_scissor_test(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
redraw = false;
|
redraw = false;
|
||||||
@@ -636,25 +662,26 @@ void App::update(float dt)
|
|||||||
main->update(width, height, zoom);
|
main->update(width, height, zoom);
|
||||||
|
|
||||||
{
|
{
|
||||||
static glm::vec4 color_button_normal{ .1, .1, .1, 1 };
|
|
||||||
static glm::vec4 color_button_hlight{ 1, .0, .0, 1 };
|
|
||||||
|
|
||||||
auto mode = Canvas::I->m_current_mode;
|
auto mode = Canvas::I->m_current_mode;
|
||||||
|
|
||||||
CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0];
|
CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0];
|
||||||
layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_active(mode == kCanvasMode::Draw && pm->m_picking);
|
const auto toolbar = pp::app::plan_canvas_tool_button_state(
|
||||||
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(canvas->m_canvas->m_touch_lock);
|
canvas_tool_mode_from_canvas_mode(mode),
|
||||||
|
pm && pm->m_picking,
|
||||||
|
canvas->m_canvas->m_touch_lock);
|
||||||
|
layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_active(toolbar.pick_active);
|
||||||
|
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(toolbar.touch_lock_active);
|
||||||
|
|
||||||
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(mode == kCanvasMode::Draw);
|
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(toolbar.pen_active);
|
||||||
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(mode == kCanvasMode::Erase);
|
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(toolbar.erase_active);
|
||||||
layout[main_id]->find<NodeButton>("btn-cam")->set_active(mode == kCanvasMode::Camera);
|
layout[main_id]->find<NodeButton>("btn-cam")->set_active(toolbar.camera_active);
|
||||||
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(mode == kCanvasMode::Line);
|
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(toolbar.line_active);
|
||||||
layout[main_id]->find<NodeButton>("btn-grid")->set_active(mode == kCanvasMode::Grid);
|
layout[main_id]->find<NodeButton>("btn-grid")->set_active(toolbar.grid_active);
|
||||||
layout[main_id]->find<NodeButton>("btn-copy")->set_active(mode == kCanvasMode::Copy);
|
layout[main_id]->find<NodeButton>("btn-copy")->set_active(toolbar.copy_active);
|
||||||
layout[main_id]->find<NodeButton>("btn-cut")->set_active(mode == kCanvasMode::Cut);
|
layout[main_id]->find<NodeButton>("btn-cut")->set_active(toolbar.cut_active);
|
||||||
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(mode == kCanvasMode::MaskFree);
|
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(toolbar.mask_free_active);
|
||||||
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(mode == kCanvasMode::MaskLine);
|
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(toolbar.mask_line_active);
|
||||||
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(mode == kCanvasMode::FloodFill);
|
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(toolbar.flood_fill_active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,9 +715,8 @@ void App::update_memory_usage(size_t bytes)
|
|||||||
{
|
{
|
||||||
if (auto txt = layout[main_id]->find<NodeText>("txt-memory"))
|
if (auto txt = layout[main_id]->find<NodeText>("txt-memory"))
|
||||||
{
|
{
|
||||||
static char buffer[128];
|
const auto label = pp::app::make_history_memory_label(bytes);
|
||||||
sprintf(buffer, "History memory: %.2f Mb", bytes / 1024.f / 1024.f);
|
txt->set_text(label.c_str());
|
||||||
txt->set_text(buffer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,91 +724,102 @@ void App::update_rec_frames()
|
|||||||
{
|
{
|
||||||
if (auto txt = layout[main_id]->find<NodeText>("txt-rec"))
|
if (auto txt = layout[main_id]->find<NodeText>("txt-rec"))
|
||||||
{
|
{
|
||||||
if (rec_running && Canvas::I->m_encoder)
|
const auto label = pp::app::make_recording_frame_label(
|
||||||
{
|
rec_running,
|
||||||
static char buffer[128];
|
Canvas::I->m_encoder != nullptr,
|
||||||
sprintf(buffer, "Recorded %d frames", Canvas::I->m_encoder->frames_count());
|
Canvas::I->m_encoder ? Canvas::I->m_encoder->frames_count() : 0);
|
||||||
txt->set_text(buffer);
|
txt->set_text(label.text.c_str());
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
txt->set_text("");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int App::res_from_index(int i)
|
int App::res_from_index(int i)
|
||||||
{
|
{
|
||||||
return res_map[i];
|
const auto resolution = pp::app::display_resolution_from_index(i);
|
||||||
|
return resolution ? resolution.value() : pp::app::document_resolution_values.front();
|
||||||
}
|
}
|
||||||
|
|
||||||
int App::res_to_index(int res)
|
int App::res_to_index(int res)
|
||||||
{
|
{
|
||||||
return (int)std::distance(res_map.begin(), std::find(res_map.begin(), res_map.end(), res));
|
const auto index = pp::app::document_resolution_to_index(res);
|
||||||
|
return index ? static_cast<int>(index.value()) : static_cast<int>(pp::app::document_resolution_values.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string App::res_to_string(int res)
|
std::string App::res_to_string(int res)
|
||||||
{
|
{
|
||||||
return res_map_str[res_to_index(res)];
|
const auto label = pp::app::document_resolution_label(res);
|
||||||
|
return label ? std::string(label.value()) : std::string("unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::renderdoc_frame_start()
|
void App::renderdoc_frame_start()
|
||||||
{
|
{
|
||||||
#if __WIN__
|
begin_render_capture_frame();
|
||||||
win32_renderdoc_frame_start();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::renderdoc_frame_end()
|
void App::renderdoc_frame_end()
|
||||||
{
|
{
|
||||||
#if __WIN__
|
end_render_capture_frame();
|
||||||
win32_renderdoc_frame_end();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::rec_clear()
|
void App::rec_clear()
|
||||||
{
|
{
|
||||||
rec_stop();
|
const auto plan = pp::app::plan_recording_clear(
|
||||||
#if defined(__IOS__) || defined(__OSX__)
|
rec_running,
|
||||||
delete_all_files_in_path(rec_path);
|
platform_deletes_recorded_files_on_clear()
|
||||||
#endif
|
);
|
||||||
rec_count = 0;
|
if (plan.stop_running_recording)
|
||||||
|
rec_stop();
|
||||||
|
if (plan.delete_recorded_files)
|
||||||
|
clear_platform_recorded_files(rec_path);
|
||||||
|
rec_count = plan.frame_count_after_clear;
|
||||||
update_rec_frames();
|
update_rec_frames();
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::rec_start()
|
void App::rec_start()
|
||||||
{
|
{
|
||||||
if (!rec_running)
|
const auto plan = pp::app::plan_recording_start(rec_running);
|
||||||
|
switch (plan)
|
||||||
{
|
{
|
||||||
update_rec_frames();
|
case pp::app::RecordingStartAction::start_thread:
|
||||||
rec_thread = std::thread(&App::rec_loop, this);
|
break;
|
||||||
|
case pp::app::RecordingStartAction::no_op_already_running:
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_rec_frames();
|
||||||
|
rec_thread = std::thread(&App::rec_loop, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::rec_stop()
|
void App::rec_stop()
|
||||||
{
|
{
|
||||||
if (rec_running)
|
const auto plan = pp::app::plan_recording_stop(rec_running);
|
||||||
|
switch (plan)
|
||||||
{
|
{
|
||||||
rec_running = false;
|
case pp::app::RecordingStopAction::stop_thread:
|
||||||
rec_cv.notify_all();
|
break;
|
||||||
if (rec_thread.joinable())
|
case pp::app::RecordingStopAction::no_op_not_running:
|
||||||
rec_thread.join();
|
return;
|
||||||
update_rec_frames();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rec_running = false;
|
||||||
|
rec_cv.notify_all();
|
||||||
|
if (rec_thread.joinable())
|
||||||
|
rec_thread.join();
|
||||||
|
update_rec_frames();
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::rec_export(std::string path)
|
void App::rec_export(std::string path)
|
||||||
{
|
{
|
||||||
int progress = 0;
|
const auto plan = pp::app::plan_recording_export(static_cast<std::size_t>(rec_count));
|
||||||
int tot = rec_count;
|
|
||||||
auto pb = layout[main_id]->add_child<NodeProgressBar>();
|
auto pb = layout[main_id]->add_child<NodeProgressBar>();
|
||||||
pb->m_progress->SetWidthP(0);
|
pb->m_progress->SetWidthP(0);
|
||||||
pb->m_title->set_text("Exporting MP4 movie");
|
pb->m_title->set_text("Exporting MP4 movie");
|
||||||
|
pb->m_total = plan.progress_total;
|
||||||
|
pb->m_count = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#if defined(__IOS__) || defined(__OSX__)
|
#if defined(__IOS__) || defined(__OSX__)
|
||||||
export_mp4(rec_path, width, height, rec_count, ^(float) {
|
export_mp4(rec_path, width, height, rec_count, ^(float) {
|
||||||
pb->m_progress->SetWidthP((float)progress / tot * 100.f);
|
pb->increment();
|
||||||
});
|
});
|
||||||
#endif
|
#endif
|
||||||
*/
|
*/
|
||||||
@@ -915,7 +952,7 @@ void App::ui_thread_tick()
|
|||||||
update(0);
|
update(0);
|
||||||
render_task([this]
|
render_task([this]
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
bind_default_render_target();
|
||||||
clear();
|
clear();
|
||||||
draw(0);
|
draw(0);
|
||||||
async_swap();
|
async_swap();
|
||||||
@@ -931,9 +968,7 @@ void App::ui_thread_main()
|
|||||||
ui_thread_id = std::this_thread::get_id();
|
ui_thread_id = std::this_thread::get_id();
|
||||||
ui_running = true;
|
ui_running = true;
|
||||||
|
|
||||||
#if __ANDROID__
|
attach_ui_thread();
|
||||||
android_attach_jni();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
LOG("ui thread init()");
|
LOG("ui thread init()");
|
||||||
init();
|
init();
|
||||||
@@ -971,10 +1006,7 @@ void App::ui_thread_main()
|
|||||||
float dt = std::chrono::duration<float>(t_now - t_start).count();
|
float dt = std::chrono::duration<float>(t_now - t_start).count();
|
||||||
t_start = t_now;
|
t_start = t_now;
|
||||||
|
|
||||||
#ifdef _WIN32
|
update_platform_frame(dt);
|
||||||
extern void win32_update_stylus(float dt);
|
|
||||||
win32_update_stylus(dt);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// increment timers
|
// increment timers
|
||||||
t_frame += dt;
|
t_frame += dt;
|
||||||
@@ -982,33 +1014,28 @@ void App::ui_thread_main()
|
|||||||
|
|
||||||
if (t_fps_counter > 1.f)
|
if (t_fps_counter > 1.f)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
report_rendered_frames(rendered_frames);
|
||||||
extern void win32_update_fps(int frames);
|
|
||||||
win32_update_fps(rendered_frames);
|
|
||||||
#elif __LINUX__
|
|
||||||
extern void linux_update_fps(int frames);
|
|
||||||
linux_update_fps(rendered_frames);
|
|
||||||
#endif
|
|
||||||
t_fps_counter = 0;
|
t_fps_counter = 0;
|
||||||
rendered_frames = 0;
|
rendered_frames = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if /*_DEBUG &&*/ (_WIN32 || __OSX__)
|
if (platform_enables_live_asset_reloading())
|
||||||
t_reloader += dt;
|
|
||||||
if (t_reloader > 1.0)
|
|
||||||
{
|
{
|
||||||
t_reloader = 0;
|
t_reloader += dt;
|
||||||
if (ShaderManager::reload())
|
if (t_reloader > 1.0)
|
||||||
{
|
{
|
||||||
stroke->update_controls();
|
t_reloader = 0;
|
||||||
redraw = true;
|
if (ShaderManager::reload())
|
||||||
|
{
|
||||||
|
stroke->update_controls();
|
||||||
|
redraw = true;
|
||||||
|
}
|
||||||
|
if (layout.reload())
|
||||||
|
redraw = true;
|
||||||
|
if (layout_designer.reload())
|
||||||
|
redraw = true;
|
||||||
}
|
}
|
||||||
if (layout.reload())
|
|
||||||
redraw = true;
|
|
||||||
if (layout_designer.reload())
|
|
||||||
redraw = true;
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
tick(dt);
|
tick(dt);
|
||||||
|
|
||||||
@@ -1017,7 +1044,7 @@ void App::ui_thread_main()
|
|||||||
update(t_frame);
|
update(t_frame);
|
||||||
render_task([this, t_frame]
|
render_task([this, t_frame]
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
bind_default_render_target();
|
||||||
clear();
|
clear();
|
||||||
draw(t_frame);
|
draw(t_frame);
|
||||||
async_swap();
|
async_swap();
|
||||||
@@ -1026,9 +1053,7 @@ void App::ui_thread_main()
|
|||||||
rendered_frames++;
|
rendered_frames++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if __ANDROID__
|
detach_ui_thread();
|
||||||
android_detach_jni();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::render_thread_start()
|
void App::render_thread_start()
|
||||||
|
|||||||
33
src/app.h
33
src/app.h
@@ -24,6 +24,12 @@
|
|||||||
#include "node_input_box.h"
|
#include "node_input_box.h"
|
||||||
#include "node_panel_animation.h"
|
#include "node_panel_animation.h"
|
||||||
#include "layout.h"
|
#include "layout.h"
|
||||||
|
#include "app_core/document_session.h"
|
||||||
|
|
||||||
|
namespace pp::platform {
|
||||||
|
class PlatformServices;
|
||||||
|
struct PlatformStoragePaths;
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(__OBJC__) && defined(__IOS__)
|
#if defined(__OBJC__) && defined(__IOS__)
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
@@ -155,6 +161,7 @@ public:
|
|||||||
float display_density = 1.f;
|
float display_density = 1.f;
|
||||||
float zoom = 1.f;
|
float zoom = 1.f;
|
||||||
int idle_ms = 100;
|
int idle_ms = 100;
|
||||||
|
pp::platform::PlatformServices* platform_services_ = nullptr;
|
||||||
|
|
||||||
#if defined(__IOS__) && defined(__OBJC__)
|
#if defined(__IOS__) && defined(__OBJC__)
|
||||||
GameViewController* ios_view;
|
GameViewController* ios_view;
|
||||||
@@ -183,6 +190,30 @@ public:
|
|||||||
void pick_dir(std::function<void(std::string path)> callback);
|
void pick_dir(std::function<void(std::string path)> callback);
|
||||||
void display_file(std::string path);
|
void display_file(std::string path);
|
||||||
void share_file(std::string path);
|
void share_file(std::string path);
|
||||||
|
void request_app_close();
|
||||||
|
void attach_ui_thread();
|
||||||
|
void detach_ui_thread();
|
||||||
|
void acquire_render_context();
|
||||||
|
void release_render_context();
|
||||||
|
void present_render_context();
|
||||||
|
void bind_default_render_target();
|
||||||
|
void bind_main_render_target();
|
||||||
|
void apply_render_platform_hints();
|
||||||
|
void install_render_debug_callback();
|
||||||
|
void begin_render_capture_frame();
|
||||||
|
void end_render_capture_frame();
|
||||||
|
[[nodiscard]] bool platform_deletes_recorded_files_on_clear();
|
||||||
|
void clear_platform_recorded_files(std::string path);
|
||||||
|
[[nodiscard]] bool platform_enables_live_asset_reloading();
|
||||||
|
void update_platform_frame(float delta_time_seconds);
|
||||||
|
void report_rendered_frames(int frames);
|
||||||
|
void save_prepared_file(
|
||||||
|
std::string path,
|
||||||
|
std::string suggested_name,
|
||||||
|
std::function<void(const std::string& path, bool saved)> callback);
|
||||||
|
void set_platform_services(pp::platform::PlatformServices* services) noexcept;
|
||||||
|
[[nodiscard]] pp::platform::PlatformServices* platform_services() const noexcept;
|
||||||
|
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths();
|
||||||
void showKeyboard();
|
void showKeyboard();
|
||||||
void hideKeyboard();
|
void hideKeyboard();
|
||||||
void initLog();
|
void initLog();
|
||||||
@@ -248,6 +279,8 @@ public:
|
|||||||
void dialog_usermanual();
|
void dialog_usermanual();
|
||||||
void dialog_changelog();
|
void dialog_changelog();
|
||||||
void dialog_about();
|
void dialog_about();
|
||||||
|
void save_document(pp::app::DocumentSaveIntent intent);
|
||||||
|
void continue_document_workflow_after_optional_save(std::function<void()> action);
|
||||||
void dialog_newdoc();
|
void dialog_newdoc();
|
||||||
void dialog_save();
|
void dialog_save();
|
||||||
void dialog_save_ver();
|
void dialog_save_ver();
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "app_core/document_cloud.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "node_progress_bar.h"
|
#include "node_progress_bar.h"
|
||||||
#include "node_dialog_cloud.h"
|
#include "node_dialog_cloud.h"
|
||||||
|
|
||||||
void App::cloud_upload()
|
void App::cloud_upload()
|
||||||
{
|
{
|
||||||
if (!canvas)
|
const bool has_canvas = canvas != nullptr;
|
||||||
|
const auto plan = pp::app::plan_cloud_upload(
|
||||||
|
has_canvas,
|
||||||
|
has_canvas && Canvas::I->m_newdoc,
|
||||||
|
has_canvas && Canvas::I->m_unsaved);
|
||||||
|
|
||||||
|
switch (plan.action)
|
||||||
|
{
|
||||||
|
case pp::app::CloudUploadAction::unavailable_no_canvas:
|
||||||
return;
|
return;
|
||||||
if (Canvas::I->m_newdoc)
|
case pp::app::CloudUploadAction::show_save_required_warning:
|
||||||
{
|
|
||||||
message_box("Warning", "This document needs to be saved before upload.");
|
message_box("Warning", "This document needs to be saved before upload.");
|
||||||
|
return;
|
||||||
|
case pp::app::CloudUploadAction::prompt_publish:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
auto upload_thread = [this] {
|
auto upload_thread = [this] {
|
||||||
BT_SetTerminate();
|
BT_SetTerminate();
|
||||||
|
|
||||||
@@ -42,7 +52,6 @@ void App::cloud_upload()
|
|||||||
m->btn_cancel->on_click = [this, m, upload_thread](Node*) {
|
m->btn_cancel->on_click = [this, m, upload_thread](Node*) {
|
||||||
m->destroy();
|
m->destroy();
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::cloud_upload_all()
|
void App::cloud_upload_all()
|
||||||
@@ -51,22 +60,23 @@ void App::cloud_upload_all()
|
|||||||
BT_SetTerminate();
|
BT_SetTerminate();
|
||||||
|
|
||||||
auto names = Asset::list_files(data_path, ".*\\.ppi");
|
auto names = Asset::list_files(data_path, ".*\\.ppi");
|
||||||
|
const auto plan = pp::app::plan_cloud_bulk_upload(names.size(), layout.m_loaded);
|
||||||
|
|
||||||
gl_state gl;
|
gl_state gl;
|
||||||
std::shared_ptr<NodeProgressBar> pb;
|
std::shared_ptr<NodeProgressBar> pb;
|
||||||
if (layout.m_loaded)
|
if (plan.show_progress)
|
||||||
pb = show_progress("Export Pano Image", names.size());
|
pb = show_progress("Export Pano Image", plan.progress_total);
|
||||||
|
|
||||||
for (const auto& n : names)
|
for (const auto& n : names)
|
||||||
{
|
{
|
||||||
std::string path = data_path + "/" + n;
|
std::string path = data_path + "/" + n;
|
||||||
upload(path);
|
upload(path);
|
||||||
|
|
||||||
if (layout.m_loaded)
|
if (plan.show_progress)
|
||||||
pb->increment();
|
pb->increment();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layout.m_loaded)
|
if (plan.show_progress)
|
||||||
pb->destroy();
|
pb->destroy();
|
||||||
|
|
||||||
}).detach();
|
}).detach();
|
||||||
@@ -74,8 +84,14 @@ void App::cloud_upload_all()
|
|||||||
|
|
||||||
void App::cloud_browse()
|
void App::cloud_browse()
|
||||||
{
|
{
|
||||||
if (!canvas)
|
const auto browse_plan = pp::app::plan_cloud_browse(canvas != nullptr);
|
||||||
|
switch (browse_plan)
|
||||||
|
{
|
||||||
|
case pp::app::CloudBrowseAction::unavailable_no_canvas:
|
||||||
return;
|
return;
|
||||||
|
case pp::app::CloudBrowseAction::show_browser:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// load thumbnail test
|
// load thumbnail test
|
||||||
auto dialog = std::make_shared<NodeDialogCloud>();
|
auto dialog = std::make_shared<NodeDialogCloud>();
|
||||||
@@ -88,7 +104,8 @@ void App::cloud_browse()
|
|||||||
|
|
||||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||||
{
|
{
|
||||||
if (dialog->selected_file.empty())
|
const auto selection_plan = pp::app::plan_cloud_download_selection(dialog->selected_file);
|
||||||
|
if (selection_plan == pp::app::CloudDownloadSelectionAction::wait_for_selection)
|
||||||
return;
|
return;
|
||||||
dialog->destroy();
|
dialog->destroy();
|
||||||
std::thread([this, dialog] {
|
std::thread([this, dialog] {
|
||||||
|
|||||||
@@ -1,16 +1,46 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "canvas.h"
|
#include "canvas.h"
|
||||||
|
#include "renderer_gl/opengl_capabilities.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum depth_test_state() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::depth_test_state());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum program_point_size_state() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::program_point_size_state());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum source_alpha_blend_factor() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::source_alpha_blend_factor());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum one_minus_source_alpha_blend_factor() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::one_minus_source_alpha_blend_factor());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum add_blend_equation() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::add_blend_equation());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void App::cmd_convert(std::string pano_path, std::string out_path)
|
void App::cmd_convert(std::string pano_path, std::string out_path)
|
||||||
{
|
{
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(depth_test_state());
|
||||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
glEnable(program_point_size_state());
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(source_alpha_blend_factor(), one_minus_source_alpha_blend_factor());
|
||||||
glBlendEquation(GL_FUNC_ADD);
|
glBlendEquation(add_blend_equation());
|
||||||
|
|
||||||
Canvas* canvas = new Canvas;
|
Canvas* command_canvas = new Canvas;
|
||||||
canvas->create(CANVAS_RES, CANVAS_RES);
|
command_canvas->create(CANVAS_RES, CANVAS_RES);
|
||||||
canvas->project_open_thread(pano_path);
|
command_canvas->project_open_thread(pano_path);
|
||||||
canvas->export_equirectangular_thread(out_path);
|
command_canvas->export_equirectangular_thread(out_path);
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/app_core/about_menu.h
Normal file
126
src/app_core/about_menu.h
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class AboutMenuCommand {
|
||||||
|
help_guide,
|
||||||
|
about_app,
|
||||||
|
whats_new,
|
||||||
|
induce_crash,
|
||||||
|
performance_test,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AboutMenuAction {
|
||||||
|
show_user_manual,
|
||||||
|
show_about_dialog,
|
||||||
|
show_whats_new_dialog,
|
||||||
|
trigger_crash_test,
|
||||||
|
run_performance_test,
|
||||||
|
no_op_unavailable,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AboutMenuPlan {
|
||||||
|
AboutMenuCommand command = AboutMenuCommand::help_guide;
|
||||||
|
AboutMenuAction action = AboutMenuAction::show_user_manual;
|
||||||
|
std::string label;
|
||||||
|
bool closes_root_popup = true;
|
||||||
|
bool requires_canvas = false;
|
||||||
|
int performance_iterations = 0;
|
||||||
|
int performance_updates_per_iteration = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AboutMenuServices {
|
||||||
|
public:
|
||||||
|
virtual ~AboutMenuServices() = default;
|
||||||
|
|
||||||
|
virtual void show_user_manual() = 0;
|
||||||
|
virtual void show_about_dialog() = 0;
|
||||||
|
virtual void show_whats_new_dialog() = 0;
|
||||||
|
virtual void trigger_crash_test() = 0;
|
||||||
|
virtual void run_performance_test(const AboutMenuPlan& plan) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline AboutMenuPlan plan_about_menu_command(
|
||||||
|
AboutMenuCommand command,
|
||||||
|
int version_major,
|
||||||
|
int version_minor,
|
||||||
|
int version_fix,
|
||||||
|
bool diagnostics_available = true,
|
||||||
|
bool has_canvas = true)
|
||||||
|
{
|
||||||
|
AboutMenuPlan plan;
|
||||||
|
plan.command = command;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case AboutMenuCommand::help_guide:
|
||||||
|
plan.action = AboutMenuAction::show_user_manual;
|
||||||
|
plan.label = "Help Guide";
|
||||||
|
break;
|
||||||
|
case AboutMenuCommand::about_app:
|
||||||
|
plan.action = AboutMenuAction::show_about_dialog;
|
||||||
|
plan.label = "About PanoPainter";
|
||||||
|
break;
|
||||||
|
case AboutMenuCommand::whats_new:
|
||||||
|
plan.action = AboutMenuAction::show_whats_new_dialog;
|
||||||
|
plan.label = "What's new in "
|
||||||
|
+ std::to_string(version_major)
|
||||||
|
+ "."
|
||||||
|
+ std::to_string(version_minor)
|
||||||
|
+ "."
|
||||||
|
+ std::to_string(version_fix)
|
||||||
|
+ "?";
|
||||||
|
break;
|
||||||
|
case AboutMenuCommand::induce_crash:
|
||||||
|
plan.label = "Induce crash";
|
||||||
|
plan.action = diagnostics_available
|
||||||
|
? AboutMenuAction::trigger_crash_test
|
||||||
|
: AboutMenuAction::no_op_unavailable;
|
||||||
|
plan.closes_root_popup = diagnostics_available;
|
||||||
|
break;
|
||||||
|
case AboutMenuCommand::performance_test:
|
||||||
|
plan.label = has_canvas ? "Performance test" : "Performance test (No canvas)";
|
||||||
|
plan.requires_canvas = true;
|
||||||
|
plan.performance_iterations = 100;
|
||||||
|
plan.performance_updates_per_iteration = 10;
|
||||||
|
plan.action = has_canvas
|
||||||
|
? AboutMenuAction::run_performance_test
|
||||||
|
: AboutMenuAction::no_op_unavailable;
|
||||||
|
plan.closes_root_popup = has_canvas;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_about_menu_plan(
|
||||||
|
const AboutMenuPlan& plan,
|
||||||
|
AboutMenuServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.action) {
|
||||||
|
case AboutMenuAction::show_user_manual:
|
||||||
|
services.show_user_manual();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case AboutMenuAction::show_about_dialog:
|
||||||
|
services.show_about_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case AboutMenuAction::show_whats_new_dialog:
|
||||||
|
services.show_whats_new_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case AboutMenuAction::trigger_crash_test:
|
||||||
|
services.trigger_crash_test();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case AboutMenuAction::run_performance_test:
|
||||||
|
services.run_performance_test(plan);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case AboutMenuAction::no_op_unavailable:
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown about menu action");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
114
src/app_core/app_preferences.h
Normal file
114
src/app_core/app_preferences.h
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class InterfaceDirection {
|
||||||
|
left_to_right,
|
||||||
|
right_to_left,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TimelapseRecordingAction {
|
||||||
|
no_op,
|
||||||
|
start_recording,
|
||||||
|
stop_recording,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScaleApplicationPlan {
|
||||||
|
float scale = 1.0F;
|
||||||
|
float display_density = 1.0F;
|
||||||
|
float font_scale = 1.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScaleOptionSelection {
|
||||||
|
bool has_selection = false;
|
||||||
|
std::size_t index = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InterfaceDirectionPlan {
|
||||||
|
InterfaceDirection direction = InterfaceDirection::left_to_right;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TimelapsePreferencePlan {
|
||||||
|
bool enabled = true;
|
||||||
|
TimelapseRecordingAction recording_action = TimelapseRecordingAction::no_op;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StoredIntegerPreferencePlan {
|
||||||
|
int value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StoredBooleanPreferencePlan {
|
||||||
|
bool value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr ScaleApplicationPlan plan_ui_scale(
|
||||||
|
float requested_scale,
|
||||||
|
float display_density) noexcept
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
requested_scale,
|
||||||
|
display_density,
|
||||||
|
requested_scale * display_density,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr ScaleApplicationPlan plan_viewport_scale(
|
||||||
|
float requested_scale,
|
||||||
|
float display_density = 1.0F) noexcept
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
requested_scale,
|
||||||
|
display_density,
|
||||||
|
requested_scale * display_density,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr ScaleOptionSelection plan_scale_option_selection(
|
||||||
|
float current_scale,
|
||||||
|
std::span<const float> options) noexcept
|
||||||
|
{
|
||||||
|
ScaleOptionSelection selection;
|
||||||
|
for (std::size_t index = 0; index < options.size(); ++index) {
|
||||||
|
if (current_scale >= options[index]) {
|
||||||
|
selection.has_selection = true;
|
||||||
|
selection.index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr InterfaceDirectionPlan plan_interface_direction(bool right_to_left) noexcept
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
right_to_left ? InterfaceDirection::right_to_left : InterfaceDirection::left_to_right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr TimelapsePreferencePlan plan_timelapse_preference(
|
||||||
|
bool enabled,
|
||||||
|
bool recording_running) noexcept
|
||||||
|
{
|
||||||
|
if (enabled && !recording_running) {
|
||||||
|
return { enabled, TimelapseRecordingAction::start_recording };
|
||||||
|
}
|
||||||
|
if (!enabled && recording_running) {
|
||||||
|
return { enabled, TimelapseRecordingAction::stop_recording };
|
||||||
|
}
|
||||||
|
return { enabled, TimelapseRecordingAction::no_op };
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_controllers_preference(
|
||||||
|
bool enabled) noexcept
|
||||||
|
{
|
||||||
|
return { enabled };
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr StoredIntegerPreferencePlan plan_canvas_cursor_mode(int mode) noexcept
|
||||||
|
{
|
||||||
|
return { mode };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
119
src/app_core/app_status.h
Normal file
119
src/app_core/app_status.h
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
inline constexpr std::array<int, 6> document_resolution_values {
|
||||||
|
512,
|
||||||
|
1024,
|
||||||
|
1536,
|
||||||
|
2048,
|
||||||
|
4096,
|
||||||
|
8192,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr std::array<std::string_view, 6> document_resolution_labels {
|
||||||
|
"2K",
|
||||||
|
"4K",
|
||||||
|
"6K",
|
||||||
|
"8K",
|
||||||
|
"16K",
|
||||||
|
"32K",
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RecordingFrameLabel {
|
||||||
|
bool visible = false;
|
||||||
|
std::string text;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<int> display_resolution_from_index(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || static_cast<std::size_t>(index) >= document_resolution_values.size()) {
|
||||||
|
return pp::foundation::Result<int>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document resolution index is out of range"));
|
||||||
|
}
|
||||||
|
return pp::foundation::Result<int>::success(
|
||||||
|
document_resolution_values[static_cast<std::size_t>(index)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<std::size_t> document_resolution_to_index(int resolution)
|
||||||
|
{
|
||||||
|
for (std::size_t index = 0; index < document_resolution_values.size(); ++index) {
|
||||||
|
if (document_resolution_values[index] == resolution) {
|
||||||
|
return pp::foundation::Result<std::size_t>::success(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document resolution is not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<std::string_view> document_resolution_label(int resolution)
|
||||||
|
{
|
||||||
|
const auto index = document_resolution_to_index(resolution);
|
||||||
|
if (!index) {
|
||||||
|
return pp::foundation::Result<std::string_view>::failure(index.status());
|
||||||
|
}
|
||||||
|
return pp::foundation::Result<std::string_view>::success(document_resolution_labels[index.value()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::string make_document_title(
|
||||||
|
std::string_view document_name,
|
||||||
|
bool has_unsaved_changes,
|
||||||
|
int resolution)
|
||||||
|
{
|
||||||
|
const auto label = document_resolution_label(resolution);
|
||||||
|
const auto resolution_label = label ? label.value() : std::string_view("unknown");
|
||||||
|
std::string title = "Panodoc: ";
|
||||||
|
title.append(document_name);
|
||||||
|
if (has_unsaved_changes) {
|
||||||
|
title.push_back('*');
|
||||||
|
}
|
||||||
|
title.append(" (");
|
||||||
|
title.append(resolution_label);
|
||||||
|
title.push_back(')');
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::string make_dpi_label(float zoom)
|
||||||
|
{
|
||||||
|
char buffer[64] {};
|
||||||
|
std::snprintf(buffer, sizeof(buffer), "%.1fx-dpi", zoom);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::string make_history_memory_label(std::size_t bytes)
|
||||||
|
{
|
||||||
|
char buffer[128] {};
|
||||||
|
std::snprintf(
|
||||||
|
buffer,
|
||||||
|
sizeof(buffer),
|
||||||
|
"History memory: %.2f Mb",
|
||||||
|
static_cast<double>(bytes) / 1024.0 / 1024.0);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline RecordingFrameLabel make_recording_frame_label(
|
||||||
|
bool is_recording,
|
||||||
|
bool encoder_available,
|
||||||
|
int encoded_frames)
|
||||||
|
{
|
||||||
|
if (!is_recording || !encoder_available) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
char buffer[128] {};
|
||||||
|
std::snprintf(buffer, sizeof(buffer), "Recorded %d frames", encoded_frames);
|
||||||
|
return {
|
||||||
|
true,
|
||||||
|
buffer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
628
src/app_core/brush_ui.h
Normal file
628
src/app_core/brush_ui.h
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class BrushUiTextureSlot {
|
||||||
|
tip,
|
||||||
|
pattern,
|
||||||
|
dual,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class BrushUiOperation {
|
||||||
|
set_tip_color,
|
||||||
|
set_texture,
|
||||||
|
replace_brush_from_preset,
|
||||||
|
stroke_settings_changed,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class BrushTextureListOperation {
|
||||||
|
add_texture,
|
||||||
|
remove_texture,
|
||||||
|
move_texture,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class BrushStrokeControlOperation {
|
||||||
|
set_float,
|
||||||
|
set_bool,
|
||||||
|
set_blend_mode,
|
||||||
|
reset_tip_aspect,
|
||||||
|
reset_default_brush,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class BrushStrokeFloatSetting {
|
||||||
|
tip_size,
|
||||||
|
tip_spacing,
|
||||||
|
tip_flow,
|
||||||
|
tip_opacity,
|
||||||
|
tip_angle,
|
||||||
|
tip_angle_smooth,
|
||||||
|
tip_mix,
|
||||||
|
tip_wet,
|
||||||
|
tip_noise,
|
||||||
|
tip_hue,
|
||||||
|
tip_saturation,
|
||||||
|
tip_value,
|
||||||
|
jitter_scale,
|
||||||
|
jitter_angle,
|
||||||
|
jitter_scatter,
|
||||||
|
jitter_flow,
|
||||||
|
jitter_opacity,
|
||||||
|
jitter_hue,
|
||||||
|
jitter_saturation,
|
||||||
|
jitter_value,
|
||||||
|
jitter_aspect,
|
||||||
|
dual_size,
|
||||||
|
dual_spacing,
|
||||||
|
dual_scatter,
|
||||||
|
tip_aspect,
|
||||||
|
dual_opacity,
|
||||||
|
dual_flow,
|
||||||
|
dual_rotate,
|
||||||
|
pattern_scale,
|
||||||
|
pattern_brightness,
|
||||||
|
pattern_contrast,
|
||||||
|
pattern_depth,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class BrushStrokeBoolSetting {
|
||||||
|
tip_angle_init,
|
||||||
|
tip_angle_follow,
|
||||||
|
tip_flow_pressure,
|
||||||
|
tip_opacity_pressure,
|
||||||
|
tip_size_pressure,
|
||||||
|
jitter_scatter_both_axis,
|
||||||
|
jitter_aspect_both_axis,
|
||||||
|
jitter_hsv_each_sample,
|
||||||
|
tip_invert,
|
||||||
|
tip_flip_x,
|
||||||
|
tip_flip_y,
|
||||||
|
pattern_enabled,
|
||||||
|
dual_enabled,
|
||||||
|
dual_scatter_both_axis,
|
||||||
|
dual_invert,
|
||||||
|
dual_flip_x,
|
||||||
|
dual_flip_y,
|
||||||
|
dual_random_flip,
|
||||||
|
tip_random_flip_x,
|
||||||
|
tip_random_flip_y,
|
||||||
|
pattern_each_sample,
|
||||||
|
pattern_invert,
|
||||||
|
pattern_flip_x,
|
||||||
|
pattern_flip_y,
|
||||||
|
pattern_random_offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class BrushStrokeBlendSetting {
|
||||||
|
tip,
|
||||||
|
dual,
|
||||||
|
pattern,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BrushUiPlan {
|
||||||
|
BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
|
||||||
|
BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
|
||||||
|
std::string path;
|
||||||
|
std::string thumbnail_path;
|
||||||
|
float r = 0.0F;
|
||||||
|
float g = 0.0F;
|
||||||
|
float b = 0.0F;
|
||||||
|
float a = 1.0F;
|
||||||
|
bool mutates_brush = false;
|
||||||
|
bool preserves_existing_color = false;
|
||||||
|
bool loads_brush_resources = false;
|
||||||
|
bool update_color_ui = false;
|
||||||
|
bool update_brush_ui = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BrushTextureListPlan {
|
||||||
|
BrushTextureListOperation operation = BrushTextureListOperation::add_texture;
|
||||||
|
int item_count = 0;
|
||||||
|
int current_index = -1;
|
||||||
|
int target_index = -1;
|
||||||
|
int move_offset = 0;
|
||||||
|
std::string source_path;
|
||||||
|
std::string high_path;
|
||||||
|
std::string thumbnail_path;
|
||||||
|
std::string brush_name;
|
||||||
|
bool user_texture = false;
|
||||||
|
bool deletes_texture_files = false;
|
||||||
|
bool saves_list = false;
|
||||||
|
bool notifies_selection = false;
|
||||||
|
bool converts_brush_alpha = false;
|
||||||
|
bool no_op = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BrushStrokeControlPlan {
|
||||||
|
BrushStrokeControlOperation operation = BrushStrokeControlOperation::set_float;
|
||||||
|
BrushStrokeFloatSetting float_setting = BrushStrokeFloatSetting::tip_size;
|
||||||
|
BrushStrokeBoolSetting bool_setting = BrushStrokeBoolSetting::tip_angle_init;
|
||||||
|
BrushStrokeBlendSetting blend_setting = BrushStrokeBlendSetting::tip;
|
||||||
|
float float_value = 0.0F;
|
||||||
|
bool bool_value = false;
|
||||||
|
int blend_mode = 0;
|
||||||
|
bool mutates_brush = false;
|
||||||
|
bool updates_controls = false;
|
||||||
|
bool refreshes_preview = false;
|
||||||
|
bool notifies_stroke_change = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BrushUiServices {
|
||||||
|
public:
|
||||||
|
virtual ~BrushUiServices() = default;
|
||||||
|
|
||||||
|
virtual void set_tip_color(float r, float g, float b, float a) = 0;
|
||||||
|
virtual void set_texture(BrushUiTextureSlot slot, std::string_view path, std::string_view thumbnail_path) = 0;
|
||||||
|
virtual void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) = 0;
|
||||||
|
virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BrushTextureListServices {
|
||||||
|
public:
|
||||||
|
virtual ~BrushTextureListServices() = default;
|
||||||
|
|
||||||
|
virtual pp::foundation::Status add_texture_from_source(
|
||||||
|
std::string_view source_path,
|
||||||
|
std::string_view high_path,
|
||||||
|
std::string_view thumbnail_path,
|
||||||
|
std::string_view brush_name,
|
||||||
|
bool converts_brush_alpha) = 0;
|
||||||
|
virtual void remove_texture(int index, bool delete_texture_files) = 0;
|
||||||
|
virtual void move_texture(int from_index, int to_index) = 0;
|
||||||
|
virtual void select_texture(int index) = 0;
|
||||||
|
virtual void save_texture_list() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BrushStrokeControlServices {
|
||||||
|
public:
|
||||||
|
virtual ~BrushStrokeControlServices() = default;
|
||||||
|
|
||||||
|
virtual void set_float_setting(BrushStrokeFloatSetting setting, float value) = 0;
|
||||||
|
virtual void set_bool_setting(BrushStrokeBoolSetting setting, bool value) = 0;
|
||||||
|
virtual void set_blend_mode(BrushStrokeBlendSetting setting, int blend_mode) = 0;
|
||||||
|
virtual void reset_tip_aspect(float value) = 0;
|
||||||
|
virtual void reset_default_brush() = 0;
|
||||||
|
virtual void update_stroke_controls() = 0;
|
||||||
|
virtual void refresh_stroke_preview() = 0;
|
||||||
|
virtual void notify_stroke_changed() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<std::string_view> brush_texture_source_stem(
|
||||||
|
std::string_view source_path) noexcept
|
||||||
|
{
|
||||||
|
const auto slash = source_path.find_last_of("/\\");
|
||||||
|
const auto name_begin = slash == std::string_view::npos ? 0U : slash + 1U;
|
||||||
|
if (name_begin >= source_path.size()) {
|
||||||
|
return pp::foundation::Result<std::string_view>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("brush texture source path must contain a file name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto dot = source_path.find_last_of('.');
|
||||||
|
if (dot == std::string_view::npos || dot <= name_begin || dot + 1U >= source_path.size()) {
|
||||||
|
return pp::foundation::Result<std::string_view>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("brush texture source path must include a file extension"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<std::string_view>::success(source_path.substr(name_begin, dot - name_begin));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept
|
||||||
|
{
|
||||||
|
if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
|
||||||
|
return pp::foundation::Status::out_of_range("brush color channels must be finite and within 0..1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_float(float value) noexcept
|
||||||
|
{
|
||||||
|
if (!std::isfinite(value)) {
|
||||||
|
return pp::foundation::Status::invalid_argument("brush stroke float setting must be finite");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_blend_mode(int blend_mode) noexcept
|
||||||
|
{
|
||||||
|
if (blend_mode < 0 || blend_mode > 63) {
|
||||||
|
return pp::foundation::Status::out_of_range("brush stroke blend mode must be within 0..63");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_color(
|
||||||
|
float r,
|
||||||
|
float g,
|
||||||
|
float b,
|
||||||
|
float a)
|
||||||
|
{
|
||||||
|
for (const auto value : { r, g, b, a }) {
|
||||||
|
const auto channel_status = validate_brush_ui_color_channel(value);
|
||||||
|
if (!channel_status.ok()) {
|
||||||
|
return pp::foundation::Result<BrushUiPlan>::failure(channel_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BrushUiPlan plan;
|
||||||
|
plan.operation = BrushUiOperation::set_tip_color;
|
||||||
|
plan.r = r;
|
||||||
|
plan.g = g;
|
||||||
|
plan.b = b;
|
||||||
|
plan.a = a;
|
||||||
|
plan.mutates_brush = true;
|
||||||
|
plan.update_color_ui = true;
|
||||||
|
return pp::foundation::Result<BrushUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_texture(
|
||||||
|
BrushUiTextureSlot slot,
|
||||||
|
std::string_view path,
|
||||||
|
std::string_view thumbnail_path)
|
||||||
|
{
|
||||||
|
if (path.empty()) {
|
||||||
|
return pp::foundation::Result<BrushUiPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("brush texture path must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
BrushUiPlan plan;
|
||||||
|
plan.operation = BrushUiOperation::set_texture;
|
||||||
|
plan.texture_slot = slot;
|
||||||
|
plan.path = std::string(path);
|
||||||
|
plan.thumbnail_path = std::string(thumbnail_path);
|
||||||
|
plan.mutates_brush = true;
|
||||||
|
plan.loads_brush_resources = true;
|
||||||
|
plan.update_color_ui = true;
|
||||||
|
plan.update_brush_ui = true;
|
||||||
|
return pp::foundation::Result<BrushUiPlan>::success(std::move(plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_preset_replace(bool has_preset_brush)
|
||||||
|
{
|
||||||
|
if (!has_preset_brush) {
|
||||||
|
return pp::foundation::Result<BrushUiPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("preset brush must be available"));
|
||||||
|
}
|
||||||
|
|
||||||
|
BrushUiPlan plan;
|
||||||
|
plan.operation = BrushUiOperation::replace_brush_from_preset;
|
||||||
|
plan.mutates_brush = true;
|
||||||
|
plan.preserves_existing_color = true;
|
||||||
|
plan.loads_brush_resources = true;
|
||||||
|
plan.update_color_ui = true;
|
||||||
|
plan.update_brush_ui = true;
|
||||||
|
return pp::foundation::Result<BrushUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr BrushUiPlan plan_brush_ui_stroke_settings_changed() noexcept
|
||||||
|
{
|
||||||
|
BrushUiPlan plan;
|
||||||
|
plan.operation = BrushUiOperation::stroke_settings_changed;
|
||||||
|
plan.mutates_brush = true;
|
||||||
|
plan.update_color_ui = true;
|
||||||
|
plan.update_brush_ui = true;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_float_setting(
|
||||||
|
BrushStrokeFloatSetting setting,
|
||||||
|
float value)
|
||||||
|
{
|
||||||
|
const auto status = validate_brush_stroke_float(value);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
BrushStrokeControlPlan plan;
|
||||||
|
plan.operation = BrushStrokeControlOperation::set_float;
|
||||||
|
plan.float_setting = setting;
|
||||||
|
plan.float_value = value;
|
||||||
|
plan.mutates_brush = true;
|
||||||
|
plan.refreshes_preview = true;
|
||||||
|
plan.notifies_stroke_change = true;
|
||||||
|
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_stroke_bool_setting(
|
||||||
|
BrushStrokeBoolSetting setting,
|
||||||
|
bool value) noexcept
|
||||||
|
{
|
||||||
|
BrushStrokeControlPlan plan;
|
||||||
|
plan.operation = BrushStrokeControlOperation::set_bool;
|
||||||
|
plan.bool_setting = setting;
|
||||||
|
plan.bool_value = value;
|
||||||
|
plan.mutates_brush = true;
|
||||||
|
plan.refreshes_preview = true;
|
||||||
|
plan.notifies_stroke_change = true;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_blend_mode(
|
||||||
|
BrushStrokeBlendSetting setting,
|
||||||
|
int blend_mode)
|
||||||
|
{
|
||||||
|
const auto status = validate_brush_stroke_blend_mode(blend_mode);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
BrushStrokeControlPlan plan;
|
||||||
|
plan.operation = BrushStrokeControlOperation::set_blend_mode;
|
||||||
|
plan.blend_setting = setting;
|
||||||
|
plan.blend_mode = blend_mode;
|
||||||
|
plan.mutates_brush = true;
|
||||||
|
plan.refreshes_preview = true;
|
||||||
|
plan.notifies_stroke_change = true;
|
||||||
|
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_tip_aspect_reset(float value = 0.5F) noexcept
|
||||||
|
{
|
||||||
|
BrushStrokeControlPlan plan;
|
||||||
|
plan.operation = BrushStrokeControlOperation::reset_tip_aspect;
|
||||||
|
plan.float_setting = BrushStrokeFloatSetting::tip_aspect;
|
||||||
|
plan.float_value = value;
|
||||||
|
plan.mutates_brush = true;
|
||||||
|
plan.refreshes_preview = true;
|
||||||
|
plan.notifies_stroke_change = true;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_default_settings_reset() noexcept
|
||||||
|
{
|
||||||
|
BrushStrokeControlPlan plan;
|
||||||
|
plan.operation = BrushStrokeControlOperation::reset_default_brush;
|
||||||
|
plan.mutates_brush = true;
|
||||||
|
plan.updates_controls = true;
|
||||||
|
plan.refreshes_preview = true;
|
||||||
|
plan.notifies_stroke_change = true;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_add(
|
||||||
|
std::string_view directory_name,
|
||||||
|
std::string_view data_path,
|
||||||
|
std::string_view source_path)
|
||||||
|
{
|
||||||
|
if (directory_name.empty()) {
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("brush texture directory must not be empty"));
|
||||||
|
}
|
||||||
|
if (data_path.empty()) {
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("brush texture data path must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto stem = brush_texture_source_stem(source_path);
|
||||||
|
if (!stem) {
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::failure(stem.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
BrushTextureListPlan plan;
|
||||||
|
plan.operation = BrushTextureListOperation::add_texture;
|
||||||
|
plan.source_path = std::string(source_path);
|
||||||
|
plan.brush_name = std::string(stem.value());
|
||||||
|
plan.high_path = std::string(data_path) + "/" + std::string(directory_name) + "/" + plan.brush_name + ".png";
|
||||||
|
plan.thumbnail_path = std::string(data_path) + "/" + std::string(directory_name) + "/thumbs/"
|
||||||
|
+ plan.brush_name + ".png";
|
||||||
|
plan.user_texture = true;
|
||||||
|
plan.saves_list = true;
|
||||||
|
plan.converts_brush_alpha = directory_name == "brushes";
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::success(std::move(plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_remove(
|
||||||
|
int item_count,
|
||||||
|
int current_index,
|
||||||
|
bool current_is_user_texture)
|
||||||
|
{
|
||||||
|
if (item_count <= 0) {
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("brush texture list must contain an item to remove"));
|
||||||
|
}
|
||||||
|
if (current_index < 0 || current_index >= item_count) {
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
BrushTextureListPlan plan;
|
||||||
|
plan.operation = BrushTextureListOperation::remove_texture;
|
||||||
|
plan.item_count = item_count;
|
||||||
|
plan.current_index = current_index;
|
||||||
|
plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
|
||||||
|
plan.user_texture = current_is_user_texture;
|
||||||
|
plan.deletes_texture_files = current_is_user_texture;
|
||||||
|
plan.saves_list = true;
|
||||||
|
plan.notifies_selection = plan.target_index >= 0;
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_move(
|
||||||
|
int item_count,
|
||||||
|
int current_index,
|
||||||
|
int offset)
|
||||||
|
{
|
||||||
|
if (item_count <= 0) {
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("brush texture list must contain an item to move"));
|
||||||
|
}
|
||||||
|
if (current_index < 0 || current_index >= item_count) {
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
|
||||||
|
}
|
||||||
|
if (offset == 0) {
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("brush texture move offset must not be zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
BrushTextureListPlan plan;
|
||||||
|
plan.operation = BrushTextureListOperation::move_texture;
|
||||||
|
plan.item_count = item_count;
|
||||||
|
plan.current_index = current_index;
|
||||||
|
plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
|
||||||
|
plan.move_offset = offset;
|
||||||
|
plan.saves_list = true;
|
||||||
|
plan.no_op = plan.target_index == current_index;
|
||||||
|
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
|
||||||
|
const BrushUiPlan& plan,
|
||||||
|
BrushUiServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.operation) {
|
||||||
|
case BrushUiOperation::set_tip_color:
|
||||||
|
{
|
||||||
|
for (const auto value : { plan.r, plan.g, plan.b, plan.a }) {
|
||||||
|
const auto channel_status = validate_brush_ui_color_channel(value);
|
||||||
|
if (!channel_status.ok()) {
|
||||||
|
return channel_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services.set_tip_color(plan.r, plan.g, plan.b, plan.a);
|
||||||
|
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrushUiOperation::set_texture:
|
||||||
|
if (plan.path.empty()) {
|
||||||
|
return pp::foundation::Status::invalid_argument("brush texture path must not be empty");
|
||||||
|
}
|
||||||
|
services.set_texture(plan.texture_slot, plan.path, plan.thumbnail_path);
|
||||||
|
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case BrushUiOperation::replace_brush_from_preset:
|
||||||
|
services.replace_brush_from_preset(plan.preserves_existing_color, plan.loads_brush_resources);
|
||||||
|
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case BrushUiOperation::stroke_settings_changed:
|
||||||
|
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown brush UI operation");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_brush_stroke_control_plan(
|
||||||
|
const BrushStrokeControlPlan& plan,
|
||||||
|
BrushStrokeControlServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.operation) {
|
||||||
|
case BrushStrokeControlOperation::set_float:
|
||||||
|
{
|
||||||
|
const auto status = validate_brush_stroke_float(plan.float_value);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
services.set_float_setting(plan.float_setting, plan.float_value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrushStrokeControlOperation::set_bool:
|
||||||
|
services.set_bool_setting(plan.bool_setting, plan.bool_value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BrushStrokeControlOperation::set_blend_mode:
|
||||||
|
{
|
||||||
|
const auto status = validate_brush_stroke_blend_mode(plan.blend_mode);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
services.set_blend_mode(plan.blend_setting, plan.blend_mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrushStrokeControlOperation::reset_tip_aspect:
|
||||||
|
{
|
||||||
|
const auto status = validate_brush_stroke_float(plan.float_value);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
services.reset_tip_aspect(plan.float_value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrushStrokeControlOperation::reset_default_brush:
|
||||||
|
services.reset_default_brush();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.updates_controls) {
|
||||||
|
services.update_stroke_controls();
|
||||||
|
}
|
||||||
|
if (plan.refreshes_preview) {
|
||||||
|
services.refresh_stroke_preview();
|
||||||
|
}
|
||||||
|
if (plan.notifies_stroke_change) {
|
||||||
|
services.notify_stroke_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_brush_texture_list_plan(
|
||||||
|
const BrushTextureListPlan& plan,
|
||||||
|
BrushTextureListServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.operation) {
|
||||||
|
case BrushTextureListOperation::add_texture:
|
||||||
|
{
|
||||||
|
if (plan.source_path.empty() || plan.high_path.empty() || plan.thumbnail_path.empty()
|
||||||
|
|| plan.brush_name.empty()) {
|
||||||
|
return pp::foundation::Status::invalid_argument("brush texture add plan has incomplete paths");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto add_status = services.add_texture_from_source(
|
||||||
|
plan.source_path,
|
||||||
|
plan.high_path,
|
||||||
|
plan.thumbnail_path,
|
||||||
|
plan.brush_name,
|
||||||
|
plan.converts_brush_alpha);
|
||||||
|
if (!add_status.ok()) {
|
||||||
|
return add_status;
|
||||||
|
}
|
||||||
|
if (plan.saves_list) {
|
||||||
|
services.save_texture_list();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrushTextureListOperation::remove_texture:
|
||||||
|
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("brush texture remove plan has invalid selection");
|
||||||
|
}
|
||||||
|
services.remove_texture(plan.current_index, plan.deletes_texture_files);
|
||||||
|
if (plan.notifies_selection && plan.target_index >= 0) {
|
||||||
|
services.select_texture(plan.target_index);
|
||||||
|
}
|
||||||
|
if (plan.saves_list) {
|
||||||
|
services.save_texture_list();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case BrushTextureListOperation::move_texture:
|
||||||
|
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
|
||||||
|
|| plan.target_index < 0 || plan.target_index >= plan.item_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("brush texture move plan has invalid indices");
|
||||||
|
}
|
||||||
|
services.move_texture(plan.current_index, plan.target_index);
|
||||||
|
if (plan.saves_list) {
|
||||||
|
services.save_texture_list();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown brush texture list operation");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
180
src/app_core/canvas_tool_ui.h
Normal file
180
src/app_core/canvas_tool_ui.h
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class CanvasToolOperation {
|
||||||
|
select_mode,
|
||||||
|
toggle_picking,
|
||||||
|
toggle_touch_lock,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CanvasToolMode {
|
||||||
|
draw,
|
||||||
|
erase,
|
||||||
|
line,
|
||||||
|
camera,
|
||||||
|
grid,
|
||||||
|
copy,
|
||||||
|
cut,
|
||||||
|
fill,
|
||||||
|
mask_free,
|
||||||
|
mask_line,
|
||||||
|
flood_fill,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CanvasToolTransformAction {
|
||||||
|
none,
|
||||||
|
copy,
|
||||||
|
cut,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasToolPlan {
|
||||||
|
CanvasToolOperation operation = CanvasToolOperation::select_mode;
|
||||||
|
CanvasToolMode mode = CanvasToolMode::draw;
|
||||||
|
CanvasToolTransformAction transform_action = CanvasToolTransformAction::none;
|
||||||
|
bool selects_toolbar_button = false;
|
||||||
|
bool updates_canvas_mode = false;
|
||||||
|
bool toggles_picking = false;
|
||||||
|
bool toggles_touch_lock = false;
|
||||||
|
bool requires_draw_mode = false;
|
||||||
|
bool no_op = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasToolButtonState {
|
||||||
|
CanvasToolMode mode = CanvasToolMode::draw;
|
||||||
|
bool pick_active = false;
|
||||||
|
bool touch_lock_active = false;
|
||||||
|
bool pen_active = false;
|
||||||
|
bool erase_active = false;
|
||||||
|
bool line_active = false;
|
||||||
|
bool camera_active = false;
|
||||||
|
bool grid_active = false;
|
||||||
|
bool copy_active = false;
|
||||||
|
bool cut_active = false;
|
||||||
|
bool fill_active = false;
|
||||||
|
bool mask_free_active = false;
|
||||||
|
bool mask_line_active = false;
|
||||||
|
bool flood_fill_active = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CanvasToolServices {
|
||||||
|
public:
|
||||||
|
virtual ~CanvasToolServices() = default;
|
||||||
|
|
||||||
|
virtual void select_toolbar_button(CanvasToolMode mode) = 0;
|
||||||
|
virtual void set_transform_action(CanvasToolTransformAction action) = 0;
|
||||||
|
virtual void set_canvas_mode(CanvasToolMode mode) = 0;
|
||||||
|
virtual void toggle_picking() = 0;
|
||||||
|
virtual void toggle_touch_lock() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr CanvasToolTransformAction transform_action_for_mode(CanvasToolMode mode) noexcept
|
||||||
|
{
|
||||||
|
if (mode == CanvasToolMode::copy) {
|
||||||
|
return CanvasToolTransformAction::copy;
|
||||||
|
}
|
||||||
|
if (mode == CanvasToolMode::cut) {
|
||||||
|
return CanvasToolTransformAction::cut;
|
||||||
|
}
|
||||||
|
return CanvasToolTransformAction::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_select(CanvasToolMode mode) noexcept
|
||||||
|
{
|
||||||
|
CanvasToolPlan plan;
|
||||||
|
plan.operation = CanvasToolOperation::select_mode;
|
||||||
|
plan.mode = mode;
|
||||||
|
plan.transform_action = transform_action_for_mode(mode);
|
||||||
|
plan.selects_toolbar_button = true;
|
||||||
|
plan.updates_canvas_mode = true;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_pick_toggle(bool current_mode_is_draw) noexcept
|
||||||
|
{
|
||||||
|
CanvasToolPlan plan;
|
||||||
|
plan.operation = CanvasToolOperation::toggle_picking;
|
||||||
|
plan.mode = CanvasToolMode::draw;
|
||||||
|
plan.requires_draw_mode = true;
|
||||||
|
plan.toggles_picking = current_mode_is_draw;
|
||||||
|
plan.no_op = !current_mode_is_draw;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_touch_lock_toggle() noexcept
|
||||||
|
{
|
||||||
|
CanvasToolPlan plan;
|
||||||
|
plan.operation = CanvasToolOperation::toggle_touch_lock;
|
||||||
|
plan.toggles_touch_lock = true;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr CanvasToolButtonState plan_canvas_tool_button_state(
|
||||||
|
CanvasToolMode mode,
|
||||||
|
bool picking,
|
||||||
|
bool touch_lock) noexcept
|
||||||
|
{
|
||||||
|
CanvasToolButtonState state;
|
||||||
|
state.mode = mode;
|
||||||
|
state.pick_active = mode == CanvasToolMode::draw && picking;
|
||||||
|
state.touch_lock_active = touch_lock;
|
||||||
|
state.pen_active = mode == CanvasToolMode::draw;
|
||||||
|
state.erase_active = mode == CanvasToolMode::erase;
|
||||||
|
state.line_active = mode == CanvasToolMode::line;
|
||||||
|
state.camera_active = mode == CanvasToolMode::camera;
|
||||||
|
state.grid_active = mode == CanvasToolMode::grid;
|
||||||
|
state.copy_active = mode == CanvasToolMode::copy;
|
||||||
|
state.cut_active = mode == CanvasToolMode::cut;
|
||||||
|
state.fill_active = mode == CanvasToolMode::fill;
|
||||||
|
state.mask_free_active = mode == CanvasToolMode::mask_free;
|
||||||
|
state.mask_line_active = mode == CanvasToolMode::mask_line;
|
||||||
|
state.flood_fill_active = mode == CanvasToolMode::flood_fill;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan(
|
||||||
|
const CanvasToolPlan& plan,
|
||||||
|
CanvasToolServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.operation) {
|
||||||
|
case CanvasToolOperation::select_mode:
|
||||||
|
if (!plan.selects_toolbar_button || !plan.updates_canvas_mode) {
|
||||||
|
return pp::foundation::Status::invalid_argument("canvas tool select plan must select toolbar and update mode");
|
||||||
|
}
|
||||||
|
if (plan.transform_action != transform_action_for_mode(plan.mode)) {
|
||||||
|
return pp::foundation::Status::invalid_argument("canvas tool select plan has mismatched transform action");
|
||||||
|
}
|
||||||
|
services.select_toolbar_button(plan.mode);
|
||||||
|
if (plan.transform_action != CanvasToolTransformAction::none) {
|
||||||
|
services.set_transform_action(plan.transform_action);
|
||||||
|
}
|
||||||
|
services.set_canvas_mode(plan.mode);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case CanvasToolOperation::toggle_picking:
|
||||||
|
if (!plan.requires_draw_mode) {
|
||||||
|
return pp::foundation::Status::invalid_argument("canvas pick plan must require draw mode");
|
||||||
|
}
|
||||||
|
if (plan.no_op) {
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
if (!plan.toggles_picking) {
|
||||||
|
return pp::foundation::Status::invalid_argument("canvas pick plan must toggle picking or be a no-op");
|
||||||
|
}
|
||||||
|
services.toggle_picking();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case CanvasToolOperation::toggle_touch_lock:
|
||||||
|
if (!plan.toggles_touch_lock || plan.no_op) {
|
||||||
|
return pp::foundation::Status::invalid_argument("canvas touch-lock plan must toggle touch lock");
|
||||||
|
}
|
||||||
|
services.toggle_touch_lock();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown canvas tool operation");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
620
src/app_core/document_animation.h
Normal file
620
src/app_core/document_animation.h
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
inline constexpr int document_animation_default_frame_duration = 1;
|
||||||
|
|
||||||
|
enum class DocumentAnimationOperation {
|
||||||
|
add_frame,
|
||||||
|
duplicate_frame,
|
||||||
|
remove_frame,
|
||||||
|
adjust_duration,
|
||||||
|
move_frame,
|
||||||
|
select_frame,
|
||||||
|
goto_frame,
|
||||||
|
goto_next,
|
||||||
|
goto_previous,
|
||||||
|
playback_step,
|
||||||
|
toggle_playback,
|
||||||
|
set_onion_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentAnimationPanelAction {
|
||||||
|
goto_frame,
|
||||||
|
next_frame,
|
||||||
|
previous_frame,
|
||||||
|
playback_step,
|
||||||
|
toggle_playback,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentAnimationPanelState {
|
||||||
|
int total_duration = 1;
|
||||||
|
int current_frame = 0;
|
||||||
|
bool playback_active = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentAnimationOperationPlan {
|
||||||
|
DocumentAnimationOperation operation = DocumentAnimationOperation::goto_frame;
|
||||||
|
int frame_count = 1;
|
||||||
|
int current_frame = 0;
|
||||||
|
int selected_frame = 0;
|
||||||
|
int target_frame = 0;
|
||||||
|
int frame_duration = document_animation_default_frame_duration;
|
||||||
|
int duration_delta = 0;
|
||||||
|
int move_offset = 0;
|
||||||
|
int onion_size = 1;
|
||||||
|
int layer_index = 0;
|
||||||
|
std::uint32_t layer_id = 0;
|
||||||
|
int playback_idle_ms = 100;
|
||||||
|
bool requires_selected_frame = false;
|
||||||
|
bool mutates_document = false;
|
||||||
|
bool reloads_animation_layers = false;
|
||||||
|
bool updates_canvas_animation = false;
|
||||||
|
bool marks_unsaved = false;
|
||||||
|
bool playback_was_active = false;
|
||||||
|
bool playback_active = false;
|
||||||
|
bool resets_playback_timer = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DocumentAnimationServices {
|
||||||
|
public:
|
||||||
|
virtual ~DocumentAnimationServices() = default;
|
||||||
|
|
||||||
|
virtual void add_frame() = 0;
|
||||||
|
virtual void duplicate_frame(int selected_frame) = 0;
|
||||||
|
virtual void remove_frame(int selected_frame, int target_frame) = 0;
|
||||||
|
virtual void set_frame_duration(int selected_frame, int duration) = 0;
|
||||||
|
virtual int move_frame(int selected_frame, int move_offset) = 0;
|
||||||
|
virtual void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) = 0;
|
||||||
|
virtual void select_layer(int layer_index) = 0;
|
||||||
|
virtual void goto_frame(int target_frame) = 0;
|
||||||
|
virtual void set_timeline_frame(int target_frame) = 0;
|
||||||
|
virtual void set_onion_size(int onion_size) = 0;
|
||||||
|
virtual void capture_playback_restore_mode() = 0;
|
||||||
|
virtual void enter_playback_camera_mode() = 0;
|
||||||
|
virtual void restore_playback_canvas_mode() = 0;
|
||||||
|
virtual void set_playback_active(bool active) = 0;
|
||||||
|
virtual void reset_playback_timer() = 0;
|
||||||
|
virtual void set_playback_idle_ms(int idle_ms) = 0;
|
||||||
|
virtual void update_canvas_animation() = 0;
|
||||||
|
virtual void update_frame_status() = 0;
|
||||||
|
virtual void reload_animation_layers() = 0;
|
||||||
|
virtual void mark_unsaved() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_count(int frame_count) noexcept
|
||||||
|
{
|
||||||
|
if (frame_count <= 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation layer must contain at least one frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_index(
|
||||||
|
int frame_count,
|
||||||
|
int index) noexcept
|
||||||
|
{
|
||||||
|
const auto count_status = validate_animation_frame_count(frame_count);
|
||||||
|
if (!count_status.ok()) {
|
||||||
|
return count_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= frame_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("animation frame index is outside the layer");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_duration(int duration) noexcept
|
||||||
|
{
|
||||||
|
if (duration < 1) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation frame duration must be at least 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_add_frame(
|
||||||
|
int frame_count,
|
||||||
|
int current_frame)
|
||||||
|
{
|
||||||
|
const auto count_status = validate_animation_frame_count(frame_count);
|
||||||
|
if (!count_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(count_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_frame < 0) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("current animation frame must not be negative"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = DocumentAnimationOperation::add_frame;
|
||||||
|
plan.frame_count = frame_count;
|
||||||
|
plan.current_frame = current_frame;
|
||||||
|
plan.selected_frame = frame_count;
|
||||||
|
plan.target_frame = current_frame;
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
plan.updates_canvas_animation = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_duplicate_frame(
|
||||||
|
int frame_count,
|
||||||
|
int selected_frame)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = DocumentAnimationOperation::duplicate_frame;
|
||||||
|
plan.frame_count = frame_count;
|
||||||
|
plan.selected_frame = selected_frame;
|
||||||
|
plan.target_frame = selected_frame + 1;
|
||||||
|
plan.requires_selected_frame = true;
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
plan.updates_canvas_animation = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_remove_frame(
|
||||||
|
int frame_count,
|
||||||
|
int selected_frame)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame_count <= 1) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("animation layer must keep at least one frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = DocumentAnimationOperation::remove_frame;
|
||||||
|
plan.frame_count = frame_count;
|
||||||
|
plan.selected_frame = selected_frame;
|
||||||
|
plan.target_frame = std::min(selected_frame, frame_count - 2);
|
||||||
|
plan.requires_selected_frame = true;
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
plan.updates_canvas_animation = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_adjust_duration(
|
||||||
|
int frame_count,
|
||||||
|
int selected_frame,
|
||||||
|
int current_duration,
|
||||||
|
int delta)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto duration_status = validate_animation_frame_duration(current_duration);
|
||||||
|
if (!duration_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(duration_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta == 0) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("animation frame duration delta must not be zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta > 0 && current_duration > std::numeric_limits<int>::max() - delta) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("animation frame duration would overflow"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = DocumentAnimationOperation::adjust_duration;
|
||||||
|
plan.frame_count = frame_count;
|
||||||
|
plan.selected_frame = selected_frame;
|
||||||
|
plan.target_frame = selected_frame;
|
||||||
|
plan.frame_duration = std::max(current_duration + delta, 1);
|
||||||
|
plan.duration_delta = delta;
|
||||||
|
plan.requires_selected_frame = true;
|
||||||
|
plan.mutates_document = plan.frame_duration != current_duration;
|
||||||
|
plan.reloads_animation_layers = plan.mutates_document;
|
||||||
|
plan.updates_canvas_animation = plan.mutates_document;
|
||||||
|
plan.marks_unsaved = plan.mutates_document;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_move_frame(
|
||||||
|
int frame_count,
|
||||||
|
int selected_frame,
|
||||||
|
int offset)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset == 0) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("animation frame move offset must not be zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = DocumentAnimationOperation::move_frame;
|
||||||
|
plan.frame_count = frame_count;
|
||||||
|
plan.selected_frame = selected_frame;
|
||||||
|
const auto unclamped_target = static_cast<std::int64_t>(selected_frame) + static_cast<std::int64_t>(offset);
|
||||||
|
plan.target_frame = static_cast<int>(std::clamp<std::int64_t>(unclamped_target, 0, frame_count - 1));
|
||||||
|
plan.move_offset = offset;
|
||||||
|
plan.requires_selected_frame = true;
|
||||||
|
plan.mutates_document = plan.target_frame != selected_frame;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
plan.updates_canvas_animation = true;
|
||||||
|
plan.marks_unsaved = plan.mutates_document;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_select_frame(
|
||||||
|
int frame_count,
|
||||||
|
int layer_index,
|
||||||
|
std::uint32_t layer_id,
|
||||||
|
int selected_frame)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer_index < 0) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = DocumentAnimationOperation::select_frame;
|
||||||
|
plan.frame_count = frame_count;
|
||||||
|
plan.selected_frame = selected_frame;
|
||||||
|
plan.target_frame = selected_frame;
|
||||||
|
plan.layer_index = layer_index;
|
||||||
|
plan.layer_id = layer_id;
|
||||||
|
plan.requires_selected_frame = true;
|
||||||
|
plan.updates_canvas_animation = true;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_goto_frame(
|
||||||
|
int total_duration,
|
||||||
|
int frame)
|
||||||
|
{
|
||||||
|
if (total_duration <= 0) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("animation duration must be greater than zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame < 0 || frame >= total_duration) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("animation timeline frame is outside the document"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = DocumentAnimationOperation::goto_frame;
|
||||||
|
plan.frame_count = total_duration;
|
||||||
|
plan.current_frame = frame;
|
||||||
|
plan.target_frame = frame;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
plan.updates_canvas_animation = true;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_step_frame(
|
||||||
|
int total_duration,
|
||||||
|
int current_frame,
|
||||||
|
int offset)
|
||||||
|
{
|
||||||
|
const auto current_status = plan_animation_goto_frame(total_duration, current_frame);
|
||||||
|
if (!current_status) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(current_status.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset == 0) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("animation frame step offset must not be zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = offset > 0 ? DocumentAnimationOperation::goto_next : DocumentAnimationOperation::goto_previous;
|
||||||
|
plan.frame_count = total_duration;
|
||||||
|
plan.current_frame = current_frame;
|
||||||
|
auto target = (static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(offset))
|
||||||
|
% static_cast<std::int64_t>(total_duration);
|
||||||
|
if (target < 0) {
|
||||||
|
target += total_duration;
|
||||||
|
}
|
||||||
|
plan.target_frame = static_cast<int>(target);
|
||||||
|
plan.updates_canvas_animation = true;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_step(
|
||||||
|
int total_duration,
|
||||||
|
int current_frame,
|
||||||
|
int offset)
|
||||||
|
{
|
||||||
|
const auto step = plan_animation_step_frame(total_duration, current_frame, offset);
|
||||||
|
if (!step) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(step.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto plan = step.value();
|
||||||
|
plan.operation = DocumentAnimationOperation::playback_step;
|
||||||
|
plan.move_offset = offset;
|
||||||
|
plan.reloads_animation_layers = false;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_toggle(
|
||||||
|
bool playback_active)
|
||||||
|
{
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = DocumentAnimationOperation::toggle_playback;
|
||||||
|
plan.playback_was_active = playback_active;
|
||||||
|
plan.playback_active = !playback_active;
|
||||||
|
plan.playback_idle_ms = playback_active ? 100 : 10;
|
||||||
|
plan.resets_playback_timer = !playback_active;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size)
|
||||||
|
{
|
||||||
|
if (onion_size < 0) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationOperationPlan plan;
|
||||||
|
plan.operation = DocumentAnimationOperation::set_onion_size;
|
||||||
|
plan.onion_size = onion_size;
|
||||||
|
plan.updates_canvas_animation = true;
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_panel_action(
|
||||||
|
DocumentAnimationPanelAction action,
|
||||||
|
const DocumentAnimationPanelState& state,
|
||||||
|
int target_frame = 0)
|
||||||
|
{
|
||||||
|
switch (action) {
|
||||||
|
case DocumentAnimationPanelAction::goto_frame:
|
||||||
|
return plan_animation_goto_frame(state.total_duration, target_frame);
|
||||||
|
|
||||||
|
case DocumentAnimationPanelAction::next_frame:
|
||||||
|
return plan_animation_step_frame(state.total_duration, state.current_frame, 1);
|
||||||
|
|
||||||
|
case DocumentAnimationPanelAction::previous_frame:
|
||||||
|
return plan_animation_step_frame(state.total_duration, state.current_frame, -1);
|
||||||
|
|
||||||
|
case DocumentAnimationPanelAction::playback_step:
|
||||||
|
return plan_animation_playback_step(state.total_duration, state.current_frame, 1);
|
||||||
|
|
||||||
|
case DocumentAnimationPanelAction::toggle_playback:
|
||||||
|
return plan_animation_playback_toggle(state.playback_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("unknown animation panel action"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_animation_operation_plan(
|
||||||
|
const DocumentAnimationOperationPlan& plan) noexcept
|
||||||
|
{
|
||||||
|
switch (plan.operation) {
|
||||||
|
case DocumentAnimationOperation::add_frame:
|
||||||
|
if (!plan.mutates_document || !plan.marks_unsaved) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation add plan must mutate the document");
|
||||||
|
}
|
||||||
|
return validate_animation_frame_count(plan.frame_count);
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::duplicate_frame:
|
||||||
|
case DocumentAnimationOperation::remove_frame:
|
||||||
|
if (!plan.requires_selected_frame || !plan.mutates_document || !plan.marks_unsaved) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation selected-frame plan must mutate the document");
|
||||||
|
}
|
||||||
|
if (plan.operation == DocumentAnimationOperation::remove_frame && plan.frame_count <= 1) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation layer must keep at least one frame");
|
||||||
|
}
|
||||||
|
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::adjust_duration:
|
||||||
|
if (!plan.requires_selected_frame) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation duration plan must require a selected frame");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validate_animation_frame_duration(plan.frame_duration);
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::move_frame:
|
||||||
|
if (!plan.requires_selected_frame || plan.move_offset == 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation move plan must require selected frame and non-zero offset");
|
||||||
|
}
|
||||||
|
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::select_frame:
|
||||||
|
if (!plan.requires_selected_frame || !plan.updates_canvas_animation || plan.layer_index < 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation frame select plan has invalid state");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::goto_frame:
|
||||||
|
case DocumentAnimationOperation::goto_next:
|
||||||
|
case DocumentAnimationOperation::goto_previous:
|
||||||
|
case DocumentAnimationOperation::playback_step:
|
||||||
|
if (!plan.updates_canvas_animation) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation goto plan must update canvas animation");
|
||||||
|
}
|
||||||
|
if (plan.operation == DocumentAnimationOperation::playback_step && plan.move_offset == 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation playback step offset must not be zero");
|
||||||
|
}
|
||||||
|
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::toggle_playback:
|
||||||
|
if (plan.playback_active == plan.playback_was_active) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation playback toggle must change state");
|
||||||
|
}
|
||||||
|
if (plan.playback_idle_ms <= 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation playback idle interval must be positive");
|
||||||
|
}
|
||||||
|
if (plan.playback_active && !plan.resets_playback_timer) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation playback start must reset timer");
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::set_onion_size:
|
||||||
|
if (plan.onion_size < 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation onion size must not be negative");
|
||||||
|
}
|
||||||
|
if (!plan.updates_canvas_animation) {
|
||||||
|
return pp::foundation::Status::invalid_argument("animation onion plan must update canvas animation");
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown animation operation");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_animation_operation_plan(
|
||||||
|
const DocumentAnimationOperationPlan& plan,
|
||||||
|
DocumentAnimationServices& services)
|
||||||
|
{
|
||||||
|
const auto validation = validate_animation_operation_plan(plan);
|
||||||
|
if (!validation.ok()) {
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (plan.operation) {
|
||||||
|
case DocumentAnimationOperation::add_frame:
|
||||||
|
services.add_frame();
|
||||||
|
services.mark_unsaved();
|
||||||
|
services.update_canvas_animation();
|
||||||
|
services.reload_animation_layers();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::duplicate_frame:
|
||||||
|
services.duplicate_frame(plan.selected_frame);
|
||||||
|
services.mark_unsaved();
|
||||||
|
services.update_canvas_animation();
|
||||||
|
services.reload_animation_layers();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::remove_frame:
|
||||||
|
services.remove_frame(plan.selected_frame, plan.target_frame);
|
||||||
|
services.mark_unsaved();
|
||||||
|
if (plan.updates_canvas_animation) {
|
||||||
|
services.goto_frame(plan.target_frame);
|
||||||
|
}
|
||||||
|
if (plan.reloads_animation_layers) {
|
||||||
|
services.reload_animation_layers();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::adjust_duration:
|
||||||
|
if (plan.mutates_document) {
|
||||||
|
services.set_frame_duration(plan.selected_frame, plan.frame_duration);
|
||||||
|
services.mark_unsaved();
|
||||||
|
if (plan.updates_canvas_animation) {
|
||||||
|
services.update_canvas_animation();
|
||||||
|
}
|
||||||
|
if (plan.reloads_animation_layers) {
|
||||||
|
services.reload_animation_layers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::move_frame:
|
||||||
|
{
|
||||||
|
const auto actual_target_frame = services.move_frame(plan.selected_frame, plan.move_offset);
|
||||||
|
if (plan.marks_unsaved) {
|
||||||
|
services.mark_unsaved();
|
||||||
|
}
|
||||||
|
if (plan.updates_canvas_animation) {
|
||||||
|
services.goto_frame(actual_target_frame);
|
||||||
|
}
|
||||||
|
if (plan.reloads_animation_layers) {
|
||||||
|
services.reload_animation_layers();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
case DocumentAnimationOperation::goto_frame:
|
||||||
|
case DocumentAnimationOperation::goto_next:
|
||||||
|
case DocumentAnimationOperation::goto_previous:
|
||||||
|
services.goto_frame(plan.target_frame);
|
||||||
|
if (plan.reloads_animation_layers) {
|
||||||
|
services.reload_animation_layers();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::select_frame:
|
||||||
|
services.select_frame(plan.layer_id, plan.layer_index, plan.selected_frame);
|
||||||
|
if (plan.updates_canvas_animation) {
|
||||||
|
services.goto_frame(plan.target_frame);
|
||||||
|
}
|
||||||
|
services.select_layer(plan.layer_index);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::playback_step:
|
||||||
|
services.goto_frame(plan.target_frame);
|
||||||
|
services.set_timeline_frame(plan.target_frame);
|
||||||
|
services.update_frame_status();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::toggle_playback:
|
||||||
|
if (plan.playback_active) {
|
||||||
|
services.capture_playback_restore_mode();
|
||||||
|
services.enter_playback_camera_mode();
|
||||||
|
if (plan.resets_playback_timer) {
|
||||||
|
services.reset_playback_timer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
services.restore_playback_canvas_mode();
|
||||||
|
}
|
||||||
|
services.set_playback_active(plan.playback_active);
|
||||||
|
services.set_playback_idle_ms(plan.playback_idle_ms);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case DocumentAnimationOperation::set_onion_size:
|
||||||
|
services.set_onion_size(plan.onion_size);
|
||||||
|
if (plan.updates_canvas_animation) {
|
||||||
|
services.update_canvas_animation();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown animation operation");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
85
src/app_core/document_canvas.h
Normal file
85
src/app_core/document_canvas.h
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
struct DocumentCanvasClearPlan {
|
||||||
|
float r = 0.0F;
|
||||||
|
float g = 0.0F;
|
||||||
|
float b = 0.0F;
|
||||||
|
float a = 0.0F;
|
||||||
|
bool clears_canvas = false;
|
||||||
|
bool records_undo = false;
|
||||||
|
bool marks_unsaved = false;
|
||||||
|
bool no_op = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DocumentCanvasClearServices {
|
||||||
|
public:
|
||||||
|
virtual ~DocumentCanvasClearServices() = default;
|
||||||
|
|
||||||
|
virtual void clear_current_canvas(float r, float g, float b, float a) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_clear_color_channel(float value) noexcept
|
||||||
|
{
|
||||||
|
if (!std::isfinite(value)) {
|
||||||
|
return pp::foundation::Status::invalid_argument("clear color channel must be finite");
|
||||||
|
}
|
||||||
|
if (value < 0.0F || value > 1.0F) {
|
||||||
|
return pp::foundation::Status::out_of_range("clear color channel must be within 0..1");
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
|
||||||
|
bool has_canvas,
|
||||||
|
float r = 0.0F,
|
||||||
|
float g = 0.0F,
|
||||||
|
float b = 0.0F,
|
||||||
|
float a = 0.0F) noexcept
|
||||||
|
{
|
||||||
|
const float channels[] { r, g, b, a };
|
||||||
|
for (const float channel : channels) {
|
||||||
|
const auto status = validate_clear_color_channel(channel);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentCanvasClearPlan>::failure(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentCanvasClearPlan plan;
|
||||||
|
plan.r = r;
|
||||||
|
plan.g = g;
|
||||||
|
plan.b = b;
|
||||||
|
plan.a = a;
|
||||||
|
plan.clears_canvas = has_canvas;
|
||||||
|
plan.records_undo = has_canvas;
|
||||||
|
plan.marks_unsaved = has_canvas;
|
||||||
|
plan.no_op = !has_canvas;
|
||||||
|
return pp::foundation::Result<DocumentCanvasClearPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_document_canvas_clear_plan(
|
||||||
|
const DocumentCanvasClearPlan& plan,
|
||||||
|
DocumentCanvasClearServices& services)
|
||||||
|
{
|
||||||
|
const float channels[] { plan.r, plan.g, plan.b, plan.a };
|
||||||
|
for (const float channel : channels) {
|
||||||
|
const auto status = validate_clear_color_channel(channel);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.no_op || !plan.clears_canvas) {
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
services.clear_current_canvas(plan.r, plan.g, plan.b, plan.a);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
79
src/app_core/document_cloud.h
Normal file
79
src/app_core/document_cloud.h
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <limits>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class CloudUploadAction {
|
||||||
|
unavailable_no_canvas,
|
||||||
|
show_save_required_warning,
|
||||||
|
prompt_publish,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CloudBrowseAction {
|
||||||
|
unavailable_no_canvas,
|
||||||
|
show_browser,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CloudDownloadSelectionAction {
|
||||||
|
wait_for_selection,
|
||||||
|
start_download,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CloudUploadPlan {
|
||||||
|
CloudUploadAction action = CloudUploadAction::unavailable_no_canvas;
|
||||||
|
bool save_before_upload = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CloudBulkUploadPlan {
|
||||||
|
std::size_t file_count = 0;
|
||||||
|
int progress_total = 0;
|
||||||
|
bool show_progress = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr CloudUploadPlan plan_cloud_upload(
|
||||||
|
bool has_canvas,
|
||||||
|
bool is_new_document,
|
||||||
|
bool has_unsaved_changes) noexcept
|
||||||
|
{
|
||||||
|
if (!has_canvas) {
|
||||||
|
return { CloudUploadAction::unavailable_no_canvas, false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_new_document) {
|
||||||
|
return { CloudUploadAction::show_save_required_warning, false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { CloudUploadAction::prompt_publish, has_unsaved_changes };
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr CloudBrowseAction plan_cloud_browse(bool has_canvas) noexcept
|
||||||
|
{
|
||||||
|
return has_canvas
|
||||||
|
? CloudBrowseAction::show_browser
|
||||||
|
: CloudBrowseAction::unavailable_no_canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr CloudDownloadSelectionAction plan_cloud_download_selection(
|
||||||
|
std::string_view selected_file) noexcept
|
||||||
|
{
|
||||||
|
return selected_file.empty()
|
||||||
|
? CloudDownloadSelectionAction::wait_for_selection
|
||||||
|
: CloudDownloadSelectionAction::start_download;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr CloudBulkUploadPlan plan_cloud_bulk_upload(
|
||||||
|
std::size_t file_count,
|
||||||
|
bool progress_ui_available) noexcept
|
||||||
|
{
|
||||||
|
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
|
||||||
|
return {
|
||||||
|
file_count,
|
||||||
|
file_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(file_count),
|
||||||
|
progress_ui_available,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1
src/app_core/document_export.cpp
Normal file
1
src/app_core/document_export.cpp
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "app_core/document_export.h"
|
||||||
283
src/app_core/document_export.h
Normal file
283
src/app_core/document_export.h
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
struct DocumentExportFileTarget {
|
||||||
|
std::string path;
|
||||||
|
std::string suggested_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentExportCollectionTarget {
|
||||||
|
std::string directory;
|
||||||
|
std::string stem_path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentExportStemTarget {
|
||||||
|
std::string stem_path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentExportSuggestedName {
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentExportStartDecision {
|
||||||
|
start_now,
|
||||||
|
show_license_disabled,
|
||||||
|
unavailable_no_canvas,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentExportMenuKind {
|
||||||
|
jpeg,
|
||||||
|
png,
|
||||||
|
layers,
|
||||||
|
cube_faces,
|
||||||
|
depth,
|
||||||
|
animation_frames,
|
||||||
|
animation_mp4,
|
||||||
|
timelapse,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentExportMenuAction {
|
||||||
|
show_jpeg_dialog,
|
||||||
|
show_png_dialog,
|
||||||
|
show_layers_dialog,
|
||||||
|
show_cube_faces_dialog,
|
||||||
|
show_depth_dialog,
|
||||||
|
show_animation_frames_dialog,
|
||||||
|
show_animation_mp4_dialog,
|
||||||
|
show_timelapse_dialog,
|
||||||
|
show_license_disabled,
|
||||||
|
unavailable_no_canvas,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentExportMenuPlan {
|
||||||
|
DocumentExportMenuKind kind = DocumentExportMenuKind::jpeg;
|
||||||
|
DocumentExportMenuAction action = DocumentExportMenuAction::show_jpeg_dialog;
|
||||||
|
bool opens_dialog = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DocumentExportMenuServices {
|
||||||
|
public:
|
||||||
|
virtual ~DocumentExportMenuServices() = default;
|
||||||
|
|
||||||
|
virtual void show_jpeg_dialog() = 0;
|
||||||
|
virtual void show_png_dialog() = 0;
|
||||||
|
virtual void show_layers_dialog() = 0;
|
||||||
|
virtual void show_cube_faces_dialog() = 0;
|
||||||
|
virtual void show_depth_dialog() = 0;
|
||||||
|
virtual void show_animation_frames_dialog() = 0;
|
||||||
|
virtual void show_animation_mp4_dialog() = 0;
|
||||||
|
virtual void show_timelapse_dialog() = 0;
|
||||||
|
virtual void show_license_disabled() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
|
||||||
|
bool requires_license,
|
||||||
|
bool license_valid,
|
||||||
|
bool has_canvas) noexcept
|
||||||
|
{
|
||||||
|
if (requires_license && !license_valid) {
|
||||||
|
return DocumentExportStartDecision::show_license_disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return has_canvas
|
||||||
|
? DocumentExportStartDecision::start_now
|
||||||
|
: DocumentExportStartDecision::unavailable_no_canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr bool document_export_menu_requires_license(
|
||||||
|
DocumentExportMenuKind kind) noexcept
|
||||||
|
{
|
||||||
|
switch (kind) {
|
||||||
|
case DocumentExportMenuKind::animation_mp4:
|
||||||
|
case DocumentExportMenuKind::timelapse:
|
||||||
|
return true;
|
||||||
|
case DocumentExportMenuKind::jpeg:
|
||||||
|
case DocumentExportMenuKind::png:
|
||||||
|
case DocumentExportMenuKind::layers:
|
||||||
|
case DocumentExportMenuKind::cube_faces:
|
||||||
|
case DocumentExportMenuKind::depth:
|
||||||
|
case DocumentExportMenuKind::animation_frames:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr DocumentExportMenuAction document_export_menu_dialog_action(
|
||||||
|
DocumentExportMenuKind kind) noexcept
|
||||||
|
{
|
||||||
|
switch (kind) {
|
||||||
|
case DocumentExportMenuKind::jpeg:
|
||||||
|
return DocumentExportMenuAction::show_jpeg_dialog;
|
||||||
|
case DocumentExportMenuKind::png:
|
||||||
|
return DocumentExportMenuAction::show_png_dialog;
|
||||||
|
case DocumentExportMenuKind::layers:
|
||||||
|
return DocumentExportMenuAction::show_layers_dialog;
|
||||||
|
case DocumentExportMenuKind::cube_faces:
|
||||||
|
return DocumentExportMenuAction::show_cube_faces_dialog;
|
||||||
|
case DocumentExportMenuKind::depth:
|
||||||
|
return DocumentExportMenuAction::show_depth_dialog;
|
||||||
|
case DocumentExportMenuKind::animation_frames:
|
||||||
|
return DocumentExportMenuAction::show_animation_frames_dialog;
|
||||||
|
case DocumentExportMenuKind::animation_mp4:
|
||||||
|
return DocumentExportMenuAction::show_animation_mp4_dialog;
|
||||||
|
case DocumentExportMenuKind::timelapse:
|
||||||
|
return DocumentExportMenuAction::show_timelapse_dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentExportMenuAction::show_jpeg_dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr DocumentExportMenuPlan plan_document_export_menu_action(
|
||||||
|
DocumentExportMenuKind kind,
|
||||||
|
bool has_canvas,
|
||||||
|
bool license_valid) noexcept
|
||||||
|
{
|
||||||
|
DocumentExportMenuPlan plan;
|
||||||
|
plan.kind = kind;
|
||||||
|
plan.action = document_export_menu_dialog_action(kind);
|
||||||
|
|
||||||
|
const auto start = plan_document_export_start(
|
||||||
|
document_export_menu_requires_license(kind),
|
||||||
|
license_valid,
|
||||||
|
has_canvas);
|
||||||
|
if (start == DocumentExportStartDecision::show_license_disabled) {
|
||||||
|
plan.action = DocumentExportMenuAction::show_license_disabled;
|
||||||
|
plan.opens_dialog = false;
|
||||||
|
} else if (start == DocumentExportStartDecision::unavailable_no_canvas) {
|
||||||
|
plan.action = DocumentExportMenuAction::unavailable_no_canvas;
|
||||||
|
plan.opens_dialog = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentExportFileTarget> make_document_export_file_target(
|
||||||
|
std::string_view work_directory,
|
||||||
|
std::string_view document_name,
|
||||||
|
std::string_view extension)
|
||||||
|
{
|
||||||
|
if (document_name.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentExportFileTarget>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentExportFileTarget>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("extension must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentExportFileTarget target;
|
||||||
|
target.suggested_name.reserve(document_name.size() + extension.size());
|
||||||
|
target.suggested_name += document_name;
|
||||||
|
target.suggested_name += extension;
|
||||||
|
target.path.reserve(work_directory.size() + target.suggested_name.size() + 1);
|
||||||
|
target.path += work_directory;
|
||||||
|
target.path += "/";
|
||||||
|
target.path += target.suggested_name;
|
||||||
|
return pp::foundation::Result<DocumentExportFileTarget>::success(std::move(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentExportCollectionTarget> make_document_export_collection_target(
|
||||||
|
std::string_view work_directory,
|
||||||
|
std::string_view document_name,
|
||||||
|
std::string_view suffix)
|
||||||
|
{
|
||||||
|
if (document_name.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentExportCollectionTarget>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentExportCollectionTarget target;
|
||||||
|
target.directory.reserve(work_directory.size() + document_name.size() + suffix.size() + 1);
|
||||||
|
target.directory += work_directory;
|
||||||
|
target.directory += "/";
|
||||||
|
target.directory += document_name;
|
||||||
|
target.directory += suffix;
|
||||||
|
target.stem_path.reserve(target.directory.size() + document_name.size() + 1);
|
||||||
|
target.stem_path += target.directory;
|
||||||
|
target.stem_path += "/";
|
||||||
|
target.stem_path += document_name;
|
||||||
|
return pp::foundation::Result<DocumentExportCollectionTarget>::success(std::move(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentExportStemTarget> make_document_export_stem_target(
|
||||||
|
std::string_view directory,
|
||||||
|
std::string_view document_name)
|
||||||
|
{
|
||||||
|
if (document_name.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentExportStemTarget>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentExportStemTarget target;
|
||||||
|
target.stem_path.reserve(directory.size() + document_name.size() + 1);
|
||||||
|
target.stem_path += directory;
|
||||||
|
target.stem_path += "/";
|
||||||
|
target.stem_path += document_name;
|
||||||
|
return pp::foundation::Result<DocumentExportStemTarget>::success(std::move(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentExportSuggestedName> make_document_export_suggested_name(
|
||||||
|
std::string_view document_name,
|
||||||
|
std::string_view suffix)
|
||||||
|
{
|
||||||
|
if (document_name.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentExportSuggestedName>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentExportSuggestedName target;
|
||||||
|
target.name.reserve(document_name.size() + suffix.size());
|
||||||
|
target.name += document_name;
|
||||||
|
target.name += suffix;
|
||||||
|
return pp::foundation::Result<DocumentExportSuggestedName>::success(std::move(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_document_export_menu_plan(
|
||||||
|
const DocumentExportMenuPlan& plan,
|
||||||
|
DocumentExportMenuServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.action) {
|
||||||
|
case DocumentExportMenuAction::show_jpeg_dialog:
|
||||||
|
services.show_jpeg_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentExportMenuAction::show_png_dialog:
|
||||||
|
services.show_png_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentExportMenuAction::show_layers_dialog:
|
||||||
|
services.show_layers_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentExportMenuAction::show_cube_faces_dialog:
|
||||||
|
services.show_cube_faces_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentExportMenuAction::show_depth_dialog:
|
||||||
|
services.show_depth_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentExportMenuAction::show_animation_frames_dialog:
|
||||||
|
services.show_animation_frames_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentExportMenuAction::show_animation_mp4_dialog:
|
||||||
|
services.show_animation_mp4_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentExportMenuAction::show_timelapse_dialog:
|
||||||
|
services.show_timelapse_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentExportMenuAction::show_license_disabled:
|
||||||
|
services.show_license_disabled();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentExportMenuAction::unavailable_no_canvas:
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown document export menu action");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
88
src/app_core/document_import.h
Normal file
88
src/app_core/document_import.h
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class DocumentImageImportAction {
|
||||||
|
import_equirectangular,
|
||||||
|
place_transform,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentImageImportPlan {
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
DocumentImageImportAction action = DocumentImageImportAction::place_transform;
|
||||||
|
bool imports_equirectangular = false;
|
||||||
|
bool enters_transform_mode = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DocumentImageImportServices {
|
||||||
|
public:
|
||||||
|
virtual ~DocumentImageImportServices() = default;
|
||||||
|
|
||||||
|
virtual void import_equirectangular(std::string_view path) = 0;
|
||||||
|
virtual void enter_transform_import(std::string_view path) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_document_image_import_dimensions(
|
||||||
|
int width,
|
||||||
|
int height) noexcept
|
||||||
|
{
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("image dimensions must be positive");
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentImageImportPlan> plan_document_image_import(
|
||||||
|
int width,
|
||||||
|
int height) noexcept
|
||||||
|
{
|
||||||
|
const auto dimensions = validate_document_image_import_dimensions(width, height);
|
||||||
|
if (!dimensions.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentImageImportPlan>::failure(dimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto wide_equirect = static_cast<long long>(width) == static_cast<long long>(height) * 2LL;
|
||||||
|
const auto vertical_cube_strip = width == height / 6;
|
||||||
|
|
||||||
|
DocumentImageImportPlan plan;
|
||||||
|
plan.width = width;
|
||||||
|
plan.height = height;
|
||||||
|
plan.imports_equirectangular = wide_equirect || vertical_cube_strip;
|
||||||
|
plan.enters_transform_mode = !plan.imports_equirectangular;
|
||||||
|
plan.action = plan.imports_equirectangular
|
||||||
|
? DocumentImageImportAction::import_equirectangular
|
||||||
|
: DocumentImageImportAction::place_transform;
|
||||||
|
return pp::foundation::Result<DocumentImageImportPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_document_image_import_plan(
|
||||||
|
const DocumentImageImportPlan& plan,
|
||||||
|
std::string_view path,
|
||||||
|
DocumentImageImportServices& services)
|
||||||
|
{
|
||||||
|
const auto dimensions = validate_document_image_import_dimensions(plan.width, plan.height);
|
||||||
|
if (!dimensions.ok()) {
|
||||||
|
return dimensions;
|
||||||
|
}
|
||||||
|
if (path.empty()) {
|
||||||
|
return pp::foundation::Status::invalid_argument("image import path must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (plan.action) {
|
||||||
|
case DocumentImageImportAction::import_equirectangular:
|
||||||
|
services.import_equirectangular(path);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentImageImportAction::place_transform:
|
||||||
|
services.enter_transform_import(path);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown image import action");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
448
src/app_core/document_layer.h
Normal file
448
src/app_core/document_layer.h
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
inline constexpr std::size_t document_layer_name_max_length = 128;
|
||||||
|
inline constexpr int document_layer_legacy_blend_mode_count = 5;
|
||||||
|
|
||||||
|
enum class DocumentLayerRenameAction {
|
||||||
|
no_op_same_name,
|
||||||
|
rename_and_record_undo,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentLayerOperation {
|
||||||
|
add,
|
||||||
|
duplicate,
|
||||||
|
select,
|
||||||
|
reorder,
|
||||||
|
remove,
|
||||||
|
set_opacity,
|
||||||
|
set_visibility,
|
||||||
|
set_alpha_lock,
|
||||||
|
set_blend_mode,
|
||||||
|
set_highlight,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentLayerMenuCommand {
|
||||||
|
clear,
|
||||||
|
rename,
|
||||||
|
merge_down,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentLayerMenuAction {
|
||||||
|
clear_current_layer,
|
||||||
|
show_rename_dialog,
|
||||||
|
merge_with_lower_layer,
|
||||||
|
show_merge_animated_not_supported,
|
||||||
|
no_op_select_layer,
|
||||||
|
no_op_select_upper_layer,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentLayerRenamePlan {
|
||||||
|
std::string old_name;
|
||||||
|
std::string new_name;
|
||||||
|
DocumentLayerRenameAction action = DocumentLayerRenameAction::no_op_same_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentLayerOperationPlan {
|
||||||
|
DocumentLayerOperation operation = DocumentLayerOperation::select;
|
||||||
|
int index = 0;
|
||||||
|
int from_index = 0;
|
||||||
|
int to_index = 0;
|
||||||
|
int insert_index = 0;
|
||||||
|
int source_index = 0;
|
||||||
|
std::string name;
|
||||||
|
float opacity = 1.0F;
|
||||||
|
bool flag = false;
|
||||||
|
int blend_mode = 0;
|
||||||
|
bool mutates_document = false;
|
||||||
|
bool marks_unsaved = false;
|
||||||
|
bool reloads_animation_layers = false;
|
||||||
|
bool updates_title = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentLayerMenuPlan {
|
||||||
|
DocumentLayerMenuCommand command = DocumentLayerMenuCommand::clear;
|
||||||
|
DocumentLayerMenuAction action = DocumentLayerMenuAction::clear_current_layer;
|
||||||
|
std::string label;
|
||||||
|
int from_index = 0;
|
||||||
|
int to_index = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DocumentLayerMenuServices {
|
||||||
|
public:
|
||||||
|
virtual ~DocumentLayerMenuServices() = default;
|
||||||
|
|
||||||
|
virtual void clear_current_layer() = 0;
|
||||||
|
virtual void show_rename_dialog() = 0;
|
||||||
|
virtual void merge_with_lower_layer(int from_index, int to_index) = 0;
|
||||||
|
virtual void show_merge_animated_not_supported() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_layer_index(
|
||||||
|
int layer_count,
|
||||||
|
int index) noexcept
|
||||||
|
{
|
||||||
|
if (layer_count <= 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= layer_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_layer_insert_index(
|
||||||
|
int layer_count,
|
||||||
|
int index) noexcept
|
||||||
|
{
|
||||||
|
if (layer_count < 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("layer count must not be negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index > layer_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("layer insert index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerRenamePlan> plan_document_layer_rename(
|
||||||
|
std::string_view old_name,
|
||||||
|
std::string_view requested_name)
|
||||||
|
{
|
||||||
|
if (requested_name.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("layer name must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requested_name.size() > document_layer_name_max_length) {
|
||||||
|
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("layer name length exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerRenamePlan plan;
|
||||||
|
plan.old_name = std::string(old_name);
|
||||||
|
plan.new_name = std::string(requested_name);
|
||||||
|
plan.action = old_name == requested_name
|
||||||
|
? DocumentLayerRenameAction::no_op_same_name
|
||||||
|
: DocumentLayerRenameAction::rename_and_record_undo;
|
||||||
|
return pp::foundation::Result<DocumentLayerRenamePlan>::success(std::move(plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_add(
|
||||||
|
int layer_count,
|
||||||
|
int insert_index,
|
||||||
|
std::string_view name)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_insert_index(layer_count, insert_index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto rename = plan_document_layer_rename({}, name);
|
||||||
|
if (!rename) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(rename.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::add;
|
||||||
|
plan.insert_index = insert_index;
|
||||||
|
plan.name = std::string(name);
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
plan.updates_title = true;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(std::move(plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_duplicate(
|
||||||
|
int layer_count,
|
||||||
|
int source_index)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(layer_count, source_index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::duplicate;
|
||||||
|
plan.source_index = source_index;
|
||||||
|
plan.insert_index = source_index + 1;
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
plan.updates_title = true;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_select(
|
||||||
|
int layer_count,
|
||||||
|
int index)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(layer_count, index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::select;
|
||||||
|
plan.index = index;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_reorder(
|
||||||
|
int layer_count,
|
||||||
|
int from_index,
|
||||||
|
int to_index)
|
||||||
|
{
|
||||||
|
auto index_status = validate_layer_index(layer_count, from_index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
index_status = validate_layer_index(layer_count, to_index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::reorder;
|
||||||
|
plan.from_index = from_index;
|
||||||
|
plan.to_index = to_index;
|
||||||
|
plan.mutates_document = from_index != to_index;
|
||||||
|
plan.marks_unsaved = plan.mutates_document;
|
||||||
|
plan.reloads_animation_layers = plan.mutates_document;
|
||||||
|
plan.updates_title = plan.mutates_document;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_remove(
|
||||||
|
int layer_count,
|
||||||
|
int index)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(layer_count, index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer_count <= 1) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document must keep at least one layer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::remove;
|
||||||
|
plan.index = index;
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
plan.updates_title = true;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_opacity(
|
||||||
|
int layer_count,
|
||||||
|
int index,
|
||||||
|
float opacity)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(layer_count, index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::set_opacity;
|
||||||
|
plan.index = index;
|
||||||
|
plan.opacity = opacity;
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
plan.updates_title = true;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_visibility(
|
||||||
|
int layer_count,
|
||||||
|
int index,
|
||||||
|
bool visible)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(layer_count, index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::set_visibility;
|
||||||
|
plan.index = index;
|
||||||
|
plan.flag = visible;
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
plan.reloads_animation_layers = true;
|
||||||
|
plan.updates_title = true;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_alpha_lock(
|
||||||
|
int layer_count,
|
||||||
|
int index,
|
||||||
|
bool locked)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(layer_count, index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::set_alpha_lock;
|
||||||
|
plan.index = index;
|
||||||
|
plan.flag = locked;
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
plan.updates_title = true;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_blend_mode(
|
||||||
|
int layer_count,
|
||||||
|
int index,
|
||||||
|
int blend_mode)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(layer_count, index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blend_mode < 0 || blend_mode >= document_layer_legacy_blend_mode_count) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::set_blend_mode;
|
||||||
|
plan.index = index;
|
||||||
|
plan.blend_mode = blend_mode;
|
||||||
|
plan.mutates_document = true;
|
||||||
|
plan.marks_unsaved = true;
|
||||||
|
plan.updates_title = true;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_highlight(
|
||||||
|
int layer_count,
|
||||||
|
int index,
|
||||||
|
bool highlight)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(layer_count, index);
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerOperationPlan plan;
|
||||||
|
plan.operation = DocumentLayerOperation::set_highlight;
|
||||||
|
plan.index = index;
|
||||||
|
plan.flag = highlight;
|
||||||
|
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentLayerMenuPlan> plan_document_layer_menu(
|
||||||
|
DocumentLayerMenuCommand command,
|
||||||
|
bool has_current_layer,
|
||||||
|
int current_index,
|
||||||
|
int animation_duration,
|
||||||
|
std::string_view current_layer_name,
|
||||||
|
std::string_view lower_layer_name)
|
||||||
|
{
|
||||||
|
if (current_index < 0) {
|
||||||
|
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("current layer index must not be negative"));
|
||||||
|
}
|
||||||
|
if (animation_duration < 0) {
|
||||||
|
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("animation duration must not be negative"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerMenuPlan plan;
|
||||||
|
plan.command = command;
|
||||||
|
plan.from_index = current_index;
|
||||||
|
plan.to_index = current_index > 0 ? current_index - 1 : 0;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case DocumentLayerMenuCommand::clear:
|
||||||
|
plan.action = has_current_layer
|
||||||
|
? DocumentLayerMenuAction::clear_current_layer
|
||||||
|
: DocumentLayerMenuAction::no_op_select_layer;
|
||||||
|
plan.label = has_current_layer
|
||||||
|
? "Clear Layer " + std::string(current_layer_name)
|
||||||
|
: "Clear Layer (Select a layer)";
|
||||||
|
break;
|
||||||
|
case DocumentLayerMenuCommand::rename:
|
||||||
|
plan.action = has_current_layer
|
||||||
|
? DocumentLayerMenuAction::show_rename_dialog
|
||||||
|
: DocumentLayerMenuAction::no_op_select_layer;
|
||||||
|
plan.label = has_current_layer
|
||||||
|
? "Rename Layer " + std::string(current_layer_name)
|
||||||
|
: "Rename Layer (Select a layer)";
|
||||||
|
break;
|
||||||
|
case DocumentLayerMenuCommand::merge_down:
|
||||||
|
if (!has_current_layer) {
|
||||||
|
plan.action = DocumentLayerMenuAction::no_op_select_layer;
|
||||||
|
plan.label = "Merge Layer (Select a layer)";
|
||||||
|
} else if (animation_duration > 1) {
|
||||||
|
plan.action = DocumentLayerMenuAction::show_merge_animated_not_supported;
|
||||||
|
plan.label = "Merge Layer (Animation not supported)";
|
||||||
|
} else if (current_index <= 0) {
|
||||||
|
plan.action = DocumentLayerMenuAction::no_op_select_upper_layer;
|
||||||
|
plan.label = "Merge Layer (Select upper layers)";
|
||||||
|
} else {
|
||||||
|
plan.action = DocumentLayerMenuAction::merge_with_lower_layer;
|
||||||
|
plan.label = "Merge with " + std::string(lower_layer_name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<DocumentLayerMenuPlan>::success(std::move(plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_document_layer_menu_plan(
|
||||||
|
const DocumentLayerMenuPlan& plan,
|
||||||
|
DocumentLayerMenuServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.action) {
|
||||||
|
case DocumentLayerMenuAction::clear_current_layer:
|
||||||
|
services.clear_current_layer();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentLayerMenuAction::show_rename_dialog:
|
||||||
|
services.show_rename_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentLayerMenuAction::merge_with_lower_layer:
|
||||||
|
services.merge_with_lower_layer(plan.from_index, plan.to_index);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentLayerMenuAction::show_merge_animated_not_supported:
|
||||||
|
services.show_merge_animated_not_supported();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case DocumentLayerMenuAction::no_op_select_layer:
|
||||||
|
case DocumentLayerMenuAction::no_op_select_upper_layer:
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown document layer menu action");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
73
src/app_core/document_platform_io.h
Normal file
73
src/app_core/document_platform_io.h
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class PickedPathAction {
|
||||||
|
ignore_empty_path,
|
||||||
|
invoke_callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DisplayFileAction {
|
||||||
|
ignore_empty_path,
|
||||||
|
open_external_file,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class VirtualKeyboardAction {
|
||||||
|
show_keyboard,
|
||||||
|
hide_keyboard,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CursorVisibilityAction {
|
||||||
|
show_cursor,
|
||||||
|
hide_cursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ClipboardReadAction {
|
||||||
|
read_text,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ClipboardWriteAction {
|
||||||
|
write_text,
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept
|
||||||
|
{
|
||||||
|
return path.empty()
|
||||||
|
? PickedPathAction::ignore_empty_path
|
||||||
|
: PickedPathAction::invoke_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr DisplayFileAction plan_display_file(std::string_view path) noexcept
|
||||||
|
{
|
||||||
|
return path.empty()
|
||||||
|
? DisplayFileAction::ignore_empty_path
|
||||||
|
: DisplayFileAction::open_external_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr VirtualKeyboardAction plan_virtual_keyboard(bool visible) noexcept
|
||||||
|
{
|
||||||
|
return visible
|
||||||
|
? VirtualKeyboardAction::show_keyboard
|
||||||
|
: VirtualKeyboardAction::hide_keyboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr CursorVisibilityAction plan_cursor_visibility(bool visible) noexcept
|
||||||
|
{
|
||||||
|
return visible
|
||||||
|
? CursorVisibilityAction::show_cursor
|
||||||
|
: CursorVisibilityAction::hide_cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr ClipboardReadAction plan_clipboard_read() noexcept
|
||||||
|
{
|
||||||
|
return ClipboardReadAction::read_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr ClipboardWriteAction plan_clipboard_write(std::string_view) noexcept
|
||||||
|
{
|
||||||
|
return ClipboardWriteAction::write_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
63
src/app_core/document_recording.h
Normal file
63
src/app_core/document_recording.h
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class RecordingStartAction {
|
||||||
|
start_thread,
|
||||||
|
no_op_already_running,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class RecordingStopAction {
|
||||||
|
stop_thread,
|
||||||
|
no_op_not_running,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RecordingClearPlan {
|
||||||
|
bool stop_running_recording = false;
|
||||||
|
bool delete_recorded_files = false;
|
||||||
|
int frame_count_after_clear = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RecordingExportPlan {
|
||||||
|
std::size_t frame_count = 0;
|
||||||
|
int progress_total = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr RecordingStartAction plan_recording_start(bool is_running) noexcept
|
||||||
|
{
|
||||||
|
return is_running
|
||||||
|
? RecordingStartAction::no_op_already_running
|
||||||
|
: RecordingStartAction::start_thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr RecordingStopAction plan_recording_stop(bool is_running) noexcept
|
||||||
|
{
|
||||||
|
return is_running
|
||||||
|
? RecordingStopAction::stop_thread
|
||||||
|
: RecordingStopAction::no_op_not_running;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr RecordingClearPlan plan_recording_clear(
|
||||||
|
bool is_running,
|
||||||
|
bool platform_deletes_recorded_files) noexcept
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
is_running,
|
||||||
|
platform_deletes_recorded_files,
|
||||||
|
0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr RecordingExportPlan plan_recording_export(std::size_t frame_count) noexcept
|
||||||
|
{
|
||||||
|
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
|
||||||
|
return {
|
||||||
|
frame_count,
|
||||||
|
frame_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(frame_count),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
82
src/app_core/document_resize.h
Normal file
82
src/app_core/document_resize.h
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_core/app_status.h"
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
struct DocumentResizeDialogState {
|
||||||
|
int current_resolution = 0;
|
||||||
|
std::string current_resolution_text;
|
||||||
|
int current_resolution_index = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentResizePlan {
|
||||||
|
int resolution = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
bool clears_history = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DocumentResizeServices {
|
||||||
|
public:
|
||||||
|
virtual ~DocumentResizeServices() = default;
|
||||||
|
|
||||||
|
virtual void resize_document(int width, int height) = 0;
|
||||||
|
virtual void update_title() = 0;
|
||||||
|
virtual void clear_history() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline DocumentResizeDialogState make_document_resize_dialog_state(
|
||||||
|
int current_resolution)
|
||||||
|
{
|
||||||
|
const auto label = document_resolution_label(current_resolution);
|
||||||
|
const auto index = document_resolution_to_index(current_resolution);
|
||||||
|
std::string text = "Current: ";
|
||||||
|
text.append(label ? std::string_view(label.value()) : std::string_view("unknown"));
|
||||||
|
|
||||||
|
return {
|
||||||
|
current_resolution,
|
||||||
|
text,
|
||||||
|
index ? static_cast<int>(index.value()) : static_cast<int>(document_resolution_values.size()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentResizePlan> plan_document_resize(
|
||||||
|
int selected_resolution_index)
|
||||||
|
{
|
||||||
|
const auto resolution = display_resolution_from_index(selected_resolution_index);
|
||||||
|
if (!resolution) {
|
||||||
|
return pp::foundation::Result<DocumentResizePlan>::failure(resolution.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto value = resolution.value();
|
||||||
|
return pp::foundation::Result<DocumentResizePlan>::success(
|
||||||
|
DocumentResizePlan {
|
||||||
|
value,
|
||||||
|
value,
|
||||||
|
value,
|
||||||
|
true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_document_resize_plan(
|
||||||
|
const DocumentResizePlan& plan,
|
||||||
|
DocumentResizeServices& services)
|
||||||
|
{
|
||||||
|
if (plan.width <= 0 || plan.height <= 0) {
|
||||||
|
return pp::foundation::Status::out_of_range("resize dimensions must be positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
services.resize_document(plan.width, plan.height);
|
||||||
|
services.update_title();
|
||||||
|
if (plan.clears_history) {
|
||||||
|
services.clear_history();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
67
src/app_core/document_route.cpp
Normal file
67
src/app_core/document_route.cpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#include "app_core/document_route.h"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] bool is_extension_char(char value) noexcept
|
||||||
|
{
|
||||||
|
const auto ch = static_cast<unsigned char>(value);
|
||||||
|
return std::isalnum(ch) != 0 || value == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::string lowercase_ascii(std::string_view value)
|
||||||
|
{
|
||||||
|
std::string lowered;
|
||||||
|
lowered.reserve(value.size());
|
||||||
|
for (const char ch : value) {
|
||||||
|
lowered.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||||
|
}
|
||||||
|
return lowered;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(std::string_view path)
|
||||||
|
{
|
||||||
|
const auto separator = path.find_last_of("/\\");
|
||||||
|
if (separator == std::string_view::npos || separator + 1U >= path.size()) {
|
||||||
|
return pp::foundation::Result<DocumentOpenRoute>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document path must include a directory and file name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto dot = path.find_last_of('.');
|
||||||
|
if (dot == std::string_view::npos || dot <= separator + 1U || dot + 1U >= path.size()) {
|
||||||
|
return pp::foundation::Result<DocumentOpenRoute>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document path must include a file extension"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string_view extension = path.substr(dot + 1U);
|
||||||
|
for (const char ch : extension) {
|
||||||
|
if (!is_extension_char(ch)) {
|
||||||
|
return pp::foundation::Result<DocumentOpenRoute>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document extension contains unsupported characters"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto lowered_extension = lowercase_ascii(extension);
|
||||||
|
auto kind = DocumentOpenKind::open_project;
|
||||||
|
if (lowered_extension == "abr") {
|
||||||
|
kind = DocumentOpenKind::import_abr;
|
||||||
|
} else if (lowered_extension == "ppbr") {
|
||||||
|
kind = DocumentOpenKind::import_ppbr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<DocumentOpenRoute>::success(
|
||||||
|
DocumentOpenRoute {
|
||||||
|
.kind = kind,
|
||||||
|
.path = std::string(path),
|
||||||
|
.directory = std::string(path.substr(0U, separator)),
|
||||||
|
.name = std::string(path.substr(separator + 1U, dot - separator - 1U)),
|
||||||
|
.extension = std::move(lowered_extension),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
src/app_core/document_route.h
Normal file
27
src/app_core/document_route.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class DocumentOpenKind {
|
||||||
|
import_abr,
|
||||||
|
import_ppbr,
|
||||||
|
open_project,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentOpenRoute {
|
||||||
|
DocumentOpenKind kind = DocumentOpenKind::open_project;
|
||||||
|
std::string path;
|
||||||
|
std::string directory;
|
||||||
|
std::string name;
|
||||||
|
std::string extension;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(
|
||||||
|
std::string_view path);
|
||||||
|
|
||||||
|
}
|
||||||
1
src/app_core/document_session.cpp
Normal file
1
src/app_core/document_session.cpp
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "app_core/document_session.h"
|
||||||
323
src/app_core/document_session.h
Normal file
323
src/app_core/document_session.h
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_core/document_route.h"
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class ProjectOpenDecision {
|
||||||
|
open_now,
|
||||||
|
prompt_discard_unsaved,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CloseRequestDecision {
|
||||||
|
close_now,
|
||||||
|
show_unsaved_prompt,
|
||||||
|
wait_for_existing_prompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentSaveIntent {
|
||||||
|
save,
|
||||||
|
save_as,
|
||||||
|
save_version,
|
||||||
|
save_dirty_version,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentSaveDecision {
|
||||||
|
no_op,
|
||||||
|
show_save_dialog,
|
||||||
|
save_existing,
|
||||||
|
save_version,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentWorkflowDecision {
|
||||||
|
unavailable,
|
||||||
|
continue_now,
|
||||||
|
prompt_save_before_continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentFileWriteDecision {
|
||||||
|
save_now,
|
||||||
|
prompt_overwrite,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DocumentOpenPlanAction {
|
||||||
|
open_project_now,
|
||||||
|
prompt_discard_unsaved_project,
|
||||||
|
prompt_import_abr,
|
||||||
|
prompt_import_ppbr,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentFileTarget {
|
||||||
|
std::string name;
|
||||||
|
std::string directory;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentVersionTarget {
|
||||||
|
std::string name;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentFileSavePlan {
|
||||||
|
DocumentFileTarget target;
|
||||||
|
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NewDocumentPlan {
|
||||||
|
DocumentFileTarget target;
|
||||||
|
int resolution = 0;
|
||||||
|
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept
|
||||||
|
{
|
||||||
|
return has_unsaved_changes
|
||||||
|
? ProjectOpenDecision::prompt_discard_unsaved
|
||||||
|
: ProjectOpenDecision::open_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr DocumentOpenPlanAction plan_document_open(
|
||||||
|
DocumentOpenKind kind,
|
||||||
|
bool has_unsaved_changes) noexcept
|
||||||
|
{
|
||||||
|
switch (kind) {
|
||||||
|
case DocumentOpenKind::import_abr:
|
||||||
|
return DocumentOpenPlanAction::prompt_import_abr;
|
||||||
|
case DocumentOpenKind::import_ppbr:
|
||||||
|
return DocumentOpenPlanAction::prompt_import_ppbr;
|
||||||
|
case DocumentOpenKind::open_project:
|
||||||
|
return has_unsaved_changes
|
||||||
|
? DocumentOpenPlanAction::prompt_discard_unsaved_project
|
||||||
|
: DocumentOpenPlanAction::open_project_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentOpenPlanAction::open_project_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr CloseRequestDecision plan_close_request(
|
||||||
|
bool has_unsaved_changes,
|
||||||
|
bool close_prompt_already_open) noexcept
|
||||||
|
{
|
||||||
|
if (!has_unsaved_changes) {
|
||||||
|
return CloseRequestDecision::close_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
return close_prompt_already_open
|
||||||
|
? CloseRequestDecision::wait_for_existing_prompt
|
||||||
|
: CloseRequestDecision::show_unsaved_prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr DocumentSaveDecision plan_document_save(
|
||||||
|
bool is_new_document,
|
||||||
|
bool has_unsaved_changes,
|
||||||
|
DocumentSaveIntent intent) noexcept
|
||||||
|
{
|
||||||
|
switch (intent) {
|
||||||
|
case DocumentSaveIntent::save:
|
||||||
|
if (is_new_document) {
|
||||||
|
return DocumentSaveDecision::show_save_dialog;
|
||||||
|
}
|
||||||
|
return has_unsaved_changes
|
||||||
|
? DocumentSaveDecision::save_existing
|
||||||
|
: DocumentSaveDecision::no_op;
|
||||||
|
case DocumentSaveIntent::save_as:
|
||||||
|
return DocumentSaveDecision::show_save_dialog;
|
||||||
|
case DocumentSaveIntent::save_version:
|
||||||
|
return is_new_document
|
||||||
|
? DocumentSaveDecision::show_save_dialog
|
||||||
|
: DocumentSaveDecision::save_version;
|
||||||
|
case DocumentSaveIntent::save_dirty_version:
|
||||||
|
if (is_new_document) {
|
||||||
|
return DocumentSaveDecision::show_save_dialog;
|
||||||
|
}
|
||||||
|
return has_unsaved_changes
|
||||||
|
? DocumentSaveDecision::save_version
|
||||||
|
: DocumentSaveDecision::no_op;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentSaveDecision::no_op;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr DocumentWorkflowDecision plan_document_workflow(
|
||||||
|
bool has_canvas,
|
||||||
|
bool has_unsaved_changes) noexcept
|
||||||
|
{
|
||||||
|
if (!has_canvas) {
|
||||||
|
return DocumentWorkflowDecision::unavailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return has_unsaved_changes
|
||||||
|
? DocumentWorkflowDecision::prompt_save_before_continue
|
||||||
|
: DocumentWorkflowDecision::continue_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentFileTarget> make_document_file_target(
|
||||||
|
std::string_view work_directory,
|
||||||
|
std::string_view document_name)
|
||||||
|
{
|
||||||
|
if (document_name.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentFileTarget>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFileTarget target;
|
||||||
|
target.name = std::string(document_name);
|
||||||
|
target.directory = std::string(work_directory);
|
||||||
|
target.path.reserve(target.directory.size() + target.name.size() + 5);
|
||||||
|
target.path += target.directory;
|
||||||
|
target.path += "/";
|
||||||
|
target.path += target.name;
|
||||||
|
target.path += ".ppi";
|
||||||
|
return pp::foundation::Result<DocumentFileTarget>::success(std::move(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr DocumentFileWriteDecision plan_document_file_write(
|
||||||
|
bool target_exists) noexcept
|
||||||
|
{
|
||||||
|
return target_exists
|
||||||
|
? DocumentFileWriteDecision::prompt_overwrite
|
||||||
|
: DocumentFileWriteDecision::save_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ExistsPredicate>
|
||||||
|
[[nodiscard]] pp::foundation::Result<DocumentFileSavePlan> plan_document_file_save(
|
||||||
|
std::string_view work_directory,
|
||||||
|
std::string_view document_name,
|
||||||
|
ExistsPredicate&& exists)
|
||||||
|
{
|
||||||
|
auto target = make_document_file_target(work_directory, document_name);
|
||||||
|
if (!target) {
|
||||||
|
return pp::foundation::Result<DocumentFileSavePlan>::failure(target.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFileSavePlan plan;
|
||||||
|
plan.target = std::move(target.value());
|
||||||
|
plan.write_decision = plan_document_file_write(exists(plan.target.path));
|
||||||
|
return pp::foundation::Result<DocumentFileSavePlan>::success(std::move(plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr pp::foundation::Result<int> document_resolution_from_index(int index) noexcept
|
||||||
|
{
|
||||||
|
constexpr std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
|
||||||
|
if (index < 0 || static_cast<std::size_t>(index) >= resolutions.size()) {
|
||||||
|
return pp::foundation::Result<int>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document resolution index is out of range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<int>::success(resolutions[static_cast<std::size_t>(index)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ExistsPredicate>
|
||||||
|
[[nodiscard]] pp::foundation::Result<NewDocumentPlan> plan_new_document(
|
||||||
|
std::string_view work_directory,
|
||||||
|
std::string_view document_name,
|
||||||
|
int resolution_index,
|
||||||
|
ExistsPredicate&& exists)
|
||||||
|
{
|
||||||
|
const auto resolution = document_resolution_from_index(resolution_index);
|
||||||
|
if (!resolution) {
|
||||||
|
return pp::foundation::Result<NewDocumentPlan>::failure(resolution.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto save_plan = plan_document_file_save(
|
||||||
|
work_directory,
|
||||||
|
document_name,
|
||||||
|
std::forward<ExistsPredicate>(exists));
|
||||||
|
if (!save_plan) {
|
||||||
|
return pp::foundation::Result<NewDocumentPlan>::failure(save_plan.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
NewDocumentPlan plan;
|
||||||
|
plan.target = std::move(save_plan.value().target);
|
||||||
|
plan.resolution = resolution.value();
|
||||||
|
plan.write_decision = save_plan.value().write_decision;
|
||||||
|
return pp::foundation::Result<NewDocumentPlan>::success(std::move(plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline bool has_legacy_two_character_version_suffix(std::string_view document_name) noexcept
|
||||||
|
{
|
||||||
|
const auto dot = document_name.rfind('.');
|
||||||
|
if (dot == std::string_view::npos || dot + 3 != document_name.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto is_word = [](char ch) noexcept {
|
||||||
|
return std::isalnum(static_cast<unsigned char>(ch)) != 0 || ch == '_';
|
||||||
|
};
|
||||||
|
return is_word(document_name[dot + 1]) && is_word(document_name[dot + 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline int legacy_version_number(std::string_view suffix) noexcept
|
||||||
|
{
|
||||||
|
int value = 0;
|
||||||
|
for (const char ch : suffix) {
|
||||||
|
if (ch < '0' || ch > '9') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value * 10 + (ch - '0');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::string make_legacy_version_name(std::string_view base_name, int version)
|
||||||
|
{
|
||||||
|
char suffix[4] {};
|
||||||
|
std::snprintf(suffix, sizeof(suffix), ".%02d", version);
|
||||||
|
std::string name;
|
||||||
|
name.reserve(base_name.size() + 3);
|
||||||
|
name += base_name;
|
||||||
|
name += suffix;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ExistsPredicate>
|
||||||
|
[[nodiscard]] pp::foundation::Result<DocumentVersionTarget> find_next_document_version_target(
|
||||||
|
std::string_view directory,
|
||||||
|
std::string_view document_name,
|
||||||
|
ExistsPredicate&& exists)
|
||||||
|
{
|
||||||
|
if (directory.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("directory must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document_name.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
int current = 0;
|
||||||
|
std::string_view base = document_name;
|
||||||
|
if (has_legacy_two_character_version_suffix(document_name)) {
|
||||||
|
const auto dot = document_name.rfind('.');
|
||||||
|
base = document_name.substr(0, dot);
|
||||||
|
current = legacy_version_number(document_name.substr(dot + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int version = current + 1; version < 99; ++version) {
|
||||||
|
DocumentVersionTarget target;
|
||||||
|
target.name = make_legacy_version_name(base, version);
|
||||||
|
target.path.reserve(directory.size() + target.name.size() + 5);
|
||||||
|
target.path += directory;
|
||||||
|
target.path += "/";
|
||||||
|
target.path += target.name;
|
||||||
|
target.path += ".ppi";
|
||||||
|
if (!exists(target.path)) {
|
||||||
|
return pp::foundation::Result<DocumentVersionTarget>::success(std::move(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("no available document version target"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
src/app_core/document_sharing.h
Normal file
19
src/app_core/document_sharing.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class DocumentShareAction {
|
||||||
|
show_save_required_warning,
|
||||||
|
share_now,
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr DocumentShareAction plan_document_share(std::string_view path) noexcept
|
||||||
|
{
|
||||||
|
return path.empty()
|
||||||
|
? DocumentShareAction::show_save_required_warning
|
||||||
|
: DocumentShareAction::share_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
209
src/app_core/file_menu.h
Normal file
209
src/app_core/file_menu.h
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_core/document_export.h"
|
||||||
|
#include "app_core/document_session.h"
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class FileMenuCommand {
|
||||||
|
new_document,
|
||||||
|
import_image,
|
||||||
|
open_project,
|
||||||
|
browse_cloud,
|
||||||
|
save,
|
||||||
|
save_as,
|
||||||
|
save_version,
|
||||||
|
export_jpeg,
|
||||||
|
export_submenu,
|
||||||
|
share,
|
||||||
|
resize,
|
||||||
|
cloud_upload,
|
||||||
|
cloud_browse,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class FileMenuAction {
|
||||||
|
show_new_document_dialog,
|
||||||
|
pick_image_for_import,
|
||||||
|
pick_project_file,
|
||||||
|
show_cloud_browser_dialog,
|
||||||
|
save_document,
|
||||||
|
show_export_jpeg_dialog,
|
||||||
|
show_export_submenu,
|
||||||
|
share_document,
|
||||||
|
show_resize_dialog,
|
||||||
|
upload_to_cloud,
|
||||||
|
browse_cloud_documents,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileMenuPlan {
|
||||||
|
FileMenuCommand command = FileMenuCommand::new_document;
|
||||||
|
FileMenuAction action = FileMenuAction::show_new_document_dialog;
|
||||||
|
DocumentSaveIntent save_intent = DocumentSaveIntent::save;
|
||||||
|
DocumentExportMenuKind export_kind = DocumentExportMenuKind::jpeg;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileMenuServices {
|
||||||
|
public:
|
||||||
|
virtual ~FileMenuServices() = default;
|
||||||
|
|
||||||
|
virtual void show_new_document_dialog() = 0;
|
||||||
|
virtual void pick_image_for_import() = 0;
|
||||||
|
virtual void pick_project_file() = 0;
|
||||||
|
virtual void show_cloud_browser_dialog() = 0;
|
||||||
|
virtual void save_document(DocumentSaveIntent intent) = 0;
|
||||||
|
virtual void show_export_jpeg_dialog(DocumentExportMenuKind kind) = 0;
|
||||||
|
virtual void show_export_submenu() = 0;
|
||||||
|
virtual void share_document() = 0;
|
||||||
|
virtual void show_resize_dialog() = 0;
|
||||||
|
virtual void upload_to_cloud() = 0;
|
||||||
|
virtual void browse_cloud_documents() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr FileMenuPlan plan_file_menu_command(FileMenuCommand command) noexcept
|
||||||
|
{
|
||||||
|
FileMenuPlan plan;
|
||||||
|
plan.command = command;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case FileMenuCommand::new_document:
|
||||||
|
plan.action = FileMenuAction::show_new_document_dialog;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::import_image:
|
||||||
|
plan.action = FileMenuAction::pick_image_for_import;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::open_project:
|
||||||
|
plan.action = FileMenuAction::pick_project_file;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::browse_cloud:
|
||||||
|
plan.action = FileMenuAction::show_cloud_browser_dialog;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::save:
|
||||||
|
plan.action = FileMenuAction::save_document;
|
||||||
|
plan.save_intent = DocumentSaveIntent::save;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::save_as:
|
||||||
|
plan.action = FileMenuAction::save_document;
|
||||||
|
plan.save_intent = DocumentSaveIntent::save_as;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::save_version:
|
||||||
|
plan.action = FileMenuAction::save_document;
|
||||||
|
plan.save_intent = DocumentSaveIntent::save_version;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::export_jpeg:
|
||||||
|
plan.action = FileMenuAction::show_export_jpeg_dialog;
|
||||||
|
plan.export_kind = DocumentExportMenuKind::jpeg;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::export_submenu:
|
||||||
|
plan.action = FileMenuAction::show_export_submenu;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::share:
|
||||||
|
plan.action = FileMenuAction::share_document;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::resize:
|
||||||
|
plan.action = FileMenuAction::show_resize_dialog;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::cloud_upload:
|
||||||
|
plan.action = FileMenuAction::upload_to_cloud;
|
||||||
|
break;
|
||||||
|
case FileMenuCommand::cloud_browse:
|
||||||
|
plan.action = FileMenuAction::browse_cloud_documents;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<FileMenuCommand> parse_file_menu_command(
|
||||||
|
std::string_view command) noexcept
|
||||||
|
{
|
||||||
|
if (command == "new" || command == "new-document") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::new_document);
|
||||||
|
}
|
||||||
|
if (command == "import" || command == "import-image") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::import_image);
|
||||||
|
}
|
||||||
|
if (command == "open" || command == "open-project") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::open_project);
|
||||||
|
}
|
||||||
|
if (command == "browse") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::browse_cloud);
|
||||||
|
}
|
||||||
|
if (command == "save") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save);
|
||||||
|
}
|
||||||
|
if (command == "save-as") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_as);
|
||||||
|
}
|
||||||
|
if (command == "save-version") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_version);
|
||||||
|
}
|
||||||
|
if (command == "export" || command == "export-jpeg") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_jpeg);
|
||||||
|
}
|
||||||
|
if (command == "export-submenu") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_submenu);
|
||||||
|
}
|
||||||
|
if (command == "share") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::share);
|
||||||
|
}
|
||||||
|
if (command == "resize") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::resize);
|
||||||
|
}
|
||||||
|
if (command == "cloud-upload") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_upload);
|
||||||
|
}
|
||||||
|
if (command == "cloud-browse") {
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_browse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<FileMenuCommand>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("unknown file menu command"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_file_menu_plan(
|
||||||
|
const FileMenuPlan& plan,
|
||||||
|
FileMenuServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.action) {
|
||||||
|
case FileMenuAction::show_new_document_dialog:
|
||||||
|
services.show_new_document_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::pick_image_for_import:
|
||||||
|
services.pick_image_for_import();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::pick_project_file:
|
||||||
|
services.pick_project_file();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::show_cloud_browser_dialog:
|
||||||
|
services.show_cloud_browser_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::save_document:
|
||||||
|
services.save_document(plan.save_intent);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::show_export_jpeg_dialog:
|
||||||
|
services.show_export_jpeg_dialog(plan.export_kind);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::show_export_submenu:
|
||||||
|
services.show_export_submenu();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::share_document:
|
||||||
|
services.share_document();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::show_resize_dialog:
|
||||||
|
services.show_resize_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::upload_to_cloud:
|
||||||
|
services.upload_to_cloud();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case FileMenuAction::browse_cloud_documents:
|
||||||
|
services.browse_cloud_documents();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown file menu action");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
145
src/app_core/grid_ui.h
Normal file
145
src/app_core/grid_ui.h
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class GridUiOperation {
|
||||||
|
request_heightmap_pick,
|
||||||
|
load_heightmap,
|
||||||
|
clear_heightmap,
|
||||||
|
reload_heightmap,
|
||||||
|
render_lightmap,
|
||||||
|
commit_heightmap,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GridUiPlan {
|
||||||
|
GridUiOperation operation = GridUiOperation::request_heightmap_pick;
|
||||||
|
std::string path;
|
||||||
|
int texture_resolution = 0;
|
||||||
|
int sample_count = 0;
|
||||||
|
bool opens_picker = false;
|
||||||
|
bool loads_heightmap = false;
|
||||||
|
bool clears_heightmap = false;
|
||||||
|
bool renders_lightmap = false;
|
||||||
|
bool commits_heightmap = false;
|
||||||
|
bool updates_preview = false;
|
||||||
|
bool updates_ground_opacity = false;
|
||||||
|
bool updates_shading_mode = false;
|
||||||
|
bool shows_unsupported_message = false;
|
||||||
|
bool shows_progress = false;
|
||||||
|
bool mutates_grid_state = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_grid_texture_resolution(int texture_resolution) noexcept
|
||||||
|
{
|
||||||
|
if (texture_resolution <= 0 || texture_resolution > 16384) {
|
||||||
|
return pp::foundation::Status::out_of_range("grid texture resolution must be within 1..16384");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_grid_lightmap_samples(int sample_count) noexcept
|
||||||
|
{
|
||||||
|
if (sample_count <= 0 || sample_count > 4096) {
|
||||||
|
return pp::foundation::Status::out_of_range("grid lightmap samples must be within 1..4096");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_pick() noexcept
|
||||||
|
{
|
||||||
|
GridUiPlan plan;
|
||||||
|
plan.operation = GridUiOperation::request_heightmap_pick;
|
||||||
|
plan.opens_picker = true;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_load(std::string_view path)
|
||||||
|
{
|
||||||
|
if (path.empty()) {
|
||||||
|
return pp::foundation::Result<GridUiPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("heightmap path must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
GridUiPlan plan;
|
||||||
|
plan.operation = GridUiOperation::load_heightmap;
|
||||||
|
plan.path = std::string(path);
|
||||||
|
plan.loads_heightmap = true;
|
||||||
|
plan.updates_preview = true;
|
||||||
|
plan.updates_ground_opacity = true;
|
||||||
|
plan.mutates_grid_state = true;
|
||||||
|
return pp::foundation::Result<GridUiPlan>::success(std::move(plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_clear(bool has_heightmap) noexcept
|
||||||
|
{
|
||||||
|
GridUiPlan plan;
|
||||||
|
plan.operation = GridUiOperation::clear_heightmap;
|
||||||
|
plan.clears_heightmap = true;
|
||||||
|
plan.updates_preview = has_heightmap;
|
||||||
|
plan.mutates_grid_state = has_heightmap;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_reload(std::string_view path)
|
||||||
|
{
|
||||||
|
auto plan = plan_grid_heightmap_load(path);
|
||||||
|
if (!plan) {
|
||||||
|
return pp::foundation::Result<GridUiPlan>::failure(plan.status());
|
||||||
|
}
|
||||||
|
plan.value().operation = GridUiOperation::reload_heightmap;
|
||||||
|
plan.value().updates_ground_opacity = false;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_lightmap_render(
|
||||||
|
bool has_heightmap,
|
||||||
|
bool supports_float32,
|
||||||
|
bool supports_float16,
|
||||||
|
int texture_resolution,
|
||||||
|
int sample_count)
|
||||||
|
{
|
||||||
|
const auto texture_status = validate_grid_texture_resolution(texture_resolution);
|
||||||
|
if (!texture_status.ok()) {
|
||||||
|
return pp::foundation::Result<GridUiPlan>::failure(texture_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto sample_status = validate_grid_lightmap_samples(sample_count);
|
||||||
|
if (!sample_status.ok()) {
|
||||||
|
return pp::foundation::Result<GridUiPlan>::failure(sample_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
GridUiPlan plan;
|
||||||
|
plan.operation = GridUiOperation::render_lightmap;
|
||||||
|
plan.texture_resolution = texture_resolution;
|
||||||
|
plan.sample_count = sample_count;
|
||||||
|
if (!supports_float32 && !supports_float16) {
|
||||||
|
plan.shows_unsupported_message = true;
|
||||||
|
return pp::foundation::Result<GridUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.renders_lightmap = has_heightmap;
|
||||||
|
plan.shows_progress = has_heightmap;
|
||||||
|
plan.updates_shading_mode = has_heightmap;
|
||||||
|
plan.mutates_grid_state = has_heightmap;
|
||||||
|
return pp::foundation::Result<GridUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_commit(bool has_canvas) noexcept
|
||||||
|
{
|
||||||
|
GridUiPlan plan;
|
||||||
|
plan.operation = GridUiOperation::commit_heightmap;
|
||||||
|
plan.commits_heightmap = has_canvas;
|
||||||
|
plan.updates_ground_opacity = has_canvas;
|
||||||
|
plan.mutates_grid_state = has_canvas;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
161
src/app_core/history_ui.h
Normal file
161
src/app_core/history_ui.h
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class HistoryUiOperation {
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
clear,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HistoryUiPlan {
|
||||||
|
HistoryUiOperation operation = HistoryUiOperation::undo;
|
||||||
|
int undo_count = 0;
|
||||||
|
int redo_count = 0;
|
||||||
|
int memory_bytes = 0;
|
||||||
|
bool invokes_undo = false;
|
||||||
|
bool invokes_redo = false;
|
||||||
|
bool clears_history = false;
|
||||||
|
bool updates_memory_label = false;
|
||||||
|
bool updates_title = false;
|
||||||
|
bool no_op = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class HistoryUiServices {
|
||||||
|
public:
|
||||||
|
virtual ~HistoryUiServices() = default;
|
||||||
|
|
||||||
|
virtual void invoke_undo() = 0;
|
||||||
|
virtual void invoke_redo() = 0;
|
||||||
|
virtual void clear_history() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_history_metric(int value, const char* message) noexcept
|
||||||
|
{
|
||||||
|
if (value < 0) {
|
||||||
|
return pp::foundation::Status::out_of_range(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_undo(int undo_count)
|
||||||
|
{
|
||||||
|
const auto count_status = validate_history_metric(undo_count, "undo action count must not be negative");
|
||||||
|
if (!count_status.ok()) {
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryUiPlan plan;
|
||||||
|
plan.operation = HistoryUiOperation::undo;
|
||||||
|
plan.undo_count = undo_count;
|
||||||
|
if (undo_count == 0) {
|
||||||
|
plan.no_op = true;
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.invokes_undo = true;
|
||||||
|
plan.updates_memory_label = true;
|
||||||
|
plan.updates_title = true;
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_redo(int redo_count)
|
||||||
|
{
|
||||||
|
const auto count_status = validate_history_metric(redo_count, "redo action count must not be negative");
|
||||||
|
if (!count_status.ok()) {
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryUiPlan plan;
|
||||||
|
plan.operation = HistoryUiOperation::redo;
|
||||||
|
plan.redo_count = redo_count;
|
||||||
|
if (redo_count == 0) {
|
||||||
|
plan.no_op = true;
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.invokes_redo = true;
|
||||||
|
plan.updates_memory_label = true;
|
||||||
|
plan.updates_title = true;
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_clear(
|
||||||
|
int undo_count,
|
||||||
|
int redo_count,
|
||||||
|
int memory_bytes)
|
||||||
|
{
|
||||||
|
const auto undo_status = validate_history_metric(undo_count, "undo action count must not be negative");
|
||||||
|
if (!undo_status.ok()) {
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::failure(undo_status);
|
||||||
|
}
|
||||||
|
const auto redo_status = validate_history_metric(redo_count, "redo action count must not be negative");
|
||||||
|
if (!redo_status.ok()) {
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::failure(redo_status);
|
||||||
|
}
|
||||||
|
const auto memory_status = validate_history_metric(memory_bytes, "history memory bytes must not be negative");
|
||||||
|
if (!memory_status.ok()) {
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::failure(memory_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryUiPlan plan;
|
||||||
|
plan.operation = HistoryUiOperation::clear;
|
||||||
|
plan.undo_count = undo_count;
|
||||||
|
plan.redo_count = redo_count;
|
||||||
|
plan.memory_bytes = memory_bytes;
|
||||||
|
if (undo_count == 0 && redo_count == 0 && memory_bytes == 0) {
|
||||||
|
plan.no_op = true;
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.clears_history = true;
|
||||||
|
plan.updates_memory_label = true;
|
||||||
|
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_history_ui_plan(
|
||||||
|
const HistoryUiPlan& plan,
|
||||||
|
HistoryUiServices& services)
|
||||||
|
{
|
||||||
|
const auto undo_status = validate_history_metric(plan.undo_count, "undo action count must not be negative");
|
||||||
|
if (!undo_status.ok()) {
|
||||||
|
return undo_status;
|
||||||
|
}
|
||||||
|
const auto redo_status = validate_history_metric(plan.redo_count, "redo action count must not be negative");
|
||||||
|
if (!redo_status.ok()) {
|
||||||
|
return redo_status;
|
||||||
|
}
|
||||||
|
const auto memory_status = validate_history_metric(plan.memory_bytes, "history memory bytes must not be negative");
|
||||||
|
if (!memory_status.ok()) {
|
||||||
|
return memory_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.no_op) {
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (plan.operation) {
|
||||||
|
case HistoryUiOperation::undo:
|
||||||
|
if (plan.invokes_undo) {
|
||||||
|
services.invoke_undo();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case HistoryUiOperation::redo:
|
||||||
|
if (plan.invokes_redo) {
|
||||||
|
services.invoke_redo();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case HistoryUiOperation::clear:
|
||||||
|
if (plan.clears_history) {
|
||||||
|
services.clear_history();
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown history operation");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
202
src/app_core/main_toolbar.h
Normal file
202
src/app_core/main_toolbar.h
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_core/document_canvas.h"
|
||||||
|
#include "app_core/history_ui.h"
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class MainToolbarCommand {
|
||||||
|
open_document,
|
||||||
|
save_document,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
clear_history,
|
||||||
|
clear_canvas,
|
||||||
|
show_message_box,
|
||||||
|
show_settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class MainToolbarAction {
|
||||||
|
show_open_dialog,
|
||||||
|
show_save_dialog,
|
||||||
|
invoke_undo,
|
||||||
|
invoke_redo,
|
||||||
|
clear_history,
|
||||||
|
clear_canvas,
|
||||||
|
show_message_box,
|
||||||
|
show_settings_dialog,
|
||||||
|
no_op_unavailable,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MainToolbarPlan {
|
||||||
|
MainToolbarCommand command = MainToolbarCommand::open_document;
|
||||||
|
MainToolbarAction action = MainToolbarAction::show_open_dialog;
|
||||||
|
std::string label;
|
||||||
|
bool requires_canvas = false;
|
||||||
|
bool updates_memory_label = false;
|
||||||
|
bool updates_title = false;
|
||||||
|
bool records_undo = false;
|
||||||
|
bool marks_unsaved = false;
|
||||||
|
bool no_op = false;
|
||||||
|
HistoryUiPlan history;
|
||||||
|
DocumentCanvasClearPlan canvas_clear;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MainToolbarServices {
|
||||||
|
public:
|
||||||
|
virtual ~MainToolbarServices() = default;
|
||||||
|
|
||||||
|
virtual void show_open_dialog() = 0;
|
||||||
|
virtual void show_save_dialog() = 0;
|
||||||
|
virtual void invoke_undo(const HistoryUiPlan& plan) = 0;
|
||||||
|
virtual void invoke_redo(const HistoryUiPlan& plan) = 0;
|
||||||
|
virtual void clear_history(const HistoryUiPlan& plan) = 0;
|
||||||
|
virtual void clear_canvas(const DocumentCanvasClearPlan& plan) = 0;
|
||||||
|
virtual void show_message_box() = 0;
|
||||||
|
virtual void show_settings_dialog() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<MainToolbarPlan> plan_main_toolbar_command(
|
||||||
|
MainToolbarCommand command,
|
||||||
|
int undo_count = 0,
|
||||||
|
int redo_count = 0,
|
||||||
|
int memory_bytes = 0,
|
||||||
|
bool has_canvas = true)
|
||||||
|
{
|
||||||
|
MainToolbarPlan plan;
|
||||||
|
plan.command = command;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case MainToolbarCommand::open_document:
|
||||||
|
plan.action = MainToolbarAction::show_open_dialog;
|
||||||
|
plan.label = "Open";
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||||
|
|
||||||
|
case MainToolbarCommand::save_document:
|
||||||
|
plan.action = MainToolbarAction::show_save_dialog;
|
||||||
|
plan.label = "Save";
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||||
|
|
||||||
|
case MainToolbarCommand::undo:
|
||||||
|
{
|
||||||
|
const auto history = plan_history_undo(undo_count);
|
||||||
|
if (!history) {
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
|
||||||
|
}
|
||||||
|
plan.action = history.value().invokes_undo
|
||||||
|
? MainToolbarAction::invoke_undo
|
||||||
|
: MainToolbarAction::no_op_unavailable;
|
||||||
|
plan.label = history.value().invokes_undo ? "Undo" : "Undo (No history)";
|
||||||
|
plan.updates_memory_label = history.value().updates_memory_label;
|
||||||
|
plan.updates_title = history.value().updates_title;
|
||||||
|
plan.no_op = history.value().no_op;
|
||||||
|
plan.history = history.value();
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
case MainToolbarCommand::redo:
|
||||||
|
{
|
||||||
|
const auto history = plan_history_redo(redo_count);
|
||||||
|
if (!history) {
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
|
||||||
|
}
|
||||||
|
plan.action = history.value().invokes_redo
|
||||||
|
? MainToolbarAction::invoke_redo
|
||||||
|
: MainToolbarAction::no_op_unavailable;
|
||||||
|
plan.label = history.value().invokes_redo ? "Redo" : "Redo (No history)";
|
||||||
|
plan.updates_memory_label = history.value().updates_memory_label;
|
||||||
|
plan.updates_title = history.value().updates_title;
|
||||||
|
plan.no_op = history.value().no_op;
|
||||||
|
plan.history = history.value();
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
case MainToolbarCommand::clear_history:
|
||||||
|
{
|
||||||
|
const auto history = plan_history_clear(undo_count, redo_count, memory_bytes);
|
||||||
|
if (!history) {
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
|
||||||
|
}
|
||||||
|
plan.action = history.value().clears_history
|
||||||
|
? MainToolbarAction::clear_history
|
||||||
|
: MainToolbarAction::no_op_unavailable;
|
||||||
|
plan.label = history.value().clears_history ? "Clear History" : "Clear History (Empty)";
|
||||||
|
plan.updates_memory_label = history.value().updates_memory_label;
|
||||||
|
plan.no_op = history.value().no_op;
|
||||||
|
plan.history = history.value();
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
case MainToolbarCommand::clear_canvas:
|
||||||
|
{
|
||||||
|
const auto clear = plan_document_canvas_clear(has_canvas);
|
||||||
|
if (!clear) {
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::failure(clear.status());
|
||||||
|
}
|
||||||
|
plan.action = clear.value().clears_canvas
|
||||||
|
? MainToolbarAction::clear_canvas
|
||||||
|
: MainToolbarAction::no_op_unavailable;
|
||||||
|
plan.label = clear.value().clears_canvas ? "Clear Canvas" : "Clear Canvas (No canvas)";
|
||||||
|
plan.requires_canvas = true;
|
||||||
|
plan.records_undo = clear.value().records_undo;
|
||||||
|
plan.marks_unsaved = clear.value().marks_unsaved;
|
||||||
|
plan.no_op = clear.value().no_op;
|
||||||
|
plan.canvas_clear = clear.value();
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
case MainToolbarCommand::show_message_box:
|
||||||
|
plan.action = MainToolbarAction::show_message_box;
|
||||||
|
plan.label = "Show Message Box";
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||||
|
|
||||||
|
case MainToolbarCommand::show_settings:
|
||||||
|
plan.action = MainToolbarAction::show_settings_dialog;
|
||||||
|
plan.label = "Settings";
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<MainToolbarPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("unknown main toolbar command"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_main_toolbar_plan(
|
||||||
|
const MainToolbarPlan& plan,
|
||||||
|
MainToolbarServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.action) {
|
||||||
|
case MainToolbarAction::show_open_dialog:
|
||||||
|
services.show_open_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case MainToolbarAction::show_save_dialog:
|
||||||
|
services.show_save_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case MainToolbarAction::invoke_undo:
|
||||||
|
services.invoke_undo(plan.history);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case MainToolbarAction::invoke_redo:
|
||||||
|
services.invoke_redo(plan.history);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case MainToolbarAction::clear_history:
|
||||||
|
services.clear_history(plan.history);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case MainToolbarAction::clear_canvas:
|
||||||
|
services.clear_canvas(plan.canvas_clear);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case MainToolbarAction::show_message_box:
|
||||||
|
services.show_message_box();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case MainToolbarAction::show_settings_dialog:
|
||||||
|
services.show_settings_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case MainToolbarAction::no_op_unavailable:
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown main toolbar action");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
229
src/app_core/quick_ui.h
Normal file
229
src/app_core/quick_ui.h
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class QuickUiSlotKind {
|
||||||
|
brush,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class QuickUiOperation {
|
||||||
|
select_slot,
|
||||||
|
open_slot_popup,
|
||||||
|
restore_state,
|
||||||
|
reset_state,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct QuickUiPlan {
|
||||||
|
QuickUiOperation operation = QuickUiOperation::select_slot;
|
||||||
|
QuickUiSlotKind slot_kind = QuickUiSlotKind::brush;
|
||||||
|
int slot_index = 0;
|
||||||
|
int previous_index = 0;
|
||||||
|
int brush_index = 0;
|
||||||
|
int color_index = 0;
|
||||||
|
int slot_count = 0;
|
||||||
|
bool fire_event = false;
|
||||||
|
bool updates_selection = false;
|
||||||
|
bool opens_brush_popup = false;
|
||||||
|
bool opens_color_picker = false;
|
||||||
|
bool invokes_change_callback = false;
|
||||||
|
bool restores_slots = false;
|
||||||
|
bool resets_slots = false;
|
||||||
|
bool redraws_brush_previews = false;
|
||||||
|
bool mutates_quick_state = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class QuickUiServices {
|
||||||
|
public:
|
||||||
|
virtual ~QuickUiServices() = default;
|
||||||
|
|
||||||
|
virtual void select_slot(QuickUiSlotKind slot_kind, int slot_index, bool fire_event) = 0;
|
||||||
|
virtual void open_slot_popup(QuickUiSlotKind slot_kind, int slot_index) = 0;
|
||||||
|
virtual void restore_state(int brush_index, int color_index, bool fire_event) = 0;
|
||||||
|
virtual void reset_state(bool fire_event) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_count(int slot_count) noexcept
|
||||||
|
{
|
||||||
|
if (slot_count <= 0) {
|
||||||
|
return pp::foundation::Status::out_of_range("quick slot count must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_index(int slot_index, int slot_count) noexcept
|
||||||
|
{
|
||||||
|
const auto count_status = validate_quick_slot_count(slot_count);
|
||||||
|
if (!count_status.ok()) {
|
||||||
|
return count_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot_index < 0 || slot_index >= slot_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("quick slot index is out of range");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_slot_click(
|
||||||
|
QuickUiSlotKind slot_kind,
|
||||||
|
int current_index,
|
||||||
|
int clicked_index,
|
||||||
|
int slot_count)
|
||||||
|
{
|
||||||
|
const auto current_status = validate_quick_slot_index(current_index, slot_count);
|
||||||
|
if (!current_status.ok()) {
|
||||||
|
return pp::foundation::Result<QuickUiPlan>::failure(current_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto clicked_status = validate_quick_slot_index(clicked_index, slot_count);
|
||||||
|
if (!clicked_status.ok()) {
|
||||||
|
return pp::foundation::Result<QuickUiPlan>::failure(clicked_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickUiPlan plan;
|
||||||
|
plan.slot_kind = slot_kind;
|
||||||
|
plan.slot_index = clicked_index;
|
||||||
|
plan.previous_index = current_index;
|
||||||
|
plan.brush_index = slot_kind == QuickUiSlotKind::brush ? clicked_index : 0;
|
||||||
|
plan.color_index = slot_kind == QuickUiSlotKind::color ? clicked_index : 0;
|
||||||
|
plan.slot_count = slot_count;
|
||||||
|
if (clicked_index != current_index) {
|
||||||
|
plan.operation = QuickUiOperation::select_slot;
|
||||||
|
plan.updates_selection = true;
|
||||||
|
plan.invokes_change_callback = true;
|
||||||
|
plan.mutates_quick_state = true;
|
||||||
|
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.operation = QuickUiOperation::open_slot_popup;
|
||||||
|
plan.opens_brush_popup = slot_kind == QuickUiSlotKind::brush;
|
||||||
|
plan.opens_color_picker = slot_kind == QuickUiSlotKind::color;
|
||||||
|
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_restore(
|
||||||
|
int brush_index,
|
||||||
|
int color_index,
|
||||||
|
int slot_count,
|
||||||
|
bool fire_event)
|
||||||
|
{
|
||||||
|
const auto brush_status = validate_quick_slot_index(brush_index, slot_count);
|
||||||
|
if (!brush_status.ok()) {
|
||||||
|
return pp::foundation::Result<QuickUiPlan>::failure(brush_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto color_status = validate_quick_slot_index(color_index, slot_count);
|
||||||
|
if (!color_status.ok()) {
|
||||||
|
return pp::foundation::Result<QuickUiPlan>::failure(color_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickUiPlan plan;
|
||||||
|
plan.operation = QuickUiOperation::restore_state;
|
||||||
|
plan.brush_index = brush_index;
|
||||||
|
plan.color_index = color_index;
|
||||||
|
plan.slot_count = slot_count;
|
||||||
|
plan.fire_event = fire_event;
|
||||||
|
plan.updates_selection = true;
|
||||||
|
plan.invokes_change_callback = fire_event;
|
||||||
|
plan.restores_slots = true;
|
||||||
|
plan.redraws_brush_previews = true;
|
||||||
|
plan.mutates_quick_state = true;
|
||||||
|
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_reset(
|
||||||
|
int slot_count,
|
||||||
|
bool fire_event)
|
||||||
|
{
|
||||||
|
const auto count_status = validate_quick_slot_count(slot_count);
|
||||||
|
if (!count_status.ok()) {
|
||||||
|
return pp::foundation::Result<QuickUiPlan>::failure(count_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickUiPlan plan;
|
||||||
|
plan.operation = QuickUiOperation::reset_state;
|
||||||
|
plan.brush_index = 0;
|
||||||
|
plan.color_index = 0;
|
||||||
|
plan.slot_count = slot_count;
|
||||||
|
plan.fire_event = fire_event;
|
||||||
|
plan.updates_selection = true;
|
||||||
|
plan.invokes_change_callback = fire_event;
|
||||||
|
plan.resets_slots = true;
|
||||||
|
plan.redraws_brush_previews = true;
|
||||||
|
plan.mutates_quick_state = true;
|
||||||
|
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_quick_ui_plan(
|
||||||
|
const QuickUiPlan& plan,
|
||||||
|
QuickUiServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.operation) {
|
||||||
|
case QuickUiOperation::select_slot:
|
||||||
|
if (!plan.updates_selection) {
|
||||||
|
return pp::foundation::Status::invalid_argument("quick select plan must update selection");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services.select_slot(plan.slot_kind, plan.slot_index, plan.invokes_change_callback);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case QuickUiOperation::open_slot_popup:
|
||||||
|
if (plan.slot_kind == QuickUiSlotKind::brush && !plan.opens_brush_popup) {
|
||||||
|
return pp::foundation::Status::invalid_argument("quick brush popup plan must open brush popup");
|
||||||
|
}
|
||||||
|
if (plan.slot_kind == QuickUiSlotKind::color && !plan.opens_color_picker) {
|
||||||
|
return pp::foundation::Status::invalid_argument("quick color popup plan must open color picker");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services.open_slot_popup(plan.slot_kind, plan.slot_index);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case QuickUiOperation::restore_state:
|
||||||
|
if (!plan.restores_slots) {
|
||||||
|
return pp::foundation::Status::invalid_argument("quick restore plan must restore slots");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const auto brush_status = validate_quick_slot_index(plan.brush_index, plan.slot_count);
|
||||||
|
if (!brush_status.ok()) {
|
||||||
|
return brush_status;
|
||||||
|
}
|
||||||
|
const auto color_status = validate_quick_slot_index(plan.color_index, plan.slot_count);
|
||||||
|
if (!color_status.ok()) {
|
||||||
|
return color_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services.restore_state(plan.brush_index, plan.color_index, plan.fire_event);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
|
||||||
|
case QuickUiOperation::reset_state:
|
||||||
|
if (!plan.resets_slots) {
|
||||||
|
return pp::foundation::Status::invalid_argument("quick reset plan must reset slots");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const auto status = validate_quick_slot_count(plan.slot_count);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services.reset_state(plan.fire_event);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown quick UI operation");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pp::app
|
||||||
185
src/app_core/tools_menu.h
Normal file
185
src/app_core/tools_menu.h
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::app {
|
||||||
|
|
||||||
|
enum class ToolsMenuCommand {
|
||||||
|
panels,
|
||||||
|
options,
|
||||||
|
clear_grids,
|
||||||
|
reset_camera,
|
||||||
|
shortcuts,
|
||||||
|
sonarpen,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ToolsMenuAction {
|
||||||
|
show_panels_submenu,
|
||||||
|
show_options_submenu,
|
||||||
|
clear_grid_overlays,
|
||||||
|
reset_camera,
|
||||||
|
show_shortcuts_dialog,
|
||||||
|
start_sonarpen,
|
||||||
|
no_op_unavailable,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ToolsPanel {
|
||||||
|
presets,
|
||||||
|
color,
|
||||||
|
color_advanced,
|
||||||
|
layers,
|
||||||
|
brush,
|
||||||
|
grids,
|
||||||
|
animation,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ToolsPanelAction {
|
||||||
|
open_floating_panel,
|
||||||
|
no_op_already_visible,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToolsMenuPlan {
|
||||||
|
ToolsMenuCommand command = ToolsMenuCommand::panels;
|
||||||
|
ToolsMenuAction action = ToolsMenuAction::show_panels_submenu;
|
||||||
|
std::string_view label;
|
||||||
|
bool closes_root_popup = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToolsPanelPlan {
|
||||||
|
ToolsPanel panel = ToolsPanel::presets;
|
||||||
|
ToolsPanelAction action = ToolsPanelAction::open_floating_panel;
|
||||||
|
std::string_view title;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
int min_width = 0;
|
||||||
|
int min_height = 0;
|
||||||
|
bool droppable = true;
|
||||||
|
bool hides_embedded_title = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ToolsMenuServices {
|
||||||
|
public:
|
||||||
|
virtual ~ToolsMenuServices() = default;
|
||||||
|
|
||||||
|
virtual void show_panels_submenu() = 0;
|
||||||
|
virtual void show_options_submenu() = 0;
|
||||||
|
virtual void clear_grid_overlays() = 0;
|
||||||
|
virtual void reset_camera() = 0;
|
||||||
|
virtual void show_shortcuts_dialog() = 0;
|
||||||
|
virtual void start_sonarpen() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr ToolsMenuPlan plan_tools_menu_command(
|
||||||
|
ToolsMenuCommand command,
|
||||||
|
bool sonarpen_available = false) noexcept
|
||||||
|
{
|
||||||
|
switch (command) {
|
||||||
|
case ToolsMenuCommand::panels:
|
||||||
|
return { command, ToolsMenuAction::show_panels_submenu, "Panels", false };
|
||||||
|
case ToolsMenuCommand::options:
|
||||||
|
return { command, ToolsMenuAction::show_options_submenu, "Options", false };
|
||||||
|
case ToolsMenuCommand::clear_grids:
|
||||||
|
return { command, ToolsMenuAction::clear_grid_overlays, "Clear Grids", true };
|
||||||
|
case ToolsMenuCommand::reset_camera:
|
||||||
|
return { command, ToolsMenuAction::reset_camera, "Reset Camera", true };
|
||||||
|
case ToolsMenuCommand::shortcuts:
|
||||||
|
return { command, ToolsMenuAction::show_shortcuts_dialog, "Shortcuts", true };
|
||||||
|
case ToolsMenuCommand::sonarpen:
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
sonarpen_available ? ToolsMenuAction::start_sonarpen : ToolsMenuAction::no_op_unavailable,
|
||||||
|
"SonarPen",
|
||||||
|
sonarpen_available,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { command, ToolsMenuAction::no_op_unavailable, "", false };
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr ToolsPanelPlan plan_tools_panel(
|
||||||
|
ToolsPanel panel,
|
||||||
|
bool already_visible) noexcept
|
||||||
|
{
|
||||||
|
ToolsPanelPlan plan;
|
||||||
|
plan.panel = panel;
|
||||||
|
plan.action = already_visible
|
||||||
|
? ToolsPanelAction::no_op_already_visible
|
||||||
|
: ToolsPanelAction::open_floating_panel;
|
||||||
|
|
||||||
|
switch (panel) {
|
||||||
|
case ToolsPanel::presets:
|
||||||
|
plan.title = "Brushes";
|
||||||
|
plan.height = 300;
|
||||||
|
plan.min_height = 300;
|
||||||
|
plan.min_width = 100;
|
||||||
|
break;
|
||||||
|
case ToolsPanel::color:
|
||||||
|
plan.title = "Color Picker";
|
||||||
|
plan.height = 300;
|
||||||
|
plan.hides_embedded_title = true;
|
||||||
|
break;
|
||||||
|
case ToolsPanel::color_advanced:
|
||||||
|
plan.title = "Color Picker";
|
||||||
|
plan.width = 300;
|
||||||
|
plan.height = 300;
|
||||||
|
break;
|
||||||
|
case ToolsPanel::layers:
|
||||||
|
plan.title = "Layers";
|
||||||
|
plan.height = 300;
|
||||||
|
plan.min_height = 100;
|
||||||
|
plan.hides_embedded_title = true;
|
||||||
|
break;
|
||||||
|
case ToolsPanel::brush:
|
||||||
|
plan.title = "Brush Settings";
|
||||||
|
plan.height = 300;
|
||||||
|
plan.hides_embedded_title = true;
|
||||||
|
break;
|
||||||
|
case ToolsPanel::grids:
|
||||||
|
plan.title = "Grid";
|
||||||
|
plan.height = 300;
|
||||||
|
plan.hides_embedded_title = true;
|
||||||
|
break;
|
||||||
|
case ToolsPanel::animation:
|
||||||
|
plan.title = "Animation";
|
||||||
|
plan.width = 500;
|
||||||
|
plan.height = 300;
|
||||||
|
plan.droppable = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_tools_menu_plan(
|
||||||
|
const ToolsMenuPlan& plan,
|
||||||
|
ToolsMenuServices& services)
|
||||||
|
{
|
||||||
|
switch (plan.action) {
|
||||||
|
case ToolsMenuAction::show_panels_submenu:
|
||||||
|
services.show_panels_submenu();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case ToolsMenuAction::show_options_submenu:
|
||||||
|
services.show_options_submenu();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case ToolsMenuAction::clear_grid_overlays:
|
||||||
|
services.clear_grid_overlays();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case ToolsMenuAction::reset_camera:
|
||||||
|
services.reset_camera();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case ToolsMenuAction::show_shortcuts_dialog:
|
||||||
|
services.show_shortcuts_dialog();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case ToolsMenuAction::start_sonarpen:
|
||||||
|
services.start_sonarpen();
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
case ToolsMenuAction::no_op_unavailable:
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown tools menu action");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "action.h"
|
#include "action.h"
|
||||||
|
#include "app_core/document_layer.h"
|
||||||
|
#include "app_core/document_resize.h"
|
||||||
|
#include "app_core/document_export.h"
|
||||||
|
#include "app_core/document_session.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "node_dialog_open.h"
|
#include "node_dialog_open.h"
|
||||||
#include "node_dialog_browse.h"
|
#include "node_dialog_browse.h"
|
||||||
@@ -21,11 +25,33 @@
|
|||||||
#include "oculus_vr.h"
|
#include "oculus_vr.h"
|
||||||
#elif __WEB__
|
#elif __WEB__
|
||||||
void webgl_pick_file(std::function<void(std::string)> callback);
|
void webgl_pick_file(std::function<void(std::string)> callback);
|
||||||
void webgl_pick_file_save(const std::string& path,
|
|
||||||
const std::string& name, std::function<void(bool)> callback);
|
|
||||||
void webgl_sync();
|
void webgl_sync();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] bool can_start_document_export(App& app, bool requires_license)
|
||||||
|
{
|
||||||
|
const auto decision = pp::app::plan_document_export_start(
|
||||||
|
requires_license,
|
||||||
|
!requires_license || app.check_license(),
|
||||||
|
app.canvas != nullptr);
|
||||||
|
|
||||||
|
switch (decision) {
|
||||||
|
case pp::app::DocumentExportStartDecision::start_now:
|
||||||
|
return true;
|
||||||
|
case pp::app::DocumentExportStartDecision::show_license_disabled:
|
||||||
|
app.message_box("License", "This function is disabled in demo mode.");
|
||||||
|
return false;
|
||||||
|
case pp::app::DocumentExportStartDecision::unavailable_no_canvas:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<NodeProgressBar> App::show_progress(const std::string& title, int total /*= 0*/)
|
std::shared_ptr<NodeProgressBar> App::show_progress(const std::string& title, int total /*= 0*/)
|
||||||
{
|
{
|
||||||
auto pb = std::make_shared<NodeProgressBar>();
|
auto pb = std::make_shared<NodeProgressBar>();
|
||||||
@@ -105,6 +131,41 @@ void App::dialog_about()
|
|||||||
layout[main_id]->add_child(dialog);
|
layout[main_id]->add_child(dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::continue_document_workflow_after_optional_save(std::function<void()> action)
|
||||||
|
{
|
||||||
|
const bool has_canvas = canvas != nullptr;
|
||||||
|
const bool has_unsaved_changes = has_canvas && Canvas::I->m_unsaved;
|
||||||
|
const auto decision = pp::app::plan_document_workflow(has_canvas, has_unsaved_changes);
|
||||||
|
switch (decision) {
|
||||||
|
case pp::app::DocumentWorkflowDecision::unavailable:
|
||||||
|
return;
|
||||||
|
case pp::app::DocumentWorkflowDecision::continue_now:
|
||||||
|
action();
|
||||||
|
return;
|
||||||
|
case pp::app::DocumentWorkflowDecision::prompt_save_before_continue:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto m = layout[main_id]->add_child<NodeMessageBox>();
|
||||||
|
m->m_title->set_text("Unsaved document");
|
||||||
|
m->m_message->set_text("Would you like to save this document before closing?");
|
||||||
|
m->btn_ok->m_text->set_text("Yes");
|
||||||
|
m->btn_cancel->m_text->set_text("No");
|
||||||
|
m->btn_ok->on_click = [this, m, action](Node*) {
|
||||||
|
Canvas::I->project_save([this, m, action](bool success) {
|
||||||
|
if (success)
|
||||||
|
action();
|
||||||
|
else
|
||||||
|
message_box("Saving Error", "There was a problem saving the document");
|
||||||
|
});
|
||||||
|
m->destroy();
|
||||||
|
};
|
||||||
|
m->btn_cancel->on_click = [m, action](Node*) {
|
||||||
|
action();
|
||||||
|
m->destroy();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void App::dialog_newdoc()
|
void App::dialog_newdoc()
|
||||||
{
|
{
|
||||||
auto show_dialog = [this] {
|
auto show_dialog = [this] {
|
||||||
@@ -122,25 +183,32 @@ void App::dialog_newdoc()
|
|||||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||||
{
|
{
|
||||||
std::string name = dialog->input->m_text;
|
std::string name = dialog->input->m_text;
|
||||||
std::string path = work_path + "/" + name + ".ppi";
|
const auto plan = pp::app::plan_new_document(
|
||||||
|
work_path,
|
||||||
if (name.empty())
|
name,
|
||||||
|
dialog->m_resolution->m_current_index,
|
||||||
|
[](const std::string& path) {
|
||||||
|
return Asset::exist(path);
|
||||||
|
});
|
||||||
|
if (!plan)
|
||||||
{
|
{
|
||||||
message_box("Warning", "You need to specify a name to file.");
|
const bool missing_name =
|
||||||
|
plan.status().code == pp::foundation::StatusCode::invalid_argument;
|
||||||
|
message_box(
|
||||||
|
"Warning",
|
||||||
|
missing_name ? "You need to specify a name to file." : plan.status().message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto action = [this, dialog, name, path] {
|
auto action = [this, dialog, plan = plan.value()] {
|
||||||
std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
|
doc_name = plan.target.name;
|
||||||
int res = resolutions[dialog->m_resolution->m_current_index];
|
doc_path = plan.target.path;
|
||||||
doc_name = name;
|
doc_filename = plan.target.name + ".ppi";
|
||||||
doc_path = path;
|
doc_dir = plan.target.directory;
|
||||||
doc_filename = name + ".ppi";
|
|
||||||
doc_dir = work_path;
|
|
||||||
|
|
||||||
layers->clear();
|
layers->clear();
|
||||||
canvas->m_canvas->m_layers.clear();
|
canvas->m_canvas->m_layers.clear();
|
||||||
canvas->m_canvas->resize(res, res);
|
canvas->m_canvas->resize(plan.resolution, plan.resolution);
|
||||||
canvas->reset_camera();
|
canvas->reset_camera();
|
||||||
ActionManager::clear();
|
ActionManager::clear();
|
||||||
|
|
||||||
@@ -154,7 +222,7 @@ void App::dialog_newdoc()
|
|||||||
App::I->hideKeyboard();
|
App::I->hideKeyboard();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Asset::exist(path))
|
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
|
||||||
{
|
{
|
||||||
// ask confirm is file already exist
|
// ask confirm is file already exist
|
||||||
auto msgbox = new NodeMessageBox();
|
auto msgbox = new NodeMessageBox();
|
||||||
@@ -181,34 +249,7 @@ void App::dialog_newdoc()
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (canvas)
|
continue_document_workflow_after_optional_save(show_dialog);
|
||||||
{
|
|
||||||
if (Canvas::I->m_unsaved)
|
|
||||||
{
|
|
||||||
auto m = layout[main_id]->add_child<NodeMessageBox>();
|
|
||||||
m->m_title->set_text("Unsaved document");
|
|
||||||
m->m_message->set_text("Would you like to save this document before closing?");
|
|
||||||
m->btn_ok->m_text->set_text("Yes");
|
|
||||||
m->btn_cancel->m_text->set_text("No");
|
|
||||||
m->btn_ok->on_click = [this, m, show_dialog](Node*) {
|
|
||||||
Canvas::I->project_save([this, m, show_dialog](bool success){
|
|
||||||
if (success)
|
|
||||||
show_dialog();
|
|
||||||
else
|
|
||||||
message_box("Saving Error", "There was a problem saving the document");
|
|
||||||
});
|
|
||||||
m->destroy();
|
|
||||||
};
|
|
||||||
m->btn_cancel->on_click = [this, m, show_dialog](Node*) {
|
|
||||||
show_dialog();
|
|
||||||
m->destroy();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
show_dialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEPRECATED
|
// DEPRECATED
|
||||||
@@ -242,34 +283,7 @@ void App::dialog_open()
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (canvas)
|
continue_document_workflow_after_optional_save(show_dialog);
|
||||||
{
|
|
||||||
if (Canvas::I->m_unsaved)
|
|
||||||
{
|
|
||||||
auto m = layout[main_id]->add_child<NodeMessageBox>();
|
|
||||||
m->m_title->set_text("Unsaved document");
|
|
||||||
m->m_message->set_text("Would you like to save this document before closing?");
|
|
||||||
m->btn_ok->m_text->set_text("Yes");
|
|
||||||
m->btn_cancel->m_text->set_text("No");
|
|
||||||
m->btn_ok->on_click = [this,m,show_dialog](Node*){
|
|
||||||
Canvas::I->project_save([this,m,show_dialog](bool success){
|
|
||||||
if (success)
|
|
||||||
show_dialog();
|
|
||||||
else
|
|
||||||
message_box("Saving Error", "There was a problem saving the document");
|
|
||||||
});
|
|
||||||
m->destroy();
|
|
||||||
};
|
|
||||||
m->btn_cancel->on_click = [this,m,show_dialog](Node*) {
|
|
||||||
show_dialog();
|
|
||||||
m->destroy();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
show_dialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::dialog_browse()
|
void App::dialog_browse()
|
||||||
@@ -299,34 +313,7 @@ void App::dialog_browse()
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (canvas)
|
continue_document_workflow_after_optional_save(show_dialog);
|
||||||
{
|
|
||||||
if (Canvas::I->m_unsaved)
|
|
||||||
{
|
|
||||||
auto m = layout[main_id]->add_child<NodeMessageBox>();
|
|
||||||
m->m_title->set_text("Unsaved document");
|
|
||||||
m->m_message->set_text("Would you like to save this document before closing?");
|
|
||||||
m->btn_ok->m_text->set_text("Yes");
|
|
||||||
m->btn_cancel->m_text->set_text("No");
|
|
||||||
m->btn_ok->on_click = [this, m, show_dialog](Node*) {
|
|
||||||
Canvas::I->project_save([this, m, show_dialog](bool success){
|
|
||||||
if (success)
|
|
||||||
show_dialog();
|
|
||||||
else
|
|
||||||
message_box("Saving Error", "There was a problem saving the document");
|
|
||||||
});
|
|
||||||
m->destroy();
|
|
||||||
};
|
|
||||||
m->btn_cancel->on_click = [this, m, show_dialog](Node*) {
|
|
||||||
show_dialog();
|
|
||||||
m->destroy();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
show_dialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::dialog_save_ver()
|
void App::dialog_save_ver()
|
||||||
@@ -337,35 +324,45 @@ void App::dialog_save_ver()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int current = 0;
|
const auto target = pp::app::find_next_document_version_target(
|
||||||
std::string next = doc_name + ".01";
|
doc_dir,
|
||||||
std::string base = doc_name;
|
doc_name,
|
||||||
|
[](const std::string& path) {
|
||||||
std::regex r(R"((.*)\.(\w{2})$)");
|
return Asset::exist(path);
|
||||||
std::smatch m;
|
});
|
||||||
if (std::regex_search(doc_name, m, r))
|
if (!target) {
|
||||||
{
|
message_box("Saving Error", target.status().message);
|
||||||
base = m[1].str();
|
return;
|
||||||
current = atoi(m[2].str().c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = current + 1; i < 99; i++)
|
doc_name = target.value().name;
|
||||||
{
|
doc_path = target.value().path;
|
||||||
static char tmp_name[256];
|
|
||||||
sprintf(tmp_name, "%s.%02d", base.c_str(), i);
|
|
||||||
next = tmp_name;
|
|
||||||
if (Asset::exist(doc_dir + "/" + next + ".ppi"))
|
|
||||||
continue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
doc_name = next;
|
|
||||||
doc_path = doc_dir + "/" + next + ".ppi";
|
|
||||||
canvas->m_canvas->m_unsaved = true;
|
canvas->m_canvas->m_unsaved = true;
|
||||||
title_update();
|
title_update();
|
||||||
canvas->m_canvas->project_save(doc_path);
|
canvas->m_canvas->project_save(doc_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::save_document(pp::app::DocumentSaveIntent intent)
|
||||||
|
{
|
||||||
|
const auto decision = pp::app::plan_document_save(
|
||||||
|
Canvas::I->m_newdoc,
|
||||||
|
Canvas::I->m_unsaved,
|
||||||
|
intent);
|
||||||
|
switch (decision) {
|
||||||
|
case pp::app::DocumentSaveDecision::show_save_dialog:
|
||||||
|
dialog_save();
|
||||||
|
break;
|
||||||
|
case pp::app::DocumentSaveDecision::save_existing:
|
||||||
|
Canvas::I->project_save();
|
||||||
|
break;
|
||||||
|
case pp::app::DocumentSaveDecision::save_version:
|
||||||
|
dialog_save_ver();
|
||||||
|
break;
|
||||||
|
case pp::app::DocumentSaveDecision::no_op:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void App::dialog_save()
|
void App::dialog_save()
|
||||||
{
|
{
|
||||||
if (!check_license())
|
if (!check_license())
|
||||||
@@ -388,32 +385,36 @@ void App::dialog_save()
|
|||||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||||
{
|
{
|
||||||
std::string name = dialog->input->m_text;
|
std::string name = dialog->input->m_text;
|
||||||
std::string path = work_path + "/" + name + ".ppi";
|
const auto plan = pp::app::plan_document_file_save(
|
||||||
|
work_path,
|
||||||
if (name.empty())
|
name,
|
||||||
|
[](const std::string& path) {
|
||||||
|
return Asset::exist(path);
|
||||||
|
});
|
||||||
|
if (!plan)
|
||||||
{
|
{
|
||||||
message_box("Warning", "You need to specify a name to file.");
|
message_box("Warning", "You need to specify a name to file.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto action = [this, dialog, name, path] {
|
auto action = [this, dialog, plan = plan.value()] {
|
||||||
canvas->m_canvas->project_save(path);
|
canvas->m_canvas->project_save(plan.target.path);
|
||||||
doc_name = name;
|
doc_name = plan.target.name;
|
||||||
doc_path = path;
|
doc_path = plan.target.path;
|
||||||
doc_dir = work_path;
|
doc_dir = plan.target.directory;
|
||||||
title_update();
|
title_update();
|
||||||
dialog->destroy();
|
dialog->destroy();
|
||||||
App::I->hideKeyboard();
|
App::I->hideKeyboard();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Asset::exist(path))
|
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
|
||||||
{
|
{
|
||||||
// ask confirm is file already exist
|
// ask confirm is file already exist
|
||||||
auto msgbox = new NodeMessageBox();
|
auto msgbox = new NodeMessageBox();
|
||||||
msgbox->set_manager(&layout);
|
msgbox->set_manager(&layout);
|
||||||
msgbox->init();
|
msgbox->init();
|
||||||
msgbox->m_title->set_text("Warning");
|
msgbox->m_title->set_text("Warning");
|
||||||
msgbox->m_message->set_text(("Are you sure you want to overwrite " + name + "?").c_str());
|
msgbox->m_message->set_text(("Are you sure you want to overwrite " + plan.value().target.name + "?").c_str());
|
||||||
msgbox->btn_ok->on_click = [this, msgbox, action](Node*) {
|
msgbox->btn_ok->on_click = [this, msgbox, action](Node*) {
|
||||||
action();
|
action();
|
||||||
msgbox->destroy();
|
msgbox->destroy();
|
||||||
@@ -437,120 +438,145 @@ void App::dialog_save()
|
|||||||
|
|
||||||
void App::dialog_export(std::string ext)
|
void App::dialog_export(std::string ext)
|
||||||
{
|
{
|
||||||
if (!check_license())
|
if (!can_start_document_export(*this, true))
|
||||||
{
|
return;
|
||||||
message_box("License", "This function is disabled in demo mode.");
|
|
||||||
|
// TODO: use picker
|
||||||
|
const auto target = pp::app::make_document_export_file_target(work_path, doc_name, ext);
|
||||||
|
if (!target) {
|
||||||
|
message_box("Export Equirectangular", target.status().message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canvas)
|
canvas->m_canvas->export_equirectangular(target.value().path, [this, target = target.value()]{
|
||||||
{
|
|
||||||
// TODO: use picker
|
|
||||||
auto path = work_path + "/" + doc_name + ext;
|
|
||||||
auto name = doc_name + ext;
|
|
||||||
canvas->m_canvas->export_equirectangular(path, [this, path, name]{
|
|
||||||
#if defined(__IOS__)
|
#if defined(__IOS__)
|
||||||
message_box("Export Equirectangular", "Image exported to Photos");
|
message_box("Export Equirectangular", "Image exported to Photos");
|
||||||
#elif defined(__OSX__)
|
#elif defined(__OSX__)
|
||||||
message_box("Export Equirectangular", "Image exported to Pictures/PanoPainter folder");
|
message_box("Export Equirectangular", "Image exported to Pictures/PanoPainter folder");
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
message_box("Export Equirectangular", "Image exported to " + work_path);
|
message_box("Export Equirectangular", "Image exported to " + work_path);
|
||||||
#elif defined(__QUEST__)
|
#elif defined(__QUEST__)
|
||||||
//auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo);
|
//auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo);
|
||||||
#elif __WEB__
|
#elif __WEB__
|
||||||
ui_task([=]{
|
ui_task([=]{
|
||||||
webgl_pick_file_save(path, name, [](bool success){ });
|
save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { });
|
||||||
});
|
|
||||||
#endif
|
|
||||||
});
|
});
|
||||||
}
|
#endif
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::dialog_export_layers()
|
void App::dialog_export_layers()
|
||||||
{
|
{
|
||||||
if (!check_license())
|
if (!can_start_document_export(*this, true))
|
||||||
{
|
return;
|
||||||
message_box("License", "This function is disabled in demo mode.");
|
|
||||||
|
#if defined(__IOS__)
|
||||||
|
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_layers");
|
||||||
|
if (!target) {
|
||||||
|
message_box("Export Layers", target.status().message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canvas)
|
if (Asset::create_dir(target.value().directory))
|
||||||
{
|
{
|
||||||
#if defined(__IOS__)
|
canvas->m_canvas->export_layers(target.value().stem_path, [this] {
|
||||||
auto dir = work_path + "/" + doc_name + "_layers";
|
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
|
||||||
if (Asset::create_dir(dir))
|
|
||||||
{
|
|
||||||
auto p = dir + "/" + doc_name;
|
|
||||||
canvas->m_canvas->export_layers(p, [this, p] {
|
|
||||||
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
pick_dir([this](std::string path) {
|
|
||||||
auto p = path + "/" + doc_name;
|
|
||||||
canvas->m_canvas->export_layers(p, [this, p] {
|
|
||||||
message_box("Export Layers", "Layers exported to: " + p);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
pick_dir([this](std::string path) {
|
||||||
|
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
|
||||||
|
if (!target) {
|
||||||
|
message_box("Export Layers", target.status().message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas->m_canvas->export_layers(target.value().stem_path, [this, target = target.value()] {
|
||||||
|
message_box("Export Layers", "Layers exported to: " + target.stem_path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::dialog_export_anim_frames()
|
void App::dialog_export_anim_frames()
|
||||||
{
|
{
|
||||||
if (!check_license())
|
if (!can_start_document_export(*this, true))
|
||||||
{
|
return;
|
||||||
message_box("License", "This function is disabled in demo mode.");
|
|
||||||
|
#if defined(__IOS__)
|
||||||
|
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_frames");
|
||||||
|
if (!target) {
|
||||||
|
message_box("Export Layers", target.status().message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canvas)
|
if (Asset::create_dir(target.value().directory))
|
||||||
{
|
{
|
||||||
#if defined(__IOS__)
|
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] {
|
||||||
auto dir = work_path + "/" + doc_name + "_frames";
|
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
|
||||||
if (Asset::create_dir(dir))
|
|
||||||
{
|
|
||||||
auto p = dir + "/" + doc_name;
|
|
||||||
canvas->m_canvas->export_anim_frames(p, [this, p] {
|
|
||||||
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
pick_dir([this](std::string path) {
|
|
||||||
auto p = path + "/" + doc_name;
|
|
||||||
canvas->m_canvas->export_anim_frames(p, [this, p] {
|
|
||||||
message_box("Export Layers", "Layers exported to: " + p);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
pick_dir([this](std::string path) {
|
||||||
|
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
|
||||||
|
if (!target) {
|
||||||
|
message_box("Export Layers", target.status().message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this, target = target.value()] {
|
||||||
|
message_box("Export Layers", "Layers exported to: " + target.stem_path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::dialog_export_depth()
|
void App::dialog_export_depth()
|
||||||
{
|
{
|
||||||
if (!check_license())
|
if (!can_start_document_export(*this, true))
|
||||||
{
|
|
||||||
message_box("License", "This function is disabled in demo mode.");
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (canvas)
|
// TODO: use picker
|
||||||
{
|
canvas->m_canvas->export_depth(doc_name, [this] {
|
||||||
// TODO: use picker
|
|
||||||
canvas->m_canvas->export_depth(doc_name, [this] {
|
|
||||||
#if defined(__IOS__)
|
#if defined(__IOS__)
|
||||||
message_box("Export 3D View + Depth", "Image and depth exported to Files/PanoPainter");
|
message_box("Export 3D View + Depth", "Image and depth exported to Files/PanoPainter");
|
||||||
#elif defined(__OSX__)
|
#elif defined(__OSX__)
|
||||||
message_box("Export 3D View + Depth", "Image and depth exported to Pictures/PanoPainter folder");
|
message_box("Export 3D View + Depth", "Image and depth exported to Pictures/PanoPainter folder");
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
message_box("Export 3D View + Depth", "Image and depth exported to " + work_path);
|
message_box("Export 3D View + Depth", "Image and depth exported to " + work_path);
|
||||||
#endif
|
#endif
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::dialog_resize()
|
void App::dialog_resize()
|
||||||
{
|
{
|
||||||
|
class LegacyDocumentResizeServices final : public pp::app::DocumentResizeServices {
|
||||||
|
public:
|
||||||
|
explicit LegacyDocumentResizeServices(App& app) noexcept
|
||||||
|
: app_(app)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void resize_document(int width, int height) override
|
||||||
|
{
|
||||||
|
if (app_.canvas)
|
||||||
|
app_.canvas->m_canvas->resize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_title() override
|
||||||
|
{
|
||||||
|
app_.title_update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_history() override
|
||||||
|
{
|
||||||
|
ActionManager::clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
App& app_;
|
||||||
|
};
|
||||||
|
|
||||||
auto dialog = std::make_shared<NodeDialogResize>();
|
auto dialog = std::make_shared<NodeDialogResize>();
|
||||||
dialog->set_manager(&layout);
|
dialog->set_manager(&layout);
|
||||||
dialog->init();
|
dialog->init();
|
||||||
@@ -561,29 +587,35 @@ void App::dialog_resize()
|
|||||||
|
|
||||||
dialog->btn_ok->on_click = [this,dialog](Node*)
|
dialog->btn_ok->on_click = [this,dialog](Node*)
|
||||||
{
|
{
|
||||||
int res = dialog->get_resolution();
|
const auto plan = pp::app::plan_document_resize(
|
||||||
if (canvas)
|
dialog->combo ? dialog->combo->m_current_index : 0);
|
||||||
canvas->m_canvas->resize(res, res);
|
if (!plan)
|
||||||
App::I->title_update();
|
{
|
||||||
ActionManager::clear();
|
dialog->destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LegacyDocumentResizeServices services(*this);
|
||||||
|
const auto status = pp::app::execute_document_resize_plan(plan.value(), services);
|
||||||
|
if (!status.ok())
|
||||||
|
LOG("Document resize failed: %s", status.message);
|
||||||
dialog->destroy();
|
dialog->destroy();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::dialog_export_cube_faces()
|
void App::dialog_export_cube_faces()
|
||||||
{
|
{
|
||||||
if (canvas)
|
if (!can_start_document_export(*this, false))
|
||||||
{
|
return;
|
||||||
canvas->m_canvas->export_cube_faces(doc_name, [this] {
|
|
||||||
|
canvas->m_canvas->export_cube_faces(doc_name, [this] {
|
||||||
#if defined(__IOS__)
|
#if defined(__IOS__)
|
||||||
message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter");
|
message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter");
|
||||||
#elif defined(__OSX__)
|
#elif defined(__OSX__)
|
||||||
message_box("Export Cube Faces", "Image and depth exported to Pictures/PanoPainter folder");
|
message_box("Export Cube Faces", "Image and depth exported to Pictures/PanoPainter folder");
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
message_box("Export Cube Faces", "Image and depth exported to " + work_path);
|
message_box("Export Cube Faces", "Image and depth exported to " + work_path);
|
||||||
#endif
|
#endif
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::dialog_layer_rename()
|
void App::dialog_layer_rename()
|
||||||
@@ -601,6 +633,17 @@ void App::dialog_layer_rename()
|
|||||||
|
|
||||||
dialog->btn_ok->on_click = [this,dialog](Node*)
|
dialog->btn_ok->on_click = [this,dialog](Node*)
|
||||||
{
|
{
|
||||||
|
const auto old_name = layers->m_current_layer->m_label_text;
|
||||||
|
const auto plan = pp::app::plan_document_layer_rename(old_name, dialog->get_name());
|
||||||
|
if (!plan)
|
||||||
|
return;
|
||||||
|
if (plan.value().action == pp::app::DocumentLayerRenameAction::no_op_same_name)
|
||||||
|
{
|
||||||
|
dialog->destroy();
|
||||||
|
App::I->hideKeyboard();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
struct ActionLayerRename : public Action
|
struct ActionLayerRename : public Action
|
||||||
{
|
{
|
||||||
std::string m_old_name;
|
std::string m_old_name;
|
||||||
@@ -624,9 +667,13 @@ void App::dialog_layer_rename()
|
|||||||
};
|
};
|
||||||
auto layer_node = std::static_pointer_cast<NodeLayer>(layers->m_current_layer->shared_from_this());
|
auto layer_node = std::static_pointer_cast<NodeLayer>(layers->m_current_layer->shared_from_this());
|
||||||
auto* layer = canvas->m_canvas->m_layers[canvas->m_canvas->m_current_layer_idx].get();
|
auto* layer = canvas->m_canvas->m_layers[canvas->m_canvas->m_current_layer_idx].get();
|
||||||
ActionManager::add(new ActionLayerRename(layers->m_current_layer->m_label_text, dialog->get_name(), layer_node, layer));
|
ActionManager::add(new ActionLayerRename(
|
||||||
layer_node->set_name(dialog->get_name().c_str());
|
plan.value().old_name,
|
||||||
layer->m_name = dialog->get_name();
|
plan.value().new_name,
|
||||||
|
layer_node,
|
||||||
|
layer));
|
||||||
|
layer_node->set_name(plan.value().new_name.c_str());
|
||||||
|
layer->m_name = plan.value().new_name;
|
||||||
dialog->destroy();
|
dialog->destroy();
|
||||||
App::I->hideKeyboard();
|
App::I->hideKeyboard();
|
||||||
};
|
};
|
||||||
@@ -681,8 +728,17 @@ void App::dialog_ppbr_export()
|
|||||||
|
|
||||||
void App::dialog_timelapse_export()
|
void App::dialog_timelapse_export()
|
||||||
{
|
{
|
||||||
|
if (!can_start_document_export(*this, false))
|
||||||
|
return;
|
||||||
|
|
||||||
#if __IOS__ || __WEB__
|
#if __IOS__ || __WEB__
|
||||||
pick_file_save("mp4", doc_name + "-timelapse",
|
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-timelapse");
|
||||||
|
if (!target) {
|
||||||
|
message_box("Export Timelapse", target.status().message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_file_save("mp4", target.value().name,
|
||||||
[this](std::string path) {
|
[this](std::string path) {
|
||||||
rec_export(path);
|
rec_export(path);
|
||||||
},
|
},
|
||||||
@@ -703,8 +759,17 @@ void App::dialog_timelapse_export()
|
|||||||
|
|
||||||
void App::dialog_export_mp4()
|
void App::dialog_export_mp4()
|
||||||
{
|
{
|
||||||
|
if (!can_start_document_export(*this, false))
|
||||||
|
return;
|
||||||
|
|
||||||
#if __IOS__ || __WEB__
|
#if __IOS__ || __WEB__
|
||||||
pick_file_save("mp4", doc_name + "-animation",
|
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-animation");
|
||||||
|
if (!target) {
|
||||||
|
message_box("Export Animation", target.status().message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_file_save("mp4", target.value().name,
|
||||||
[this](std::string path) {
|
[this](std::string path) {
|
||||||
export_anim_mp4(path);
|
export_anim_mp4(path);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,76 +1,90 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "app_core/document_platform_io.h"
|
||||||
|
#include "app_core/document_sharing.h"
|
||||||
|
#include "platform_api/platform_services.h"
|
||||||
|
#include "platform_legacy/legacy_platform_services.h"
|
||||||
|
#include "renderer_gl/opengl_capabilities.h"
|
||||||
|
|
||||||
#ifdef __ANDROID__
|
namespace {
|
||||||
void displayKeyboard(bool pShow);
|
|
||||||
void android_pick_file(std::function<void(std::string)> callback);
|
|
||||||
void android_pick_file_save(std::function<void(std::string)> callback);
|
|
||||||
std::string android_get_clipboard();
|
|
||||||
bool android_set_clipboard(const std::string& s);
|
|
||||||
#elif _WIN32
|
|
||||||
std::string win32_open_file(const char* filter);
|
|
||||||
std::string win32_save_file(const char* filter);
|
|
||||||
std::string win32_open_dir();
|
|
||||||
void win32_show_cursor(bool visible);
|
|
||||||
bool win32_clipboard_set_text(const std::string & s);
|
|
||||||
std::string win32_clipboard_get_text();
|
|
||||||
#elif __APPLE__
|
|
||||||
#elif __LINUX__
|
|
||||||
#include <tinyfiledialogs.h>
|
|
||||||
#elif __WEB__
|
|
||||||
void webgl_pick_file(std::function<void(std::string)> callback);
|
|
||||||
void webgl_pick_file_save(const std::string& path,
|
|
||||||
const std::string& name, std::function<void(bool)> callback);
|
|
||||||
void webgl_sync();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
[[nodiscard]] GLint rgba8_internal_format() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool should_dispatch_keyboard_visibility(bool visible) noexcept
|
||||||
|
{
|
||||||
|
const auto action = pp::app::plan_virtual_keyboard(visible);
|
||||||
|
return visible
|
||||||
|
? action == pp::app::VirtualKeyboardAction::show_keyboard
|
||||||
|
: action == pp::app::VirtualKeyboardAction::hide_keyboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool should_dispatch_cursor_visibility(bool visible) noexcept
|
||||||
|
{
|
||||||
|
const auto action = pp::app::plan_cursor_visibility(visible);
|
||||||
|
return visible
|
||||||
|
? action == pp::app::CursorVisibilityAction::show_cursor
|
||||||
|
: action == pp::app::CursorVisibilityAction::hide_cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] pp::platform::PlatformServices& active_platform_services()
|
||||||
|
{
|
||||||
|
if (App::I)
|
||||||
|
{
|
||||||
|
if (auto* services = App::I->platform_services())
|
||||||
|
return *services;
|
||||||
|
}
|
||||||
|
return pp::platform::legacy::platform_services();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void App::set_platform_services(pp::platform::PlatformServices* services) noexcept
|
||||||
|
{
|
||||||
|
platform_services_ = services;
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::platform::PlatformServices* App::platform_services() const noexcept
|
||||||
|
{
|
||||||
|
return platform_services_;
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::platform::PlatformStoragePaths App::prepare_storage_paths()
|
||||||
|
{
|
||||||
|
return active_platform_services().prepare_storage_paths();
|
||||||
|
}
|
||||||
|
|
||||||
std::string App::clipboard_get_text()
|
std::string App::clipboard_get_text()
|
||||||
{
|
{
|
||||||
#if _WIN32
|
if (pp::app::plan_clipboard_read() != pp::app::ClipboardReadAction::read_text)
|
||||||
return win32_clipboard_get_text();
|
return {};
|
||||||
#elif __IOS__
|
|
||||||
return [ios_view clipboard_get_string];
|
return active_platform_services().clipboard_text();
|
||||||
#elif __OSX__
|
|
||||||
return [osx_view clipboard_get_string];
|
|
||||||
#elif __ANDROID__
|
|
||||||
return android_get_clipboard();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool App::clipboard_set_text(const std::string& s)
|
bool App::clipboard_set_text(const std::string& s)
|
||||||
{
|
{
|
||||||
#if _WIN32
|
if (pp::app::plan_clipboard_write(s) != pp::app::ClipboardWriteAction::write_text)
|
||||||
return win32_clipboard_set_text(s);
|
return false;
|
||||||
#elif __IOS__
|
|
||||||
return [ios_view clipboard_set_string:s];
|
return active_platform_services().set_clipboard_text(s);
|
||||||
#elif __OSX__
|
|
||||||
return [osx_view clipboard_set_string:s];
|
|
||||||
#elif __ANDROID__
|
|
||||||
return android_set_clipboard(s);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::stacktrace()
|
void App::stacktrace()
|
||||||
{
|
{
|
||||||
#if __OSX__
|
active_platform_services().log_stacktrace();
|
||||||
NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
|
|
||||||
LOG("callstack:\n%s", [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::crash_test()
|
void App::crash_test()
|
||||||
{
|
{
|
||||||
#ifdef __IOS__
|
active_platform_services().trigger_crash_test();
|
||||||
[ios_view crash];
|
|
||||||
#elif __OSX__
|
|
||||||
[osx_view hockeyapp_crash];
|
|
||||||
#elif defined(_WIN32)
|
|
||||||
__debugbreak();
|
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
int *x = nullptr; *x = 42;
|
|
||||||
LOG("%d", *x);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::tick(float dt)
|
void App::tick(float dt)
|
||||||
@@ -84,7 +98,7 @@ void App::tick(float dt)
|
|||||||
void App::resize(float w, float h)
|
void App::resize(float w, float h)
|
||||||
{
|
{
|
||||||
LOG("App::resize %d %d", (int)w, (int)h);
|
LOG("App::resize %d %d", (int)w, (int)h);
|
||||||
uirtt.create(w, h, -1, GL_RGBA8, true);
|
uirtt.create(static_cast<int>(w), static_cast<int>(h), -1, rgba8_internal_format(), true);
|
||||||
redraw = true;
|
redraw = true;
|
||||||
width = w;
|
width = w;
|
||||||
height = h;
|
height = h;
|
||||||
@@ -92,123 +106,50 @@ void App::resize(float w, float h)
|
|||||||
|
|
||||||
void App::show_cursor()
|
void App::show_cursor()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
if (!should_dispatch_cursor_visibility(true))
|
||||||
win32_show_cursor(true);
|
return;
|
||||||
#elif __OSX__
|
|
||||||
[osx_view show_cursor:true];
|
active_platform_services().set_cursor_visible(true);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::hide_cursor()
|
void App::hide_cursor()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
if (!should_dispatch_cursor_visibility(false))
|
||||||
win32_show_cursor(false);
|
return;
|
||||||
#elif __OSX__
|
|
||||||
[osx_view show_cursor:false];
|
active_platform_services().set_cursor_visible(false);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::showKeyboard()
|
void App::showKeyboard()
|
||||||
{
|
{
|
||||||
LOG("show keyboard");
|
LOG("show keyboard");
|
||||||
redraw = true;
|
redraw = true;
|
||||||
#ifdef __IOS__
|
if (!should_dispatch_keyboard_visibility(true))
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
return;
|
||||||
[ios_view show_keyboard];
|
|
||||||
});
|
active_platform_services().set_virtual_keyboard_visible(true);
|
||||||
#elif __ANDROID__
|
|
||||||
displayKeyboard(true);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::hideKeyboard()
|
void App::hideKeyboard()
|
||||||
{
|
{
|
||||||
LOG("hide keyboard");
|
LOG("hide keyboard");
|
||||||
redraw = true;
|
redraw = true;
|
||||||
#ifdef __IOS__
|
if (!should_dispatch_keyboard_visibility(false))
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
return;
|
||||||
[ios_view hide_keyboard];
|
|
||||||
});
|
active_platform_services().set_virtual_keyboard_visible(false);
|
||||||
#elif __ANDROID__
|
|
||||||
displayKeyboard(false);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::pick_image(std::function<void(std::string path)> callback)
|
void App::pick_image(std::function<void(std::string path)> callback)
|
||||||
{
|
{
|
||||||
redraw = true;
|
redraw = true;
|
||||||
#ifdef __IOS__
|
active_platform_services().pick_image(std::move(callback));
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[ios_view pick_photo:callback];
|
|
||||||
});
|
|
||||||
#elif __OSX__
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil];
|
|
||||||
std::string path = [osx_view pick_file:fileTypes];
|
|
||||||
if (!path.empty())
|
|
||||||
callback(path);
|
|
||||||
});
|
|
||||||
#elif __ANDROID__
|
|
||||||
android_pick_file(callback);
|
|
||||||
#elif _WIN32
|
|
||||||
std::string path = win32_open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png");
|
|
||||||
if (!path.empty())
|
|
||||||
callback(path);
|
|
||||||
#elif __LINUX__
|
|
||||||
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
|
|
||||||
callback(p);
|
|
||||||
#elif __WEB__
|
|
||||||
webgl_pick_file(callback);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::pick_file(std::vector<std::string> types, std::function<void (std::string)> callback)
|
void App::pick_file(std::vector<std::string> types, std::function<void (std::string)> callback)
|
||||||
{
|
{
|
||||||
redraw = true;
|
redraw = true;
|
||||||
#ifdef __IOS__
|
active_platform_services().pick_file(std::move(types), std::move(callback));
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
|
|
||||||
for (const auto& t : types)
|
|
||||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
|
||||||
[ios_view pick_file:fileTypes then:callback];
|
|
||||||
});
|
|
||||||
#elif __OSX__
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
|
|
||||||
for (const auto& t : types)
|
|
||||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
|
||||||
std::string path = [osx_view pick_file:fileTypes];
|
|
||||||
if (!path.empty())
|
|
||||||
callback(path);
|
|
||||||
});
|
|
||||||
#elif __ANDROID__
|
|
||||||
android_pick_file(callback);
|
|
||||||
#elif _WIN32
|
|
||||||
std::string filter = "Supported Files (";
|
|
||||||
bool first_type = true;
|
|
||||||
for (auto& t : types)
|
|
||||||
{
|
|
||||||
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
|
|
||||||
first_type = false;
|
|
||||||
}
|
|
||||||
filter.append(")");
|
|
||||||
filter.push_back(0);
|
|
||||||
first_type = true;
|
|
||||||
for (auto& t : types)
|
|
||||||
{
|
|
||||||
filter.append(std::string(first_type ? "" : ";") + "*." + t);
|
|
||||||
first_type = false;
|
|
||||||
}
|
|
||||||
filter.push_back(0);
|
|
||||||
std::string path = win32_open_file(filter.c_str());
|
|
||||||
if (!path.empty())
|
|
||||||
callback(path);
|
|
||||||
#elif __LINUX__
|
|
||||||
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
|
|
||||||
callback(p);
|
|
||||||
#elif __WEB__
|
|
||||||
webgl_pick_file(callback);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if __IOS__
|
#if __IOS__
|
||||||
@@ -220,10 +161,7 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam
|
|||||||
std::string path = tmp_path + "/" + default_name + ext;
|
std::string path = tmp_path + "/" + default_name + ext;
|
||||||
std::thread([=]{
|
std::thread([=]{
|
||||||
writer(path);
|
writer(path);
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
save_prepared_file(path, default_name + ext, callback);
|
||||||
[ios_view pick_file_save:path];
|
|
||||||
});
|
|
||||||
callback(path, true);
|
|
||||||
}).detach();
|
}).detach();
|
||||||
}
|
}
|
||||||
#elif __WEB__
|
#elif __WEB__
|
||||||
@@ -234,110 +172,137 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam
|
|||||||
auto path = data_path + "/" + default_name + "." + type;
|
auto path = data_path + "/" + default_name + "." + type;
|
||||||
LOG("App::pick_file_save %s", path.c_str());
|
LOG("App::pick_file_save %s", path.c_str());
|
||||||
writer(path);
|
writer(path);
|
||||||
webgl_pick_file_save(path, default_name + "." + type, callback);
|
save_prepared_file(path, default_name + "." + type, std::move(callback));
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)
|
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)
|
||||||
{
|
{
|
||||||
redraw = true;
|
redraw = true;
|
||||||
#if __OSX__
|
active_platform_services().pick_save_file(std::move(types), std::move(callback));
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
//NSArray* fileTypes = [NSArray arrayWithObjects:@"ppi", @"PPI", nil];
|
|
||||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
|
|
||||||
for (const auto& t : types)
|
|
||||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
|
||||||
std::string path = [osx_view pick_file_save:fileTypes];
|
|
||||||
if (!path.empty())
|
|
||||||
callback(path);
|
|
||||||
});
|
|
||||||
#elif __ANDROID__
|
|
||||||
android_pick_file_save(callback);
|
|
||||||
#elif _WIN32
|
|
||||||
std::string filter = "Supported Files (";
|
|
||||||
bool first_type = true;
|
|
||||||
for (auto& t : types)
|
|
||||||
{
|
|
||||||
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
|
|
||||||
first_type = false;
|
|
||||||
}
|
|
||||||
filter.append(")");
|
|
||||||
filter.push_back(0);
|
|
||||||
first_type = true;
|
|
||||||
for (auto& t : types)
|
|
||||||
{
|
|
||||||
filter.append(std::string(first_type ? "" : ";") + "*." + t);
|
|
||||||
first_type = false;
|
|
||||||
}
|
|
||||||
filter.push_back(0);
|
|
||||||
std::string path = win32_save_file(filter.c_str());
|
|
||||||
if (!path.empty())
|
|
||||||
callback(path);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void App::pick_dir(std::function<void(std::string path)> callback)
|
void App::pick_dir(std::function<void(std::string path)> callback)
|
||||||
{
|
{
|
||||||
redraw = true;
|
redraw = true;
|
||||||
#ifdef __IOS__
|
active_platform_services().pick_directory(std::move(callback));
|
||||||
// NOT IMPLEMENTED
|
|
||||||
#elif __OSX__
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
std::string path = [osx_view pick_dir];
|
|
||||||
if (!path.empty())
|
|
||||||
callback(path);
|
|
||||||
});
|
|
||||||
#elif __ANDROID__
|
|
||||||
// NOT IMPLEMENTED
|
|
||||||
#elif _WIN32
|
|
||||||
// TODO: to be implemented
|
|
||||||
std::string path = win32_open_dir();
|
|
||||||
if (!path.empty())
|
|
||||||
callback(path);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::display_file(std::string path)
|
void App::display_file(std::string path)
|
||||||
{
|
{
|
||||||
#ifdef __IOS__
|
if (pp::app::plan_display_file(path) == pp::app::DisplayFileAction::ignore_empty_path)
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
return;
|
||||||
[ios_view display_file:path];
|
|
||||||
});
|
active_platform_services().display_file(path);
|
||||||
#elif __OSX__
|
|
||||||
[[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:path.c_str()]];
|
|
||||||
// dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
// std::string path = [osx_view pick_file];
|
|
||||||
// if (!path.empty())
|
|
||||||
// callback(path);
|
|
||||||
// });
|
|
||||||
#elif __ANDROID__
|
|
||||||
//displayKeyboard(and_app, false);
|
|
||||||
#elif _WIN32
|
|
||||||
// std::string path = win32_open_file();
|
|
||||||
// if (!path.empty())
|
|
||||||
// callback(path);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::share_file(std::string path)
|
void App::share_file(std::string path)
|
||||||
{
|
{
|
||||||
if (path.empty())
|
const auto plan = pp::app::plan_document_share(path);
|
||||||
|
if (plan == pp::app::DocumentShareAction::show_save_required_warning)
|
||||||
{
|
{
|
||||||
message_box("Sharing failed", "Please save the document before sharing it.");
|
message_box("Sharing failed", "Please save the document before sharing it.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#ifdef __IOS__
|
active_platform_services().share_file(path);
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
}
|
||||||
[ios_view share_file:[NSString stringWithUTF8String:path.c_str()]];
|
|
||||||
});
|
void App::request_app_close()
|
||||||
#elif __OSX__
|
{
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
active_platform_services().request_app_close();
|
||||||
[osx_view share_file:[NSString stringWithUTF8String:path.c_str()]];
|
}
|
||||||
});
|
|
||||||
#elif __ANDROID__
|
void App::attach_ui_thread()
|
||||||
#elif _WIN32
|
{
|
||||||
// not implemented
|
active_platform_services().attach_ui_thread();
|
||||||
#endif
|
}
|
||||||
|
|
||||||
|
void App::detach_ui_thread()
|
||||||
|
{
|
||||||
|
active_platform_services().detach_ui_thread();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::acquire_render_context()
|
||||||
|
{
|
||||||
|
active_platform_services().acquire_render_context();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::release_render_context()
|
||||||
|
{
|
||||||
|
active_platform_services().release_render_context();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::present_render_context()
|
||||||
|
{
|
||||||
|
active_platform_services().present_render_context();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::bind_default_render_target()
|
||||||
|
{
|
||||||
|
active_platform_services().bind_default_render_target();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::bind_main_render_target()
|
||||||
|
{
|
||||||
|
active_platform_services().bind_main_render_target();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::apply_render_platform_hints()
|
||||||
|
{
|
||||||
|
active_platform_services().apply_render_platform_hints();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::install_render_debug_callback()
|
||||||
|
{
|
||||||
|
active_platform_services().install_render_debug_callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::begin_render_capture_frame()
|
||||||
|
{
|
||||||
|
active_platform_services().begin_render_capture_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::end_render_capture_frame()
|
||||||
|
{
|
||||||
|
active_platform_services().end_render_capture_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::platform_deletes_recorded_files_on_clear()
|
||||||
|
{
|
||||||
|
return active_platform_services().deletes_recorded_files_on_clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::clear_platform_recorded_files(std::string path)
|
||||||
|
{
|
||||||
|
active_platform_services().clear_recorded_files(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::platform_enables_live_asset_reloading()
|
||||||
|
{
|
||||||
|
return active_platform_services().enables_live_asset_reloading();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::update_platform_frame(float delta_time_seconds)
|
||||||
|
{
|
||||||
|
active_platform_services().update_platform_frame(delta_time_seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::report_rendered_frames(int frames)
|
||||||
|
{
|
||||||
|
active_platform_services().report_rendered_frames(frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::save_prepared_file(
|
||||||
|
std::string path,
|
||||||
|
std::string suggested_name,
|
||||||
|
std::function<void(const std::string& path, bool saved)> callback)
|
||||||
|
{
|
||||||
|
active_platform_services().save_prepared_file(
|
||||||
|
path,
|
||||||
|
suggested_name,
|
||||||
|
[callback = std::move(callback)](std::string saved_path, bool saved) {
|
||||||
|
callback(saved_path, saved);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser)
|
bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser)
|
||||||
|
|||||||
1505
src/app_layout.cpp
1505
src/app_layout.cpp
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,72 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "renderer_api/shader_catalog.h"
|
||||||
|
#include "renderer_gl/opengl_capabilities.h"
|
||||||
#include "shader.h"
|
#include "shader.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum extension_count_query() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::extension_count_query());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum extension_string_name() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::extension_string_name());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::renderer::gl::OpenGlCapabilities shader_manager_capabilities() noexcept
|
||||||
|
{
|
||||||
|
pp::renderer::gl::OpenGlCapabilities capabilities;
|
||||||
|
capabilities.framebuffer_fetch = ShaderManager::ext_framebuffer_fetch;
|
||||||
|
capabilities.map_buffer_alignment = ShaderManager::ext_map_aligned;
|
||||||
|
capabilities.float32_textures = ShaderManager::ext_float32;
|
||||||
|
capabilities.float32_linear = ShaderManager::ext_float32_linear;
|
||||||
|
capabilities.float16_textures = ShaderManager::ext_float16;
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void App::initShaders()
|
void App::initShaders()
|
||||||
{
|
{
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
if (!check_uniform_uniqueness())
|
if (!check_uniform_uniqueness())
|
||||||
std::logic_error("check_uniform_uniqueness() failed");
|
LOG("check_uniform_uniqueness() failed");
|
||||||
#endif // _DEBUG
|
#endif // _DEBUG
|
||||||
|
|
||||||
render_task([] {
|
render_task([] {
|
||||||
GLint n_exts;
|
GLint n_exts;
|
||||||
glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
|
glGetIntegerv(extension_count_query(), &n_exts);
|
||||||
|
std::vector<std::string> extension_storage;
|
||||||
|
std::vector<std::string_view> extension_views;
|
||||||
|
extension_storage.reserve(n_exts);
|
||||||
|
extension_views.reserve(n_exts);
|
||||||
for (int i = 0; i < n_exts; i++)
|
for (int i = 0; i < n_exts; i++)
|
||||||
{
|
{
|
||||||
std::string ext = (const char*)glGetStringi(GL_EXTENSIONS, i);
|
extension_storage.emplace_back((const char*)glGetStringi(extension_string_name(), i));
|
||||||
if (ext.find("shader_framebuffer_fetch") != std::string::npos)
|
extension_views.push_back(extension_storage.back());
|
||||||
ShaderManager::ext_framebuffer_fetch = true;
|
LOG("EXT: %s", extension_storage.back().c_str());
|
||||||
if (ext.find("map_buffer_alignment") != std::string::npos)
|
|
||||||
ShaderManager::ext_map_aligned = true;
|
|
||||||
#if __GLES__ && !__WEB__
|
|
||||||
if (ext.find("texture_float") != std::string::npos)
|
|
||||||
ShaderManager::ext_float32 = true;
|
|
||||||
if (ext.find("texture_float_linear") != std::string::npos)
|
|
||||||
ShaderManager::ext_float32_linear = true;
|
|
||||||
if (ext.find("color_buffer_float") != std::string::npos)
|
|
||||||
ShaderManager::ext_float32 = true;
|
|
||||||
if (ext.find("texture_half_float") != std::string::npos)
|
|
||||||
ShaderManager::ext_float16 = true;
|
|
||||||
if (ext.find("color_buffer_half_float") != std::string::npos)
|
|
||||||
ShaderManager::ext_float16 = true;
|
|
||||||
#endif
|
|
||||||
LOG("EXT: %s", ext.c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pp::renderer::gl::OpenGlRuntime runtime;
|
||||||
|
#if __GL__
|
||||||
|
runtime.desktop_gl = true;
|
||||||
|
#endif
|
||||||
|
#if __GLES__
|
||||||
|
runtime.gles = true;
|
||||||
|
#endif
|
||||||
|
#if __WEB__
|
||||||
|
runtime.web = true;
|
||||||
|
#endif
|
||||||
|
const auto capabilities = pp::renderer::gl::detect_opengl_capabilities(extension_views, runtime);
|
||||||
|
ShaderManager::ext_framebuffer_fetch = capabilities.framebuffer_fetch;
|
||||||
|
ShaderManager::ext_map_aligned = capabilities.map_buffer_alignment;
|
||||||
|
ShaderManager::ext_float32 = capabilities.float32_textures;
|
||||||
|
ShaderManager::ext_float32_linear = capabilities.float32_linear;
|
||||||
|
ShaderManager::ext_float16 = capabilities.float16_textures;
|
||||||
|
ShaderManager::set_render_device_features(pp::renderer::gl::render_device_features(capabilities));
|
||||||
});
|
});
|
||||||
|
|
||||||
#if __GL__
|
#if __GL__
|
||||||
@@ -41,60 +75,25 @@ void App::initShaders()
|
|||||||
ShaderManager::ext_float32 = true;
|
ShaderManager::ext_float32 = true;
|
||||||
ShaderManager::ext_float16 = true;
|
ShaderManager::ext_float16 = true;
|
||||||
#endif
|
#endif
|
||||||
|
ShaderManager::set_render_device_features(
|
||||||
|
pp::renderer::gl::render_device_features(shader_manager_capabilities()));
|
||||||
|
|
||||||
LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled");
|
LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled");
|
||||||
|
|
||||||
LOG("initializing shaders");
|
LOG("initializing shaders");
|
||||||
if (!ShaderManager::load(kShader::Texture, "data/shaders/texture.glsl"))
|
const auto shader_catalog = pp::renderer::panopainter_shader_catalog();
|
||||||
LOG("Failed to create shader Texture");
|
const auto catalog_status = pp::renderer::validate_shader_catalog(shader_catalog);
|
||||||
if (!ShaderManager::load(kShader::TextureAlpha, "data/shaders/texture-alpha.glsl"))
|
if (!catalog_status.ok())
|
||||||
LOG("Failed to create shader TextureAlpha");
|
{
|
||||||
if (!ShaderManager::load(kShader::TextureMask, "data/shaders/texture-mask.glsl"))
|
LOG("Shader catalog validation failed: %s", catalog_status.message);
|
||||||
LOG("Failed to create shader TextureMask");
|
return;
|
||||||
if (!ShaderManager::load(kShader::TextureColorize, "data/shaders/texture-colorize.glsl"))
|
}
|
||||||
LOG("Failed to create shader TextureColorize");
|
|
||||||
if (!ShaderManager::load(kShader::TextureBlend, "data/shaders/texture-blend.glsl"))
|
for (const auto& shader : shader_catalog)
|
||||||
LOG("Failed to create shader TextureBlend");
|
{
|
||||||
if (!ShaderManager::load(kShader::StrokePreview, "data/shaders/stroke-preview.glsl"))
|
if (!ShaderManager::load(static_cast<kShader>(const_hash(shader.name)), shader.path))
|
||||||
LOG("Failed to create shader StrokePreview");
|
LOG("Failed to create shader %s", shader.name);
|
||||||
if (!ShaderManager::load(kShader::CompErase, "data/shaders/comp-erase.glsl"))
|
}
|
||||||
LOG("Failed to create shader CompErase");
|
|
||||||
if (!ShaderManager::load(kShader::CompDraw, "data/shaders/comp-draw.glsl"))
|
|
||||||
LOG("Failed to create shader CompDraw");
|
|
||||||
if (!ShaderManager::load(kShader::Color, "data/shaders/color.glsl"))
|
|
||||||
LOG("Failed to create shader Color");
|
|
||||||
if (!ShaderManager::load(kShader::ColorQuad, "data/shaders/color-quad.glsl"))
|
|
||||||
LOG("Failed to create shader ColorQuad");
|
|
||||||
if (!ShaderManager::load(kShader::ColorTri, "data/shaders/color-tri.glsl"))
|
|
||||||
LOG("Failed to create shader ColorTri");
|
|
||||||
if (!ShaderManager::load(kShader::ColorHue, "data/shaders/color-hue.glsl"))
|
|
||||||
LOG("Failed to create shader ColorHue");
|
|
||||||
if (!ShaderManager::load(kShader::UVs, "data/shaders/uvs.glsl"))
|
|
||||||
LOG("Failed to create shader UVs");
|
|
||||||
if (!ShaderManager::load(kShader::Font, "data/shaders/font.glsl"))
|
|
||||||
LOG("Failed to create shader Font");
|
|
||||||
if (!ShaderManager::load(kShader::Atlas, "data/shaders/atlas.glsl"))
|
|
||||||
LOG("Failed to create shader Atlas");
|
|
||||||
if (!ShaderManager::load(kShader::Stroke, "data/shaders/stroke.glsl"))
|
|
||||||
LOG("Failed to create shader Stroke");
|
|
||||||
if (!ShaderManager::load(kShader::StrokePad, "data/shaders/stroke-pad.glsl"))
|
|
||||||
LOG("Failed to create shader StrokePad");
|
|
||||||
if (!ShaderManager::load(kShader::StrokeDilate, "data/shaders/stroke-dilate.glsl"))
|
|
||||||
LOG("Failed to create shader StrokeDilate");
|
|
||||||
if (!ShaderManager::load(kShader::Checkerboard, "data/shaders/checkerboard.glsl"))
|
|
||||||
LOG("Failed to create shader Checkerboard");
|
|
||||||
if (!ShaderManager::load(kShader::Equirect, "data/shaders/equirect.glsl"))
|
|
||||||
LOG("Failed to create shader Equirect");
|
|
||||||
if (!ShaderManager::load(kShader::BrushStroke, "data/shaders/stroke-instanced.glsl"))
|
|
||||||
LOG("Failed to create shader BrushStroke");
|
|
||||||
if (!ShaderManager::load(kShader::VertexColor, "data/shaders/vertex-color.glsl"))
|
|
||||||
LOG("Failed to create shader VertexColor");
|
|
||||||
if (!ShaderManager::load(kShader::Lambert, "data/shaders/lambert.glsl"))
|
|
||||||
LOG("Failed to create shader Lambert");
|
|
||||||
if (!ShaderManager::load(kShader::LambertLightmap, "data/shaders/lightmap.glsl"))
|
|
||||||
LOG("Failed to create shader LambertLightmap");
|
|
||||||
if (!ShaderManager::load(kShader::BakeUV, "data/shaders/bake-uv.glsl"))
|
|
||||||
LOG("Failed to create shader BakeUV");
|
|
||||||
LOG("shaders initialized");
|
LOG("shaders initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
160
src/app_vr.cpp
160
src/app_vr.cpp
@@ -1,13 +1,98 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "shape.h"
|
#include "shape.h"
|
||||||
|
#include "renderer_gl/opengl_capabilities.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
bool win32_vr_start();
|
bool win32_vr_start();
|
||||||
void win32_vr_stop();
|
void win32_vr_stop();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void set_active_texture_unit(std::uint32_t unit_index)
|
||||||
|
{
|
||||||
|
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
|
||||||
|
}
|
||||||
|
|
||||||
|
void unbind_texture_2d()
|
||||||
|
{
|
||||||
|
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void enable_opengl_state(std::uint32_t state) noexcept
|
||||||
|
{
|
||||||
|
glEnable(static_cast<GLenum>(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable_opengl_state(std::uint32_t state) noexcept
|
||||||
|
{
|
||||||
|
glDisable(static_cast<GLenum>(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
|
||||||
|
{
|
||||||
|
glViewport(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_opengl_mask(std::uint32_t mask) noexcept
|
||||||
|
{
|
||||||
|
glClear(static_cast<GLbitfield>(mask));
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
|
||||||
|
{
|
||||||
|
const auto status = pp::renderer::gl::apply_opengl_viewport(
|
||||||
|
viewport,
|
||||||
|
pp::renderer::gl::OpenGlViewportDispatch {
|
||||||
|
.viewport = set_opengl_viewport,
|
||||||
|
});
|
||||||
|
if (!status.ok())
|
||||||
|
LOG("OpenGL VR UI viewport failed: %s", status.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_vr_ui_scissor_test(bool enabled)
|
||||||
|
{
|
||||||
|
const auto status = pp::renderer::gl::apply_opengl_scissor_test(
|
||||||
|
enabled,
|
||||||
|
pp::renderer::gl::OpenGlScissorTestDispatch {
|
||||||
|
.enable = enable_opengl_state,
|
||||||
|
.disable = disable_opengl_state,
|
||||||
|
});
|
||||||
|
if (!status.ok())
|
||||||
|
LOG("OpenGL VR UI scissor test failed: %s", status.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_vr_render_capability(std::uint32_t state, bool enabled)
|
||||||
|
{
|
||||||
|
const auto status = pp::renderer::gl::apply_opengl_capability(
|
||||||
|
state,
|
||||||
|
enabled,
|
||||||
|
pp::renderer::gl::OpenGlCapabilityDispatch {
|
||||||
|
.enable = enable_opengl_state,
|
||||||
|
.disable = disable_opengl_state,
|
||||||
|
});
|
||||||
|
if (!status.ok())
|
||||||
|
LOG("OpenGL VR render state failed: %s", status.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_vr_depth_buffer()
|
||||||
|
{
|
||||||
|
const auto status = pp::renderer::gl::clear_opengl_buffers(
|
||||||
|
pp::renderer::gl::framebuffer_depth_buffer_mask(),
|
||||||
|
pp::renderer::gl::OpenGlBufferClearDispatch {
|
||||||
|
.clear = clear_opengl_mask,
|
||||||
|
});
|
||||||
|
if (!status.ok())
|
||||||
|
LOG("OpenGL VR depth clear failed: %s", status.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
bool trigger_down = false;
|
bool trigger_down = false;
|
||||||
cbuffer<glm::vec3> controller_points(10);
|
cbuffer<glm::vec3> controller_points(10);
|
||||||
glm::vec3 controller_last_point;
|
glm::vec3 controller_last_point;
|
||||||
@@ -37,13 +122,16 @@ void App::vr_draw_ui()
|
|||||||
{
|
{
|
||||||
uirtt.bindFramebuffer();
|
uirtt.bindFramebuffer();
|
||||||
uirtt.clear();
|
uirtt.clear();
|
||||||
glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight());
|
apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect {
|
||||||
glEnable(GL_SCISSOR_TEST);
|
.width = static_cast<std::int32_t>(uirtt.getWidth()),
|
||||||
|
.height = static_cast<std::int32_t>(uirtt.getHeight()),
|
||||||
|
});
|
||||||
|
apply_vr_ui_scissor_test(true);
|
||||||
auto observer = std::bind(&App::update_ui_observer, this, std::placeholders::_1);
|
auto observer = std::bind(&App::update_ui_observer, this, std::placeholders::_1);
|
||||||
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
|
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
|
||||||
layout[main_id]->m_children[i]->watch(observer);
|
layout[main_id]->m_children[i]->watch(observer);
|
||||||
//msgbox->watch(observer);
|
//msgbox->watch(observer);
|
||||||
glDisable(GL_SCISSOR_TEST);
|
apply_vr_ui_scissor_test(false);
|
||||||
uirtt.unbindFramebuffer();
|
uirtt.unbindFramebuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,12 +273,12 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
glm::vec3 origin = glm::vec3(0, 0, -1) * glm::transpose(glm::mat3(pose));
|
glm::vec3 origin = glm::vec3(0, 0, -1) * glm::transpose(glm::mat3(pose));
|
||||||
vr_rot = glm::lookAt({ 0, 0, 0 }, origin, { 0, 1, 0 });
|
vr_rot = glm::lookAt({ 0, 0, 0 }, origin, { 0, 1, 0 });
|
||||||
|
|
||||||
auto blend = glIsEnabled(GL_BLEND);
|
auto blend = glIsEnabled(pp::renderer::gl::blend_state());
|
||||||
auto depth = glIsEnabled(GL_DEPTH_TEST);
|
auto depth = glIsEnabled(pp::renderer::gl::depth_test_state());
|
||||||
|
|
||||||
glDisable(GL_BLEND);
|
apply_vr_render_capability(pp::renderer::gl::blend_state(), false);
|
||||||
glDisable(GL_DEPTH_TEST);
|
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
|
||||||
glClear(GL_DEPTH_BUFFER_BIT);
|
clear_vr_depth_buffer();
|
||||||
|
|
||||||
for (int plane_index = 0; plane_index < 6; plane_index++)
|
for (int plane_index = 0; plane_index < 6; plane_index++)
|
||||||
{
|
{
|
||||||
@@ -205,9 +293,9 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
m_face_plane.draw_fill();
|
m_face_plane.draw_fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
glEnable(GL_BLEND);
|
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||||
glEnable(GL_DEPTH_TEST);
|
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), true);
|
||||||
glClear(GL_DEPTH_BUFFER_BIT);
|
clear_vr_depth_buffer();
|
||||||
|
|
||||||
for (size_t i = 0; i < canvas->m_canvas->m_layers.size(); i++)
|
for (size_t i = 0; i < canvas->m_canvas->m_layers.size(); i++)
|
||||||
{
|
{
|
||||||
@@ -241,17 +329,17 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
//ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
|
//ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
|
||||||
ShaderManager::u_int(kShaderUniform::Mask, canvas->m_canvas->m_smask_active);
|
ShaderManager::u_int(kShaderUniform::Mask, canvas->m_canvas->m_smask_active);
|
||||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||||
glActiveTexture(GL_TEXTURE1);
|
set_active_texture_unit(1);
|
||||||
canvas->m_canvas->m_tmp[plane_index].bindTexture();
|
canvas->m_canvas->m_tmp[plane_index].bindTexture();
|
||||||
glActiveTexture(GL_TEXTURE2);
|
set_active_texture_unit(2);
|
||||||
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
|
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
|
||||||
m_face_plane.draw_fill();
|
m_face_plane.draw_fill();
|
||||||
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
|
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
|
||||||
glActiveTexture(GL_TEXTURE1);
|
set_active_texture_unit(1);
|
||||||
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
|
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||||
}
|
}
|
||||||
else if (canvas->m_canvas->m_show_tmp && canvas->m_canvas->m_current_layer_idx == layer_index)
|
else if (canvas->m_canvas->m_show_tmp && canvas->m_canvas->m_current_layer_idx == layer_index)
|
||||||
@@ -292,28 +380,28 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
|
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
|
||||||
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
|
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||||
glActiveTexture(GL_TEXTURE1);
|
set_active_texture_unit(1);
|
||||||
canvas->m_canvas->m_tmp[plane_index].bindTexture();
|
canvas->m_canvas->m_tmp[plane_index].bindTexture();
|
||||||
glActiveTexture(GL_TEXTURE2);
|
set_active_texture_unit(2);
|
||||||
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
|
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
|
||||||
glActiveTexture(GL_TEXTURE3);
|
set_active_texture_unit(3);
|
||||||
if (b->m_dual_enabled)
|
if (b->m_dual_enabled)
|
||||||
canvas->m_canvas->m_tmp_dual[plane_index].bindTexture();
|
canvas->m_canvas->m_tmp_dual[plane_index].bindTexture();
|
||||||
glActiveTexture(GL_TEXTURE4);
|
set_active_texture_unit(4);
|
||||||
b->m_pattern_texture ?
|
b->m_pattern_texture ?
|
||||||
b->m_pattern_texture->bind() :
|
b->m_pattern_texture->bind() :
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
unbind_texture_2d();
|
||||||
m_face_plane.draw_fill();
|
m_face_plane.draw_fill();
|
||||||
glActiveTexture(GL_TEXTURE3);
|
set_active_texture_unit(3);
|
||||||
if (b->m_dual_enabled)
|
if (b->m_dual_enabled)
|
||||||
canvas->m_canvas->m_tmp_dual[plane_index].unbindTexture();
|
canvas->m_canvas->m_tmp_dual[plane_index].unbindTexture();
|
||||||
glActiveTexture(GL_TEXTURE2);
|
set_active_texture_unit(2);
|
||||||
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
|
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
|
||||||
glActiveTexture(GL_TEXTURE1);
|
set_active_texture_unit(1);
|
||||||
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
|
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -325,7 +413,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
ShaderManager::u_int(kShaderUniform::Highlight, canvas->m_canvas->m_layers[layer_index]->m_hightlight);
|
ShaderManager::u_int(kShaderUniform::Highlight, canvas->m_canvas->m_layers[layer_index]->m_hightlight);
|
||||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||||
m_face_plane.draw_fill();
|
m_face_plane.draw_fill();
|
||||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||||
@@ -352,7 +440,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
m_face_plane.draw_stroke();
|
m_face_plane.draw_stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
|
||||||
// draw the brush
|
// draw the brush
|
||||||
/*
|
/*
|
||||||
auto mode = dynamic_cast<CanvasModePen*>(canvas->m_canvas->modes[(int)canvas->m_canvas->m_current_mode][0]);
|
auto mode = dynamic_cast<CanvasModePen*>(canvas->m_canvas->modes[(int)canvas->m_canvas->m_current_mode][0]);
|
||||||
@@ -377,8 +465,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size / height)) *
|
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size / height)) *
|
||||||
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
|
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
|
||||||
);
|
);
|
||||||
glEnable(GL_BLEND);
|
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
|
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
|
||||||
tex.bind();
|
tex.bind();
|
||||||
sampler_linear.bind(0);
|
sampler_linear.bind(0);
|
||||||
@@ -399,7 +487,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
ShaderManager::use(kShader::Texture);
|
ShaderManager::use(kShader::Texture);
|
||||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
uirtt.bindTexture();
|
uirtt.bindTexture();
|
||||||
m_face_plane.draw_fill();
|
m_face_plane.draw_fill();
|
||||||
uirtt.unbindTexture();
|
uirtt.unbindTexture();
|
||||||
@@ -451,8 +539,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size * 100.f / height)) *
|
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size * 100.f / height)) *
|
||||||
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
|
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
|
||||||
);
|
);
|
||||||
glEnable(GL_BLEND);
|
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
|
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
|
||||||
tex.bind();
|
tex.bind();
|
||||||
sampler_linear.bind(0);
|
sampler_linear.bind(0);
|
||||||
@@ -466,7 +554,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
for (auto& mode : *canvas->m_canvas->m_mode)
|
for (auto& mode : *canvas->m_canvas->m_mode)
|
||||||
mode->on_Draw(ortho_proj, proj, camera);
|
mode->on_Draw(ortho_proj, proj, camera);
|
||||||
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(pp::renderer::gl::depth_test_state());
|
||||||
if (canvas->m_canvas->m_smask_active)
|
if (canvas->m_canvas->m_smask_active)
|
||||||
{
|
{
|
||||||
canvas->m_canvas->modes[(int)kCanvasMode::MaskFree][0]->on_Draw(ortho_proj, proj, camera);
|
canvas->m_canvas->modes[(int)kCanvasMode::MaskFree][0]->on_Draw(ortho_proj, proj, camera);
|
||||||
@@ -479,8 +567,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
|||||||
mode->on_Draw(ortho_proj, proj, camera);
|
mode->on_Draw(ortho_proj, proj, camera);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
apply_vr_render_capability(pp::renderer::gl::blend_state(), blend != 0U);
|
||||||
depth ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
|
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), depth != 0U);
|
||||||
sampler.unbind();
|
sampler.unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
94
src/assets/image_format.cpp
Normal file
94
src/assets/image_format.cpp
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#include "assets/image_format.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::array png_signature {
|
||||||
|
std::byte { 0x89 },
|
||||||
|
std::byte { 0x50 },
|
||||||
|
std::byte { 0x4e },
|
||||||
|
std::byte { 0x47 },
|
||||||
|
std::byte { 0x0d },
|
||||||
|
std::byte { 0x0a },
|
||||||
|
std::byte { 0x1a },
|
||||||
|
std::byte { 0x0a },
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] bool starts_with(std::span<const std::byte> bytes, std::span<const std::byte> prefix) noexcept
|
||||||
|
{
|
||||||
|
if (bytes.size() < prefix.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < prefix.size(); ++i) {
|
||||||
|
if (bytes[i] != prefix[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool is_prefix_of(std::span<const std::byte> bytes, std::span<const std::byte> signature) noexcept
|
||||||
|
{
|
||||||
|
if (bytes.size() >= signature.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < bytes.size(); ++i) {
|
||||||
|
if (bytes[i] != signature[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<ImageFormat> detect_image_format(std::span<const std::byte> bytes) noexcept
|
||||||
|
{
|
||||||
|
if (bytes.empty()) {
|
||||||
|
return pp::foundation::Result<ImageFormat>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("image data must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (starts_with(bytes, png_signature)) {
|
||||||
|
return pp::foundation::Result<ImageFormat>::success(ImageFormat::png);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_prefix_of(bytes, png_signature)) {
|
||||||
|
return pp::foundation::Result<ImageFormat>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("image data is a truncated PNG signature"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes.size() < 3U) {
|
||||||
|
return pp::foundation::Result<ImageFormat>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("image data is too short to identify"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes[0] == std::byte { 0xff } && bytes[1] == std::byte { 0xd8 } && bytes[2] == std::byte { 0xff }) {
|
||||||
|
return pp::foundation::Result<ImageFormat>::success(ImageFormat::jpeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<ImageFormat>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("unsupported image signature"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* image_format_name(ImageFormat format) noexcept
|
||||||
|
{
|
||||||
|
switch (format) {
|
||||||
|
case ImageFormat::png:
|
||||||
|
return "png";
|
||||||
|
case ImageFormat::jpeg:
|
||||||
|
return "jpeg";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
src/assets/image_format.h
Normal file
20
src/assets/image_format.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
enum class ImageFormat {
|
||||||
|
png,
|
||||||
|
jpeg,
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<ImageFormat> detect_image_format(
|
||||||
|
std::span<const std::byte> bytes) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] const char* image_format_name(ImageFormat format) noexcept;
|
||||||
|
|
||||||
|
}
|
||||||
146
src/assets/image_metadata.cpp
Normal file
146
src/assets/image_metadata.cpp
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#include "assets/image_metadata.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::byte png_signature[] {
|
||||||
|
std::byte { 0x89 },
|
||||||
|
std::byte { 0x50 },
|
||||||
|
std::byte { 0x4e },
|
||||||
|
std::byte { 0x47 },
|
||||||
|
std::byte { 0x0d },
|
||||||
|
std::byte { 0x0a },
|
||||||
|
std::byte { 0x1a },
|
||||||
|
std::byte { 0x0a },
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] bool has_png_signature(std::span<const std::byte> bytes) noexcept
|
||||||
|
{
|
||||||
|
if (bytes.size() < 8U) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < 8U; ++i) {
|
||||||
|
if (bytes[i] != png_signature[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint32_t read_u32_be(std::span<const std::byte> bytes, std::size_t offset) noexcept
|
||||||
|
{
|
||||||
|
return (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset])) << 24U)
|
||||||
|
| (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 1U])) << 16U)
|
||||||
|
| (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 2U])) << 8U)
|
||||||
|
| static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 3U]));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<ImageColorType> parse_png_color_type(std::byte value) noexcept
|
||||||
|
{
|
||||||
|
switch (std::to_integer<std::uint8_t>(value)) {
|
||||||
|
case 0:
|
||||||
|
return pp::foundation::Result<ImageColorType>::success(ImageColorType::grayscale);
|
||||||
|
case 2:
|
||||||
|
return pp::foundation::Result<ImageColorType>::success(ImageColorType::rgb);
|
||||||
|
case 3:
|
||||||
|
return pp::foundation::Result<ImageColorType>::success(ImageColorType::indexed);
|
||||||
|
case 4:
|
||||||
|
return pp::foundation::Result<ImageColorType>::success(ImageColorType::grayscale_alpha);
|
||||||
|
case 6:
|
||||||
|
return pp::foundation::Result<ImageColorType>::success(ImageColorType::rgba);
|
||||||
|
default:
|
||||||
|
return pp::foundation::Result<ImageColorType>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PNG color type is unsupported"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint8_t component_count(ImageColorType color_type) noexcept
|
||||||
|
{
|
||||||
|
switch (color_type) {
|
||||||
|
case ImageColorType::grayscale:
|
||||||
|
case ImageColorType::indexed:
|
||||||
|
return 1;
|
||||||
|
case ImageColorType::grayscale_alpha:
|
||||||
|
return 2;
|
||||||
|
case ImageColorType::rgb:
|
||||||
|
return 3;
|
||||||
|
case ImageColorType::rgba:
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<ImageMetadata> parse_png_metadata(std::span<const std::byte> bytes) noexcept
|
||||||
|
{
|
||||||
|
constexpr std::size_t png_ihdr_end = 33;
|
||||||
|
if (bytes.size() < png_ihdr_end) {
|
||||||
|
return pp::foundation::Result<ImageMetadata>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PNG metadata is truncated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!has_png_signature(bytes)) {
|
||||||
|
return pp::foundation::Result<ImageMetadata>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PNG signature is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ihdr_length = read_u32_be(bytes, 8);
|
||||||
|
if (ihdr_length != 13U || bytes[12] != std::byte { 'I' } || bytes[13] != std::byte { 'H' }
|
||||||
|
|| bytes[14] != std::byte { 'D' } || bytes[15] != std::byte { 'R' }) {
|
||||||
|
return pp::foundation::Result<ImageMetadata>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PNG IHDR chunk is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto width = read_u32_be(bytes, 16);
|
||||||
|
const auto height = read_u32_be(bytes, 20);
|
||||||
|
if (width == 0 || height == 0 || width > max_image_dimension || height > max_image_dimension) {
|
||||||
|
return pp::foundation::Result<ImageMetadata>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PNG dimensions are outside the configured range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto bit_depth = std::to_integer<std::uint8_t>(bytes[24]);
|
||||||
|
if (bit_depth == 0U) {
|
||||||
|
return pp::foundation::Result<ImageMetadata>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PNG bit depth is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto color_type = parse_png_color_type(bytes[25]);
|
||||||
|
if (!color_type) {
|
||||||
|
return pp::foundation::Result<ImageMetadata>::failure(color_type.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<ImageMetadata>::success(ImageMetadata {
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.bit_depth = bit_depth,
|
||||||
|
.components = component_count(color_type.value()),
|
||||||
|
.color_type = color_type.value(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* image_color_type_name(ImageColorType color_type) noexcept
|
||||||
|
{
|
||||||
|
switch (color_type) {
|
||||||
|
case ImageColorType::grayscale:
|
||||||
|
return "grayscale";
|
||||||
|
case ImageColorType::rgb:
|
||||||
|
return "rgb";
|
||||||
|
case ImageColorType::indexed:
|
||||||
|
return "indexed";
|
||||||
|
case ImageColorType::grayscale_alpha:
|
||||||
|
return "grayscale_alpha";
|
||||||
|
case ImageColorType::rgba:
|
||||||
|
return "rgba";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
src/assets/image_metadata.h
Normal file
34
src/assets/image_metadata.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
constexpr std::uint32_t max_image_dimension = 262144;
|
||||||
|
|
||||||
|
enum class ImageColorType : std::uint8_t {
|
||||||
|
grayscale,
|
||||||
|
rgb,
|
||||||
|
indexed,
|
||||||
|
grayscale_alpha,
|
||||||
|
rgba,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ImageMetadata {
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::uint8_t bit_depth = 0;
|
||||||
|
std::uint8_t components = 0;
|
||||||
|
ImageColorType color_type = ImageColorType::rgba;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<ImageMetadata> parse_png_metadata(
|
||||||
|
std::span<const std::byte> bytes) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] const char* image_color_type_name(ImageColorType color_type) noexcept;
|
||||||
|
|
||||||
|
}
|
||||||
159
src/assets/image_pixels.cpp
Normal file
159
src/assets/image_pixels.cpp
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
#include "assets/image_pixels.h"
|
||||||
|
|
||||||
|
#include "assets/image_metadata.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#define STB_IMAGE_STATIC
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include <stb/stb_image.h>
|
||||||
|
|
||||||
|
#define STB_IMAGE_WRITE_STATIC
|
||||||
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
|
#include <stb/stb_image_write.h>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::size_t> rgba_byte_size(
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height) noexcept
|
||||||
|
{
|
||||||
|
const auto pixels = static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height);
|
||||||
|
constexpr auto channels = 4ULL;
|
||||||
|
if (pixels > std::numeric_limits<std::uint64_t>::max() / channels) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("RGBA byte size overflows"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto bytes = pixels * channels;
|
||||||
|
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("RGBA byte size exceeds addressable memory"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_png_bytes(void* context, void* data, int size)
|
||||||
|
{
|
||||||
|
if (context == nullptr || data == nullptr || size <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* bytes = static_cast<std::vector<std::byte>*>(context);
|
||||||
|
const auto* begin = static_cast<const std::byte*>(data);
|
||||||
|
bytes->insert(bytes->end(), begin, begin + size);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<Rgba8Image> decode_png_rgba8(std::span<const std::byte> bytes)
|
||||||
|
{
|
||||||
|
const auto metadata = parse_png_metadata(bytes);
|
||||||
|
if (!metadata) {
|
||||||
|
return pp::foundation::Result<Rgba8Image>::failure(metadata.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||||
|
return pp::foundation::Result<Rgba8Image>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PNG payload is too large for the decoder"));
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
int source_components = 0;
|
||||||
|
auto* decoded = stbi_load_from_memory(
|
||||||
|
reinterpret_cast<const stbi_uc*>(bytes.data()),
|
||||||
|
static_cast<int>(bytes.size()),
|
||||||
|
&width,
|
||||||
|
&height,
|
||||||
|
&source_components,
|
||||||
|
4);
|
||||||
|
if (decoded == nullptr) {
|
||||||
|
return pp::foundation::Result<Rgba8Image>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PNG payload could not be decoded"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto cleanup = [decoded]() noexcept {
|
||||||
|
stbi_image_free(decoded);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0
|
||||||
|
|| static_cast<std::uint32_t>(width) != metadata.value().width
|
||||||
|
|| static_cast<std::uint32_t>(height) != metadata.value().height) {
|
||||||
|
cleanup();
|
||||||
|
return pp::foundation::Result<Rgba8Image>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("decoded PNG dimensions are inconsistent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto byte_count = rgba_byte_size(metadata.value().width, metadata.value().height);
|
||||||
|
if (!byte_count) {
|
||||||
|
cleanup();
|
||||||
|
return pp::foundation::Result<Rgba8Image>::failure(byte_count.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
Rgba8Image image {
|
||||||
|
.width = metadata.value().width,
|
||||||
|
.height = metadata.value().height,
|
||||||
|
.pixels = {},
|
||||||
|
};
|
||||||
|
image.pixels.assign(decoded, decoded + byte_count.value());
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
return pp::foundation::Result<Rgba8Image>::success(std::move(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<std::vector<std::byte>> encode_png_rgba8(
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height,
|
||||||
|
std::span<const std::uint8_t> pixels)
|
||||||
|
{
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PNG dimensions must be greater than zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width > static_cast<std::uint32_t>(std::numeric_limits<int>::max())
|
||||||
|
|| height > static_cast<std::uint32_t>(std::numeric_limits<int>::max())) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PNG dimensions exceed encoder limits"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto byte_count = rgba_byte_size(width, height);
|
||||||
|
if (!byte_count) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(byte_count.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixels.size() != byte_count.value()) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("RGBA pixel payload size does not match dimensions"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto stride = static_cast<std::uint64_t>(width) * 4ULL;
|
||||||
|
if (stride > static_cast<std::uint64_t>(std::numeric_limits<int>::max())) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PNG row stride exceeds encoder limits"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::byte> encoded;
|
||||||
|
const auto result = stbi_write_png_to_func(
|
||||||
|
append_png_bytes,
|
||||||
|
&encoded,
|
||||||
|
static_cast<int>(width),
|
||||||
|
static_cast<int>(height),
|
||||||
|
4,
|
||||||
|
pixels.data(),
|
||||||
|
static_cast<int>(stride));
|
||||||
|
|
||||||
|
if (result == 0 || encoded.empty()) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("RGBA pixels could not be encoded as PNG"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::success(std::move(encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
src/assets/image_pixels.h
Normal file
26
src/assets/image_pixels.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
struct Rgba8Image {
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::vector<std::uint8_t> pixels;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<Rgba8Image> decode_png_rgba8(
|
||||||
|
std::span<const std::byte> bytes);
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> encode_png_rgba8(
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height,
|
||||||
|
std::span<const std::uint8_t> pixels);
|
||||||
|
|
||||||
|
}
|
||||||
888
src/assets/ppi_header.cpp
Normal file
888
src/assets/ppi_header.cpp
Normal file
@@ -0,0 +1,888 @@
|
|||||||
|
#include "assets/ppi_header.h"
|
||||||
|
|
||||||
|
#include "assets/image_metadata.h"
|
||||||
|
#include "foundation/binary_stream.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <bit>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_u32(pp::foundation::ByteReader& reader) noexcept
|
||||||
|
{
|
||||||
|
return reader.read_u32_le();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_positive_i32(
|
||||||
|
pp::foundation::ByteReader& reader,
|
||||||
|
const char* message) noexcept
|
||||||
|
{
|
||||||
|
const auto value = reader.read_u32_le();
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.value() > static_cast<std::uint32_t>(std::numeric_limits<std::int32_t>::max())) {
|
||||||
|
return pp::foundation::Result<std::uint32_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<float> read_f32(pp::foundation::ByteReader& reader) noexcept
|
||||||
|
{
|
||||||
|
const auto bits = reader.read_u32_le();
|
||||||
|
if (!bits) {
|
||||||
|
return pp::foundation::Result<float>::failure(bits.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<float>::success(std::bit_cast<float>(bits.value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_u32(std::vector<std::byte>& bytes, std::uint32_t value)
|
||||||
|
{
|
||||||
|
bytes.push_back(static_cast<std::byte>(value & 0xffU));
|
||||||
|
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
||||||
|
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
|
||||||
|
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_f32(std::vector<std::byte>& bytes, float value)
|
||||||
|
{
|
||||||
|
append_u32(bytes, std::bit_cast<std::uint32_t>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_ascii(std::vector<std::byte>& bytes, std::string_view value)
|
||||||
|
{
|
||||||
|
for (const auto ch : value) {
|
||||||
|
bytes.push_back(static_cast<std::byte>(ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status skip_bytes(
|
||||||
|
pp::foundation::ByteReader& reader,
|
||||||
|
std::size_t bytes) noexcept
|
||||||
|
{
|
||||||
|
const auto skipped = reader.read_bytes(bytes);
|
||||||
|
if (!skipped) {
|
||||||
|
return skipped.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_canvas_size(std::uint32_t width, std::uint32_t height) noexcept
|
||||||
|
{
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("PPI canvas dimensions must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width > max_ppi_canvas_dimension || height > max_ppi_canvas_dimension) {
|
||||||
|
return pp::foundation::Status::out_of_range("PPI canvas dimensions exceed the configured limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::string generated_layer_name(std::string_view base_name, std::uint32_t layer_index, std::uint32_t layer_count)
|
||||||
|
{
|
||||||
|
if (layer_count == 1U) {
|
||||||
|
return std::string(base_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string name(base_name);
|
||||||
|
name.push_back(' ');
|
||||||
|
name += std::to_string(layer_index + 1U);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status add_payload_bytes(PpiBodySummary& summary, std::uint32_t bytes) noexcept
|
||||||
|
{
|
||||||
|
const auto next = summary.compressed_face_bytes + static_cast<std::uint64_t>(bytes);
|
||||||
|
if (next > max_ppi_face_payload_bytes) {
|
||||||
|
return pp::foundation::Status::out_of_range("PPI compressed face payload exceeds the configured limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.compressed_face_bytes = next;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<ImageMetadata> validate_face_png_payload(
|
||||||
|
std::span<const std::byte> payload,
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height) noexcept
|
||||||
|
{
|
||||||
|
const auto metadata = parse_png_metadata(payload);
|
||||||
|
if (!metadata) {
|
||||||
|
return pp::foundation::Result<ImageMetadata>::failure(metadata.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.value().width != width || metadata.value().height != height) {
|
||||||
|
return pp::foundation::Result<ImageMetadata>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI face PNG dimensions do not match the dirty box"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.value().bit_depth != 8U || metadata.value().components != 4U
|
||||||
|
|| metadata.value().color_type != ImageColorType::rgba) {
|
||||||
|
return pp::foundation::Result<ImageMetadata>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI face PNG payload must be 8-bit RGBA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<ImageMetadata>::success(metadata.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(std::span<const std::byte> bytes) noexcept
|
||||||
|
{
|
||||||
|
if (bytes.size() < ppi_header_size) {
|
||||||
|
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI header is truncated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::ByteReader reader(bytes.subspan(0, ppi_header_size));
|
||||||
|
const auto magic = reader.read_bytes(4);
|
||||||
|
if (!magic || magic.value()[0] != std::byte { 'P' } || magic.value()[1] != std::byte { 'P' }
|
||||||
|
|| magic.value()[2] != std::byte { 'I' } || magic.value()[3] != std::byte { 0 }) {
|
||||||
|
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI header magic is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
PpiHeaderInfo info;
|
||||||
|
const auto doc_major = read_u32(reader);
|
||||||
|
const auto doc_minor = read_u32(reader);
|
||||||
|
const auto soft_major = read_u32(reader);
|
||||||
|
const auto soft_minor = read_u32(reader);
|
||||||
|
const auto soft_fix = read_u32(reader);
|
||||||
|
const auto soft_build = read_u32(reader);
|
||||||
|
const auto thumb_width = read_u32(reader);
|
||||||
|
const auto thumb_height = read_u32(reader);
|
||||||
|
const auto thumb_components = read_u32(reader);
|
||||||
|
if (!doc_major || !doc_minor || !soft_major || !soft_minor || !soft_fix || !soft_build
|
||||||
|
|| !thumb_width || !thumb_height || !thumb_components) {
|
||||||
|
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI header is truncated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
info.document_version = { doc_major.value(), doc_minor.value() };
|
||||||
|
info.software_version = {
|
||||||
|
soft_major.value(),
|
||||||
|
soft_minor.value(),
|
||||||
|
soft_fix.value(),
|
||||||
|
soft_build.value(),
|
||||||
|
};
|
||||||
|
info.thumbnail = {
|
||||||
|
thumb_width.value(),
|
||||||
|
thumb_height.value(),
|
||||||
|
thumb_components.value(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (info.document_version.major != 0 || info.document_version.minor < 1) {
|
||||||
|
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI document version is unsupported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.thumbnail.width != 128 || info.thumbnail.height != 128 || info.thumbnail.components != 4) {
|
||||||
|
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<PpiHeaderInfo>::success(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<std::size_t> ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept
|
||||||
|
{
|
||||||
|
if (thumbnail.width == 0 || thumbnail.height == 0 || thumbnail.components == 0) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto width = static_cast<std::uint64_t>(thumbnail.width);
|
||||||
|
const auto height = static_cast<std::uint64_t>(thumbnail.height);
|
||||||
|
const auto components = static_cast<std::uint64_t>(thumbnail.components);
|
||||||
|
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pixels = width * height;
|
||||||
|
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto bytes = pixels * components;
|
||||||
|
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI thumbnail byte size exceeds addressable memory"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(std::span<const std::byte> bytes) noexcept
|
||||||
|
{
|
||||||
|
const auto header = parse_ppi_header(bytes);
|
||||||
|
if (!header) {
|
||||||
|
return pp::foundation::Result<PpiProjectLayout>::failure(header.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto thumbnail_bytes = ppi_thumbnail_byte_size(header.value().thumbnail);
|
||||||
|
if (!thumbnail_bytes) {
|
||||||
|
return pp::foundation::Result<PpiProjectLayout>::failure(thumbnail_bytes.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnail_bytes.value() > std::numeric_limits<std::size_t>::max() - ppi_header_size) {
|
||||||
|
return pp::foundation::Result<PpiProjectLayout>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto body_offset = ppi_header_size + thumbnail_bytes.value();
|
||||||
|
if (bytes.size() < body_offset) {
|
||||||
|
return pp::foundation::Result<PpiProjectLayout>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI thumbnail payload is truncated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<PpiProjectLayout>::success(PpiProjectLayout {
|
||||||
|
.header = header.value(),
|
||||||
|
.thumbnail_offset = ppi_header_size,
|
||||||
|
.thumbnail_bytes = thumbnail_bytes.value(),
|
||||||
|
.body_offset = body_offset,
|
||||||
|
.body_bytes = bytes.size() - body_offset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
|
||||||
|
PpiHeaderInfo header,
|
||||||
|
std::span<const std::byte> body,
|
||||||
|
PpiBodyIndex* index) noexcept
|
||||||
|
{
|
||||||
|
if (index != nullptr) {
|
||||||
|
index->summary = {};
|
||||||
|
index->layers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::ByteReader reader(body);
|
||||||
|
const auto width = read_positive_i32(reader, "PPI canvas width is outside the supported range");
|
||||||
|
const auto height = read_positive_i32(reader, "PPI canvas height is outside the supported range");
|
||||||
|
const auto layer_count = read_positive_i32(reader, "PPI layer count is outside the supported range");
|
||||||
|
if (!width || !height || !layer_count) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
!width ? width.status() : (!height ? height.status() : layer_count.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto canvas_status = validate_canvas_size(width.value(), height.value());
|
||||||
|
if (!canvas_status.ok()) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(canvas_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer_count.value() == 0 || layer_count.value() > max_ppi_layer_count) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
PpiBodySummary summary {
|
||||||
|
.width = width.value(),
|
||||||
|
.height = height.value(),
|
||||||
|
.layer_count = layer_count.value(),
|
||||||
|
.declared_frame_count = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<bool> seen_orders;
|
||||||
|
if (index != nullptr) {
|
||||||
|
index->layers.resize(summary.layer_count);
|
||||||
|
seen_orders.assign(summary.layer_count, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.document_version.minor >= 3U) {
|
||||||
|
const auto declared_frames = read_positive_i32(reader, "PPI declared frame count is outside the supported range");
|
||||||
|
if (!declared_frames) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(declared_frames.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (declared_frames.value() == 0 || declared_frames.value() > max_ppi_frame_count) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI declared frame count is outside the configured range"));
|
||||||
|
}
|
||||||
|
summary.declared_frame_count = declared_frames.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::uint32_t layer_index = 0; layer_index < summary.layer_count; ++layer_index) {
|
||||||
|
const auto order = read_positive_i32(reader, "PPI layer order is outside the supported range");
|
||||||
|
const auto opacity = read_f32(reader);
|
||||||
|
const auto name_length = read_positive_i32(reader, "PPI layer name length is outside the supported range");
|
||||||
|
if (!order || !opacity || !name_length) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
!order ? order.status() : (!opacity ? opacity.status() : name_length.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.value() >= summary.layer_count) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer order is outside the layer list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index != nullptr) {
|
||||||
|
if (seen_orders[order.value()]) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI layer order is duplicated"));
|
||||||
|
}
|
||||||
|
seen_orders[order.value()] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::isfinite(opacity.value())) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI layer opacity must be finite"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opacity.value() < 0.0F || opacity.value() > 1.0F) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name_length.value() > max_ppi_layer_name_length) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto name_bytes = reader.read_bytes(name_length.value());
|
||||||
|
if (!name_bytes) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(name_bytes.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
PpiLayerSummary layer_summary;
|
||||||
|
if (index != nullptr) {
|
||||||
|
layer_summary.stored_order = order.value();
|
||||||
|
layer_summary.opacity = opacity.value();
|
||||||
|
layer_summary.name.reserve(name_bytes.value().size());
|
||||||
|
for (const auto byte : name_bytes.value()) {
|
||||||
|
layer_summary.name.push_back(static_cast<char>(std::to_integer<unsigned char>(byte)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.document_version.minor >= 2U) {
|
||||||
|
const auto blend_mode = read_positive_i32(reader, "PPI layer blend mode is outside the supported range");
|
||||||
|
const auto alpha_locked = reader.read_u8();
|
||||||
|
const auto visible = reader.read_u8();
|
||||||
|
if (!blend_mode || !alpha_locked || !visible) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
!blend_mode ? blend_mode.status() : (!alpha_locked ? alpha_locked.status() : visible.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alpha_locked.value() > 1U || visible.value() > 1U) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI layer boolean field is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blend_mode.value() > 4U) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index != nullptr) {
|
||||||
|
layer_summary.blend_mode = blend_mode.value();
|
||||||
|
layer_summary.alpha_locked = alpha_locked.value() != 0U;
|
||||||
|
layer_summary.visible = visible.value() != 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t layer_frames = 1;
|
||||||
|
if (header.document_version.minor >= 3U) {
|
||||||
|
const auto frame_count = read_positive_i32(reader, "PPI layer frame count is outside the supported range");
|
||||||
|
if (!frame_count) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(frame_count.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame_count.value() == 0 || frame_count.value() > max_ppi_frame_count) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range"));
|
||||||
|
}
|
||||||
|
layer_frames = frame_count.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.total_layer_frames > max_ppi_frame_count - layer_frames) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI total frame count exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
summary.total_layer_frames += layer_frames;
|
||||||
|
|
||||||
|
if (index != nullptr) {
|
||||||
|
layer_summary.frames.resize(layer_frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::uint32_t frame_index = 0; frame_index < layer_frames; ++frame_index) {
|
||||||
|
if (header.document_version.minor >= 3U) {
|
||||||
|
const auto duration = read_positive_i32(reader, "PPI frame duration is outside the supported range");
|
||||||
|
if (!duration) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(duration.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration.value() == 0) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index != nullptr) {
|
||||||
|
layer_summary.frames[frame_index].duration_ms = duration.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::uint32_t face = 0; face < 6U; ++face) {
|
||||||
|
const auto has_data = read_positive_i32(reader, "PPI face data flag is outside the supported range");
|
||||||
|
if (!has_data) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(has_data.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_data.value() > 1U) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI face data flag is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_data.value() == 0U) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++summary.dirty_face_count;
|
||||||
|
const auto x0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||||
|
const auto y0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||||
|
const auto x1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||||
|
const auto y1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||||
|
const auto data_size = read_positive_i32(reader, "PPI compressed face data size is outside the supported range");
|
||||||
|
if (!x0 || !y0 || !x1 || !y1 || !data_size) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
!x0 ? x0.status()
|
||||||
|
: (!y0 ? y0.status() : (!x1 ? x1.status() : (!y1 ? y1.status() : data_size.status()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x0.value() >= x1.value() || y0.value() >= y1.value() || x1.value() > summary.width
|
||||||
|
|| y1.value() > summary.height) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI dirty box is outside the canvas"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data_size.value() == 0U) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI compressed face payload must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto byte_status = add_payload_bytes(summary, data_size.value());
|
||||||
|
if (!byte_status.ok()) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(byte_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto payload_offset = reader.position();
|
||||||
|
const auto payload = reader.read_bytes(data_size.value());
|
||||||
|
if (!payload) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(payload.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto png_metadata = validate_face_png_payload(
|
||||||
|
payload.value(),
|
||||||
|
x1.value() - x0.value(),
|
||||||
|
y1.value() - y0.value());
|
||||||
|
if (!png_metadata) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(png_metadata.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
++summary.rgba_face_payload_count;
|
||||||
|
if (index != nullptr) {
|
||||||
|
layer_summary.frames[frame_index].faces[face] = PpiFacePayloadSummary {
|
||||||
|
.has_data = true,
|
||||||
|
.x0 = x0.value(),
|
||||||
|
.y0 = y0.value(),
|
||||||
|
.x1 = x1.value(),
|
||||||
|
.y1 = y1.value(),
|
||||||
|
.body_payload_offset = static_cast<std::uint32_t>(payload_offset),
|
||||||
|
.payload_bytes = data_size.value(),
|
||||||
|
.png_width = png_metadata.value().width,
|
||||||
|
.png_height = png_metadata.value().height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index != nullptr) {
|
||||||
|
index->layers[order.value()] = std::move(layer_summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.document_version.minor >= 3U && summary.total_layer_frames != summary.declared_frame_count) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI declared frame count does not match layer frames"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.document_version.minor >= 4U) {
|
||||||
|
const auto info_bytes = read_positive_i32(reader, "PPI info block size is outside the supported range");
|
||||||
|
if (!info_bytes) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(info_bytes.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.info_bytes = info_bytes.value();
|
||||||
|
const auto info_status = skip_bytes(reader, summary.info_bytes);
|
||||||
|
if (!info_status.ok()) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(info_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reader.empty()) {
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI body has trailing bytes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index != nullptr) {
|
||||||
|
index->summary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<PpiBodySummary>::success(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||||
|
PpiHeaderInfo header,
|
||||||
|
std::span<const std::byte> body) noexcept
|
||||||
|
{
|
||||||
|
return parse_ppi_body_impl(header, body, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<PpiBodyIndex> parse_ppi_body_index(
|
||||||
|
PpiHeaderInfo header,
|
||||||
|
std::span<const std::byte> body)
|
||||||
|
{
|
||||||
|
PpiBodyIndex index;
|
||||||
|
const auto summary = parse_ppi_body_impl(header, body, &index);
|
||||||
|
if (!summary) {
|
||||||
|
return pp::foundation::Result<PpiBodyIndex>::failure(summary.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<PpiBodyIndex>::success(std::move(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(std::span<const std::byte> bytes) noexcept
|
||||||
|
{
|
||||||
|
const auto layout = parse_ppi_project_layout(bytes);
|
||||||
|
if (!layout) {
|
||||||
|
return pp::foundation::Result<PpiProjectSummary>::failure(layout.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto body = parse_ppi_body_summary(
|
||||||
|
layout.value().header,
|
||||||
|
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
|
||||||
|
if (!body) {
|
||||||
|
return pp::foundation::Result<PpiProjectSummary>::failure(body.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<PpiProjectSummary>::success(PpiProjectSummary {
|
||||||
|
.layout = layout.value(),
|
||||||
|
.body = body.value(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(std::span<const std::byte> bytes)
|
||||||
|
{
|
||||||
|
const auto layout = parse_ppi_project_layout(bytes);
|
||||||
|
if (!layout) {
|
||||||
|
return pp::foundation::Result<PpiProjectIndex>::failure(layout.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto body = parse_ppi_body_index(
|
||||||
|
layout.value().header,
|
||||||
|
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
|
||||||
|
if (!body) {
|
||||||
|
return pp::foundation::Result<PpiProjectIndex>::failure(body.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<PpiProjectIndex>::success(PpiProjectIndex {
|
||||||
|
.layout = layout.value(),
|
||||||
|
.body = body.value(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(std::span<const std::byte> bytes)
|
||||||
|
{
|
||||||
|
auto project = parse_ppi_project_index(bytes);
|
||||||
|
if (!project) {
|
||||||
|
return pp::foundation::Result<PpiDecodedProjectImages>::failure(project.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
PpiDecodedProjectImages decoded {
|
||||||
|
.project = project.value(),
|
||||||
|
.faces = {},
|
||||||
|
};
|
||||||
|
decoded.faces.reserve(decoded.project.body.summary.rgba_face_payload_count);
|
||||||
|
|
||||||
|
const auto body = bytes.subspan(decoded.project.layout.body_offset, decoded.project.layout.body_bytes);
|
||||||
|
for (std::size_t layer_index = 0; layer_index < decoded.project.body.layers.size(); ++layer_index) {
|
||||||
|
const auto& layer = decoded.project.body.layers[layer_index];
|
||||||
|
for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) {
|
||||||
|
const auto& frame = layer.frames[frame_index];
|
||||||
|
for (std::size_t face_index = 0; face_index < frame.faces.size(); ++face_index) {
|
||||||
|
const auto& face = frame.faces[face_index];
|
||||||
|
if (!face.has_data) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (face.body_payload_offset > body.size()
|
||||||
|
|| face.payload_bytes > body.size() - face.body_payload_offset) {
|
||||||
|
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI face payload range is outside the body"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto image = decode_png_rgba8(
|
||||||
|
body.subspan(face.body_payload_offset, face.payload_bytes));
|
||||||
|
if (!image) {
|
||||||
|
return pp::foundation::Result<PpiDecodedProjectImages>::failure(image.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image.value().width != face.png_width || image.value().height != face.png_height) {
|
||||||
|
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("decoded PPI face payload dimensions changed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded.faces.push_back(PpiDecodedFacePayload {
|
||||||
|
.layer_index = static_cast<std::uint32_t>(layer_index),
|
||||||
|
.frame_index = static_cast<std::uint32_t>(frame_index),
|
||||||
|
.face_index = static_cast<std::uint32_t>(face_index),
|
||||||
|
.descriptor = face,
|
||||||
|
.image = image.value(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<PpiDecodedProjectImages>::success(std::move(decoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<std::vector<std::byte>> create_ppi_project(PpiProjectConfig config)
|
||||||
|
{
|
||||||
|
const auto canvas_status = validate_canvas_size(config.width, config.height);
|
||||||
|
if (!canvas_status.ok()) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(canvas_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.layers.empty() || config.layers.size() > max_ppi_layer_count) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t total_frame_count = 0;
|
||||||
|
std::vector<std::size_t> layer_frame_offsets;
|
||||||
|
layer_frame_offsets.reserve(config.layers.size());
|
||||||
|
for (const auto& layer : config.layers) {
|
||||||
|
if (layer.name.empty()) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI layer name must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.name.size() > max_ppi_layer_name_length) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::isfinite(layer.metadata.opacity)) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI layer opacity must be finite"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.metadata.opacity < 0.0F || layer.metadata.opacity > 1.0F) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.metadata.blend_mode > 4U) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.frames.empty() || layer.frames.size() > max_ppi_frame_count) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.frames.size() > max_ppi_frame_count - total_frame_count) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI total layer frame count exceeds the configured range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
layer_frame_offsets.push_back(total_frame_count);
|
||||||
|
total_frame_count += static_cast<std::uint32_t>(layer.frames.size());
|
||||||
|
|
||||||
|
for (const auto& frame : layer.frames) {
|
||||||
|
if (frame.duration_ms == 0) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::array<bool, 6>> seen_faces(total_frame_count);
|
||||||
|
std::uint64_t total_payload_bytes = 0;
|
||||||
|
for (const auto& face : config.dirty_faces) {
|
||||||
|
if (face.layer_index >= config.layers.size()) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI dirty face layer index is outside the layer list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (face.frame_index >= config.layers[face.layer_index].frames.size()) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI dirty face frame index is outside the frame list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (face.face_index >= 6U) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI dirty face index is outside the cube face list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto slot_index = layer_frame_offsets[face.layer_index] + static_cast<std::size_t>(face.frame_index);
|
||||||
|
if (seen_faces[slot_index][face.face_index]) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI dirty face slot is duplicated"));
|
||||||
|
}
|
||||||
|
seen_faces[slot_index][face.face_index] = true;
|
||||||
|
|
||||||
|
if (face.width == 0 || face.height == 0) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI dirty face dimensions must be greater than zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (face.x > config.width || face.width > config.width - face.x
|
||||||
|
|| face.y > config.height || face.height > config.height - face.y) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI dirty face box is outside the canvas"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (face.png_rgba8.empty()) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI dirty face PNG payload must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (face.png_rgba8.size() > static_cast<std::size_t>(std::numeric_limits<std::uint32_t>::max())) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI dirty face PNG payload is too large"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto next_payload_bytes = total_payload_bytes + face.png_rgba8.size();
|
||||||
|
if (next_payload_bytes > max_ppi_face_payload_bytes) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI dirty face PNG payloads exceed the configured limit"));
|
||||||
|
}
|
||||||
|
total_payload_bytes = next_payload_bytes;
|
||||||
|
|
||||||
|
const auto metadata = validate_face_png_payload(face.png_rgba8, face.width, face.height);
|
||||||
|
if (!metadata) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(metadata.status());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::byte> bytes {
|
||||||
|
std::byte { 'P' },
|
||||||
|
std::byte { 'P' },
|
||||||
|
std::byte { 'I' },
|
||||||
|
std::byte { 0 },
|
||||||
|
};
|
||||||
|
append_u32(bytes, 0);
|
||||||
|
append_u32(bytes, 4);
|
||||||
|
append_u32(bytes, 0);
|
||||||
|
append_u32(bytes, 0);
|
||||||
|
append_u32(bytes, 0);
|
||||||
|
append_u32(bytes, 0);
|
||||||
|
append_u32(bytes, 128);
|
||||||
|
append_u32(bytes, 128);
|
||||||
|
append_u32(bytes, 4);
|
||||||
|
|
||||||
|
constexpr std::size_t thumbnail_bytes = 128U * 128U * 4U;
|
||||||
|
bytes.resize(ppi_header_size + thumbnail_bytes, std::byte { 0 });
|
||||||
|
|
||||||
|
append_u32(bytes, config.width);
|
||||||
|
append_u32(bytes, config.height);
|
||||||
|
append_u32(bytes, static_cast<std::uint32_t>(config.layers.size()));
|
||||||
|
append_u32(bytes, total_frame_count);
|
||||||
|
for (std::uint32_t layer = 0; layer < config.layers.size(); ++layer) {
|
||||||
|
const auto& layer_config = config.layers[layer];
|
||||||
|
append_u32(bytes, layer);
|
||||||
|
append_f32(bytes, layer_config.metadata.opacity);
|
||||||
|
append_u32(bytes, static_cast<std::uint32_t>(layer_config.name.size()));
|
||||||
|
append_ascii(bytes, layer_config.name);
|
||||||
|
append_u32(bytes, layer_config.metadata.blend_mode);
|
||||||
|
bytes.push_back(layer_config.metadata.alpha_locked ? std::byte { 1 } : std::byte { 0 });
|
||||||
|
bytes.push_back(layer_config.metadata.visible ? std::byte { 1 } : std::byte { 0 });
|
||||||
|
append_u32(bytes, static_cast<std::uint32_t>(layer_config.frames.size()));
|
||||||
|
for (std::uint32_t frame = 0; frame < layer_config.frames.size(); ++frame) {
|
||||||
|
append_u32(bytes, layer_config.frames[frame].duration_ms);
|
||||||
|
for (std::uint32_t face = 0; face < 6U; ++face) {
|
||||||
|
const PpiDirtyFacePayloadConfig* dirty_face = nullptr;
|
||||||
|
for (const auto& candidate : config.dirty_faces) {
|
||||||
|
if (candidate.layer_index == layer && candidate.frame_index == frame
|
||||||
|
&& candidate.face_index == face) {
|
||||||
|
dirty_face = &candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirty_face == nullptr) {
|
||||||
|
append_u32(bytes, 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
append_u32(bytes, 1);
|
||||||
|
append_u32(bytes, dirty_face->x);
|
||||||
|
append_u32(bytes, dirty_face->y);
|
||||||
|
append_u32(bytes, dirty_face->x + dirty_face->width);
|
||||||
|
append_u32(bytes, dirty_face->y + dirty_face->height);
|
||||||
|
append_u32(bytes, static_cast<std::uint32_t>(dirty_face->png_rgba8.size()));
|
||||||
|
bytes.insert(bytes.end(), dirty_face->png_rgba8.begin(), dirty_face->png_rgba8.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append_u32(bytes, 0);
|
||||||
|
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::success(std::move(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(PpiMinimalProjectConfig config)
|
||||||
|
{
|
||||||
|
if (config.layer_count == 0 || config.layer_count > max_ppi_layer_count) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.frame_count == 0 || config.frame_count > max_ppi_frame_count) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("PPI frame count is outside the configured range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> names;
|
||||||
|
names.reserve(config.layer_count);
|
||||||
|
std::vector<std::vector<PpiFrameConfig>> frame_lists;
|
||||||
|
frame_lists.reserve(config.layer_count);
|
||||||
|
std::vector<PpiLayerConfig> layers;
|
||||||
|
layers.reserve(config.layer_count);
|
||||||
|
for (std::uint32_t layer = 0; layer < config.layer_count; ++layer) {
|
||||||
|
names.push_back(generated_layer_name(config.layer_name, layer, config.layer_count));
|
||||||
|
auto& frames = frame_lists.emplace_back();
|
||||||
|
frames.assign(config.frame_count, PpiFrameConfig { .duration_ms = config.frame_duration_ms });
|
||||||
|
layers.push_back(PpiLayerConfig {
|
||||||
|
.name = names.back(),
|
||||||
|
.metadata = config.layer_metadata,
|
||||||
|
.frames = std::span<const PpiFrameConfig>(frames.data(), frames.size()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return create_ppi_project(PpiProjectConfig {
|
||||||
|
.width = config.width,
|
||||||
|
.height = config.height,
|
||||||
|
.layers = std::span<const PpiLayerConfig>(layers.data(), layers.size()),
|
||||||
|
.dirty_faces = config.dirty_faces,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
199
src/assets/ppi_header.h
Normal file
199
src/assets/ppi_header.h
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "assets/image_pixels.h"
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
constexpr std::size_t ppi_header_size = 40;
|
||||||
|
constexpr std::uint32_t max_ppi_canvas_dimension = 131072;
|
||||||
|
constexpr std::uint32_t max_ppi_layer_count = 1024;
|
||||||
|
constexpr std::uint32_t max_ppi_frame_count = 100000;
|
||||||
|
constexpr std::size_t max_ppi_layer_name_length = 128;
|
||||||
|
constexpr std::uint64_t max_ppi_face_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
|
||||||
|
|
||||||
|
struct PpiVersion {
|
||||||
|
std::uint32_t major = 0;
|
||||||
|
std::uint32_t minor = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiSoftwareVersion {
|
||||||
|
std::uint32_t major = 0;
|
||||||
|
std::uint32_t minor = 0;
|
||||||
|
std::uint32_t fix = 0;
|
||||||
|
std::uint32_t build = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiThumbnailInfo {
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::uint32_t components = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiHeaderInfo {
|
||||||
|
PpiVersion document_version;
|
||||||
|
PpiSoftwareVersion software_version;
|
||||||
|
PpiThumbnailInfo thumbnail;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiProjectLayout {
|
||||||
|
PpiHeaderInfo header;
|
||||||
|
std::size_t thumbnail_offset = 0;
|
||||||
|
std::size_t thumbnail_bytes = 0;
|
||||||
|
std::size_t body_offset = 0;
|
||||||
|
std::size_t body_bytes = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiBodySummary {
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::uint32_t layer_count = 0;
|
||||||
|
std::uint32_t declared_frame_count = 0;
|
||||||
|
std::uint32_t total_layer_frames = 0;
|
||||||
|
std::uint32_t dirty_face_count = 0;
|
||||||
|
std::uint32_t rgba_face_payload_count = 0;
|
||||||
|
std::uint64_t compressed_face_bytes = 0;
|
||||||
|
std::uint32_t info_bytes = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiProjectSummary {
|
||||||
|
PpiProjectLayout layout;
|
||||||
|
PpiBodySummary body;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiFacePayloadSummary {
|
||||||
|
bool has_data = false;
|
||||||
|
std::uint32_t x0 = 0;
|
||||||
|
std::uint32_t y0 = 0;
|
||||||
|
std::uint32_t x1 = 0;
|
||||||
|
std::uint32_t y1 = 0;
|
||||||
|
std::uint32_t body_payload_offset = 0;
|
||||||
|
std::uint32_t payload_bytes = 0;
|
||||||
|
std::uint32_t png_width = 0;
|
||||||
|
std::uint32_t png_height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiFrameSummary {
|
||||||
|
std::uint32_t duration_ms = 100;
|
||||||
|
std::array<PpiFacePayloadSummary, 6> faces;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiLayerSummary {
|
||||||
|
std::uint32_t stored_order = 0;
|
||||||
|
std::string name;
|
||||||
|
float opacity = 1.0F;
|
||||||
|
std::uint32_t blend_mode = 0;
|
||||||
|
bool alpha_locked = false;
|
||||||
|
bool visible = true;
|
||||||
|
std::vector<PpiFrameSummary> frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiBodyIndex {
|
||||||
|
PpiBodySummary summary;
|
||||||
|
std::vector<PpiLayerSummary> layers;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiProjectIndex {
|
||||||
|
PpiProjectLayout layout;
|
||||||
|
PpiBodyIndex body;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiDecodedFacePayload {
|
||||||
|
std::uint32_t layer_index = 0;
|
||||||
|
std::uint32_t frame_index = 0;
|
||||||
|
std::uint32_t face_index = 0;
|
||||||
|
PpiFacePayloadSummary descriptor;
|
||||||
|
Rgba8Image image;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiDecodedProjectImages {
|
||||||
|
PpiProjectIndex project;
|
||||||
|
std::vector<PpiDecodedFacePayload> faces;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiDirtyFacePayloadConfig {
|
||||||
|
std::uint32_t layer_index = 0;
|
||||||
|
std::uint32_t frame_index = 0;
|
||||||
|
std::uint32_t face_index = 0;
|
||||||
|
std::uint32_t x = 0;
|
||||||
|
std::uint32_t y = 0;
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::span<const std::byte> png_rgba8;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiLayerMetadataConfig {
|
||||||
|
float opacity = 1.0F;
|
||||||
|
std::uint32_t blend_mode = 0;
|
||||||
|
bool alpha_locked = false;
|
||||||
|
bool visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiFrameConfig {
|
||||||
|
std::uint32_t duration_ms = 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiLayerConfig {
|
||||||
|
std::string_view name;
|
||||||
|
PpiLayerMetadataConfig metadata;
|
||||||
|
std::span<const PpiFrameConfig> frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiProjectConfig {
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::span<const PpiLayerConfig> layers;
|
||||||
|
std::span<const PpiDirtyFacePayloadConfig> dirty_faces;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PpiMinimalProjectConfig {
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::string layer_name;
|
||||||
|
PpiLayerMetadataConfig layer_metadata;
|
||||||
|
std::uint32_t layer_count = 1;
|
||||||
|
std::uint32_t frame_count = 1;
|
||||||
|
std::uint32_t frame_duration_ms = 100;
|
||||||
|
std::span<const PpiDirtyFacePayloadConfig> dirty_faces;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
|
||||||
|
std::span<const std::byte> bytes) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::size_t> ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(
|
||||||
|
std::span<const std::byte> bytes) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||||
|
PpiHeaderInfo header,
|
||||||
|
std::span<const std::byte> body) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<PpiBodyIndex> parse_ppi_body_index(
|
||||||
|
PpiHeaderInfo header,
|
||||||
|
std::span<const std::byte> body);
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(
|
||||||
|
std::span<const std::byte> bytes) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(
|
||||||
|
std::span<const std::byte> bytes);
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(
|
||||||
|
std::span<const std::byte> bytes);
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> create_ppi_project(
|
||||||
|
PpiProjectConfig config);
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(
|
||||||
|
PpiMinimalProjectConfig config);
|
||||||
|
|
||||||
|
}
|
||||||
183
src/assets/settings_document.cpp
Normal file
183
src/assets/settings_document.cpp
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
#include "assets/settings_document.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] bool is_valid_key_char(char value) noexcept
|
||||||
|
{
|
||||||
|
const auto ch = static_cast<unsigned char>(value);
|
||||||
|
return std::isalnum(ch) != 0 || value == '_' || value == '-' || value == '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t SettingsDocument::size() const noexcept
|
||||||
|
{
|
||||||
|
return entries_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsDocument::empty() const noexcept
|
||||||
|
{
|
||||||
|
return entries_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsDocument::has(std::string_view key) const noexcept
|
||||||
|
{
|
||||||
|
return find_entry(key) != entries_.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<SettingsEntry>& SettingsDocument::entries() const noexcept
|
||||||
|
{
|
||||||
|
return entries_;
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status SettingsDocument::set(std::string_view key, SettingsValue value)
|
||||||
|
{
|
||||||
|
const auto key_status = validate_settings_key(key);
|
||||||
|
if (!key_status.ok()) {
|
||||||
|
return key_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto value_status = validate_settings_value(value);
|
||||||
|
if (!value_status.ok()) {
|
||||||
|
return value_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto found = find_entry(key);
|
||||||
|
if (found != entries_.end()) {
|
||||||
|
found->value = value;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries_.size() >= max_settings_entries) {
|
||||||
|
return pp::foundation::Status::out_of_range("settings entry count exceeds the configured limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
entries_.push_back(SettingsEntry {
|
||||||
|
.key = std::string(key),
|
||||||
|
.value = value,
|
||||||
|
});
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<SettingsValue> SettingsDocument::get(std::string_view key) const
|
||||||
|
{
|
||||||
|
const auto key_status = validate_settings_key(key);
|
||||||
|
if (!key_status.ok()) {
|
||||||
|
return pp::foundation::Result<SettingsValue>::failure(key_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto found = find_entry(key);
|
||||||
|
if (found == entries_.end()) {
|
||||||
|
return pp::foundation::Result<SettingsValue>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("settings key was not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<SettingsValue>::success(found->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status SettingsDocument::unset(std::string_view key) noexcept
|
||||||
|
{
|
||||||
|
const auto key_status = validate_settings_key(key);
|
||||||
|
if (!key_status.ok()) {
|
||||||
|
return key_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto found = find_entry(key);
|
||||||
|
if (found == entries_.end()) {
|
||||||
|
return pp::foundation::Status::out_of_range("settings key was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
entries_.erase(found);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsDocument::clear() noexcept
|
||||||
|
{
|
||||||
|
entries_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SettingsEntry>::iterator SettingsDocument::find_entry(std::string_view key) noexcept
|
||||||
|
{
|
||||||
|
return std::find_if(
|
||||||
|
entries_.begin(),
|
||||||
|
entries_.end(),
|
||||||
|
[key](const SettingsEntry& entry) {
|
||||||
|
return entry.key == key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SettingsEntry>::const_iterator SettingsDocument::find_entry(std::string_view key) const noexcept
|
||||||
|
{
|
||||||
|
return std::find_if(
|
||||||
|
entries_.begin(),
|
||||||
|
entries_.end(),
|
||||||
|
[key](const SettingsEntry& entry) {
|
||||||
|
return entry.key == key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status validate_settings_key(std::string_view key) noexcept
|
||||||
|
{
|
||||||
|
if (key.empty()) {
|
||||||
|
return pp::foundation::Status::invalid_argument("settings key must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.size() > max_settings_key_length) {
|
||||||
|
return pp::foundation::Status::out_of_range("settings key length exceeds the configured limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.front() == '.' || key.back() == '.') {
|
||||||
|
return pp::foundation::Status::invalid_argument("settings key must not start or end with a dot");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto ch : key) {
|
||||||
|
if (!is_valid_key_char(ch)) {
|
||||||
|
return pp::foundation::Status::invalid_argument("settings key contains an unsupported character");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status validate_settings_value(const SettingsValue& value) noexcept
|
||||||
|
{
|
||||||
|
if (const auto* string_value = std::get_if<std::string>(&value)) {
|
||||||
|
if (string_value->size() > max_settings_string_length) {
|
||||||
|
return pp::foundation::Status::out_of_range("settings string length exceeds the configured limit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* double_value = std::get_if<double>(&value)) {
|
||||||
|
if (!std::isfinite(*double_value)) {
|
||||||
|
return pp::foundation::Status::invalid_argument("settings floating point value must be finite");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* settings_value_type_name(const SettingsValue& value) noexcept
|
||||||
|
{
|
||||||
|
if (std::holds_alternative<bool>(value)) {
|
||||||
|
return "bool";
|
||||||
|
}
|
||||||
|
if (std::holds_alternative<std::int64_t>(value)) {
|
||||||
|
return "int64";
|
||||||
|
}
|
||||||
|
if (std::holds_alternative<double>(value)) {
|
||||||
|
return "double";
|
||||||
|
}
|
||||||
|
if (std::holds_alternative<std::string>(value)) {
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
src/assets/settings_document.h
Normal file
48
src/assets/settings_document.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pp::assets {
|
||||||
|
|
||||||
|
constexpr std::size_t max_settings_entries = 4096;
|
||||||
|
constexpr std::size_t max_settings_key_length = 128;
|
||||||
|
constexpr std::size_t max_settings_string_length = 4096;
|
||||||
|
|
||||||
|
using SettingsValue = std::variant<bool, std::int64_t, double, std::string>;
|
||||||
|
|
||||||
|
struct SettingsEntry {
|
||||||
|
std::string key;
|
||||||
|
SettingsValue value;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SettingsDocument {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] std::size_t size() const noexcept;
|
||||||
|
[[nodiscard]] bool empty() const noexcept;
|
||||||
|
[[nodiscard]] bool has(std::string_view key) const noexcept;
|
||||||
|
[[nodiscard]] const std::vector<SettingsEntry>& entries() const noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status set(std::string_view key, SettingsValue value);
|
||||||
|
[[nodiscard]] pp::foundation::Result<SettingsValue> get(std::string_view key) const;
|
||||||
|
[[nodiscard]] pp::foundation::Status unset(std::string_view key) noexcept;
|
||||||
|
void clear() noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] std::vector<SettingsEntry>::iterator find_entry(std::string_view key) noexcept;
|
||||||
|
[[nodiscard]] std::vector<SettingsEntry>::const_iterator find_entry(std::string_view key) const noexcept;
|
||||||
|
|
||||||
|
std::vector<SettingsEntry> entries_;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_settings_key(std::string_view key) noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_settings_value(const SettingsValue& value) noexcept;
|
||||||
|
[[nodiscard]] const char* settings_value_type_name(const SettingsValue& value) noexcept;
|
||||||
|
|
||||||
|
}
|
||||||
582
src/canvas.cpp
582
src/canvas.cpp
File diff suppressed because it is too large
Load Diff
@@ -205,7 +205,7 @@ public:
|
|||||||
void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz);
|
void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz);
|
||||||
std::array<std::vector<vertex_t>, 6> stroke_draw_project(std::array<vertex_t, 4>& B, bool project_3d = false, glm::mat4 mv = glm::mat4(1)) const;
|
std::array<std::vector<vertex_t>, 6> stroke_draw_project(std::array<vertex_t, 4>& B, bool project_3d = false, glm::mat4 mv = glm::mat4(1)) const;
|
||||||
// return rect {origin, size}
|
// return rect {origin, size}
|
||||||
glm::vec4 stroke_draw_samples(int i, std::vector<vertex_t>& P);
|
glm::vec4 stroke_draw_samples(int i, std::vector<vertex_t>& P, bool copy_stroke_destination);
|
||||||
std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke) const;
|
std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke) const;
|
||||||
void stroke_draw();
|
void stroke_draw();
|
||||||
void stroke_end();
|
void stroke_end();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "canvas.h"
|
#include "canvas.h"
|
||||||
#include "canvas_actions.h"
|
#include "canvas_actions.h"
|
||||||
#include "node_panel_layer.h"
|
#include "node_panel_layer.h"
|
||||||
|
#include "renderer_gl/opengl_capabilities.h"
|
||||||
|
|
||||||
void ActionStroke::undo()
|
void ActionStroke::undo()
|
||||||
{
|
{
|
||||||
@@ -36,8 +37,21 @@ void ActionStroke::undo()
|
|||||||
{
|
{
|
||||||
App::I->render_task([&]
|
App::I->render_task([&]
|
||||||
{
|
{
|
||||||
|
const auto texture_target = pp::renderer::gl::texture_2d_target();
|
||||||
|
const auto pixel_format = pp::renderer::gl::rgba_pixel_format();
|
||||||
|
const auto component_type = pp::renderer::gl::unsigned_byte_component_type();
|
||||||
|
|
||||||
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).bindTexture();
|
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).bindTexture();
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, (int)m_box[i].x, (int)m_box[i].y, (int)box_sz.x, (int)box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, m_image[i].get());
|
glTexSubImage2D(
|
||||||
|
texture_target,
|
||||||
|
0,
|
||||||
|
(int)m_box[i].x,
|
||||||
|
(int)m_box[i].y,
|
||||||
|
(int)box_sz.x,
|
||||||
|
(int)box_sz.y,
|
||||||
|
pixel_format,
|
||||||
|
component_type,
|
||||||
|
m_image[i].get());
|
||||||
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).unbindTexture();
|
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).unbindTexture();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -76,11 +90,22 @@ Action* ActionStroke::get_redo()
|
|||||||
glm::vec2 box_sz = zw(box) - xy(box);
|
glm::vec2 box_sz = zw(box) - xy(box);
|
||||||
if (box_sz.x > 0 && box_sz.y > 0 && box_sz.x <= layer->w && box_sz.y <= layer->h)
|
if (box_sz.x > 0 && box_sz.y > 0 && box_sz.x <= layer->w && box_sz.y <= layer->h)
|
||||||
{
|
{
|
||||||
action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
|
action->m_image[i] = std::make_unique<uint8_t[]>(
|
||||||
|
static_cast<size_t>((int)box_sz.x) * static_cast<size_t>((int)box_sz.y) * 4U);
|
||||||
App::I->render_task([&]
|
App::I->render_task([&]
|
||||||
{
|
{
|
||||||
|
const auto pixel_format = pp::renderer::gl::rgba_pixel_format();
|
||||||
|
const auto component_type = pp::renderer::gl::unsigned_byte_component_type();
|
||||||
|
|
||||||
layer->rtt(i, m_frame_idx).bindFramebuffer();
|
layer->rtt(i, m_frame_idx).bindFramebuffer();
|
||||||
glReadPixels(box_or.x, box_or.y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
|
glReadPixels(
|
||||||
|
(int)box_or.x,
|
||||||
|
(int)box_or.y,
|
||||||
|
(int)box_sz.x,
|
||||||
|
(int)box_sz.y,
|
||||||
|
pixel_format,
|
||||||
|
component_type,
|
||||||
|
action->m_image[i].get());
|
||||||
layer->rtt(i, m_frame_idx).unbindFramebuffer();
|
layer->rtt(i, m_frame_idx).unbindFramebuffer();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
#include "canvas_layer.h"
|
#include "canvas_layer.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "renderer_gl/opengl_capabilities.h"
|
||||||
#include "rtt.h"
|
#include "rtt.h"
|
||||||
|
|
||||||
uint32_t Layer::s_count = 0;
|
uint32_t Layer::s_count = 0;
|
||||||
@@ -44,7 +45,7 @@ TextureCube Layer::gen_cube()
|
|||||||
{
|
{
|
||||||
ret.bind();
|
ret.bind();
|
||||||
rtt(i).bindFramebuffer();
|
rtt(i).bindFramebuffer();
|
||||||
glCopyTexSubImage2D(TextureCube::m_faces_map[i], 0, 0, 0, 0, 0, w, w);
|
glCopyTexSubImage2D(pp::renderer::gl::cube_face_texture_target(i), 0, 0, 0, 0, 0, w, w);
|
||||||
rtt(i).unbindFramebuffer();
|
rtt(i).unbindFramebuffer();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
|
|||||||
latlong.create(size.x * 4, size.y * 2);
|
latlong.create(size.x * 4, size.y * 2);
|
||||||
ret.create(size.x * 4, size.y * 2);
|
ret.create(size.x * 4, size.y * 2);
|
||||||
|
|
||||||
glDisable(GL_BLEND);
|
glDisable(pp::renderer::gl::blend_state());
|
||||||
|
|
||||||
latlong.bindFramebuffer();
|
latlong.bindFramebuffer();
|
||||||
|
|
||||||
@@ -78,8 +79,8 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
|
|||||||
|
|
||||||
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||||
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
|
glBindTexture(pp::renderer::gl::texture_cube_map_target(), cube.m_cubetex_id);
|
||||||
|
|
||||||
ShaderManager::use(kShader::Equirect);
|
ShaderManager::use(kShader::Equirect);
|
||||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
|
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
|
||||||
@@ -88,7 +89,7 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
|
|||||||
Canvas::I->m_plane.draw_fill();
|
Canvas::I->m_plane.draw_fill();
|
||||||
|
|
||||||
ret.bind();
|
ret.bind();
|
||||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, latlong.getWidth(), latlong.getHeight());
|
glCopyTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0, 0, 0, 0, 0, latlong.getWidth(), latlong.getHeight());
|
||||||
|
|
||||||
latlong.unbindFramebuffer();
|
latlong.unbindFramebuffer();
|
||||||
|
|
||||||
@@ -115,13 +116,13 @@ PBO Layer::gen_equirect_pbo(glm::ivec2 size /*= { 0, 0 }*/)
|
|||||||
|
|
||||||
App::I->render_task([&]
|
App::I->render_task([&]
|
||||||
{
|
{
|
||||||
glDisable(GL_BLEND);
|
glDisable(pp::renderer::gl::blend_state());
|
||||||
|
|
||||||
latlong.bindFramebuffer();
|
latlong.bindFramebuffer();
|
||||||
|
|
||||||
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||||
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
|
glBindTexture(pp::renderer::gl::texture_cube_map_target(), cube.m_cubetex_id);
|
||||||
|
|
||||||
ShaderManager::use(kShader::Equirect);
|
ShaderManager::use(kShader::Equirect);
|
||||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
|
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
|
||||||
@@ -458,7 +459,7 @@ void LayerFrame::clear(const glm::vec4& c)
|
|||||||
{
|
{
|
||||||
// push clear color state
|
// push clear color state
|
||||||
GLfloat cc[4];
|
GLfloat cc[4];
|
||||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
glGetFloatv(pp::renderer::gl::color_clear_value_query(), cc);
|
||||||
glClearColor(c.r, c.g, c.b, c.a);
|
glClearColor(c.r, c.g, c.b, c.a);
|
||||||
|
|
||||||
bool erase = (c.a == 0.f);
|
bool erase = (c.a == 0.f);
|
||||||
@@ -466,7 +467,7 @@ void LayerFrame::clear(const glm::vec4& c)
|
|||||||
for (int i = 0; i < 6; i++)
|
for (int i = 0; i < 6; i++)
|
||||||
{
|
{
|
||||||
m_rtt[i].bindFramebuffer();
|
m_rtt[i].bindFramebuffer();
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
|
||||||
m_rtt[i].unbindFramebuffer();
|
m_rtt[i].unbindFramebuffer();
|
||||||
|
|
||||||
if (erase)
|
if (erase)
|
||||||
@@ -530,9 +531,11 @@ void LayerFrame::restore(const Snapshot& snap)
|
|||||||
|
|
||||||
m_rtt[i].bindTexture();
|
m_rtt[i].bindTexture();
|
||||||
glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
|
glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0,
|
glTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0,
|
||||||
m_dirty_box[i].x, m_dirty_box[i].y,
|
static_cast<int>(m_dirty_box[i].x), static_cast<int>(m_dirty_box[i].y),
|
||||||
box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE,
|
static_cast<int>(box_sz.x), static_cast<int>(box_sz.y),
|
||||||
|
pp::renderer::gl::rgba_pixel_format(),
|
||||||
|
pp::renderer::gl::unsigned_byte_component_type(),
|
||||||
snap.image[i].get());
|
snap.image[i].get());
|
||||||
m_rtt[i].unbindTexture();
|
m_rtt[i].unbindTexture();
|
||||||
LOG("restore face %d - %d bytes (%dx%d)", i,
|
LOG("restore face %d - %d bytes (%dx%d)", i,
|
||||||
@@ -560,8 +563,11 @@ LayerFrame::Snapshot LayerFrame::snapshot(std::array<glm::vec4, 6>* dirty_box /*
|
|||||||
|
|
||||||
m_rtt[i].bindFramebuffer();
|
m_rtt[i].bindFramebuffer();
|
||||||
glm::vec2 box_sz = zw(snap.m_dirty_box[i]) - xy(snap.m_dirty_box[i]);
|
glm::vec2 box_sz = zw(snap.m_dirty_box[i]) - xy(snap.m_dirty_box[i]);
|
||||||
glReadPixels(snap.m_dirty_box[i].x, snap.m_dirty_box[i].y,
|
glReadPixels(static_cast<int>(snap.m_dirty_box[i].x), static_cast<int>(snap.m_dirty_box[i].y),
|
||||||
box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, snap.image[i].get());
|
static_cast<int>(box_sz.x), static_cast<int>(box_sz.y),
|
||||||
|
pp::renderer::gl::rgba_pixel_format(),
|
||||||
|
pp::renderer::gl::unsigned_byte_component_type(),
|
||||||
|
snap.image[i].get());
|
||||||
m_rtt[i].unbindFramebuffer();
|
m_rtt[i].unbindFramebuffer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "canvas_modes.h"
|
#include "canvas_modes.h"
|
||||||
#include "layout.h"
|
#include "layout.h"
|
||||||
@@ -7,9 +10,19 @@
|
|||||||
#include "node_canvas.h"
|
#include "node_canvas.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "renderer_gl/opengl_capabilities.h"
|
||||||
|
|
||||||
NodeCanvas* CanvasMode::node;
|
NodeCanvas* CanvasMode::node;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void set_active_texture_unit(std::uint32_t unit_index)
|
||||||
|
{
|
||||||
|
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void CanvasModeBasicCamera::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
|
void CanvasModeBasicCamera::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
|
||||||
{
|
{
|
||||||
switch (me->m_type)
|
switch (me->m_type)
|
||||||
@@ -292,7 +305,10 @@ void CanvasModePen::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const
|
|||||||
}
|
}
|
||||||
glReadPixels((pos.x / App::I->width) * fb_width,
|
glReadPixels((pos.x / App::I->width) * fb_width,
|
||||||
((App::I->height - pos.y - 1) / App::I->height) * fb_height,
|
((App::I->height - pos.y - 1) / App::I->height) * fb_height,
|
||||||
1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
1, 1,
|
||||||
|
pp::renderer::gl::rgba_pixel_format(),
|
||||||
|
pp::renderer::gl::unsigned_byte_component_type(),
|
||||||
|
&pixel);
|
||||||
bool outline = glm::min(tip_scale.x, tip_scale.y) < 20 || m_resizing ? false : m_draw_outline;
|
bool outline = glm::min(tip_scale.x, tip_scale.y) < 20 || m_resizing ? false : m_draw_outline;
|
||||||
ShaderManager::u_int(kShaderUniform::DrawOutline, outline);
|
ShaderManager::u_int(kShaderUniform::DrawOutline, outline);
|
||||||
ShaderManager::u_vec4(kShaderUniform::Col, outline ? glm::vec4(1.f - glm::vec3(pixel) / 255.f, 1.f) : tip_color);
|
ShaderManager::u_vec4(kShaderUniform::Col, outline ? glm::vec4(1.f - glm::vec3(pixel) / 255.f, 1.f) : tip_color);
|
||||||
@@ -303,15 +319,15 @@ void CanvasModePen::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const
|
|||||||
glm::eulerAngleZ(tip_angle) *
|
glm::eulerAngleZ(tip_angle) *
|
||||||
glm::scale(glm::vec3(tip_scale, 1))
|
glm::scale(glm::vec3(tip_scale, 1))
|
||||||
);
|
);
|
||||||
bool blend = glIsEnabled(GL_BLEND);
|
bool blend = glIsEnabled(pp::renderer::gl::blend_state());
|
||||||
glEnable(GL_BLEND);
|
glEnable(pp::renderer::gl::blend_state());
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
auto& tex = *brush->m_tip_texture;
|
auto& tex = *brush->m_tip_texture;
|
||||||
tex.bind();
|
tex.bind();
|
||||||
Canvas::I->m_sampler_brush.bind(0);
|
Canvas::I->m_sampler_brush.bind(0);
|
||||||
Canvas::I->m_plane.draw_fill();
|
Canvas::I->m_plane.draw_fill();
|
||||||
tex.unbind();
|
tex.unbind();
|
||||||
if (!blend) glDisable(GL_BLEND);
|
if (!blend) glDisable(pp::renderer::gl::blend_state());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,15 +425,15 @@ void CanvasModeLine::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, cons
|
|||||||
glm::eulerAngleZ(tip_angle) *
|
glm::eulerAngleZ(tip_angle) *
|
||||||
glm::scale(glm::vec3(tip_scale, 1))
|
glm::scale(glm::vec3(tip_scale, 1))
|
||||||
);
|
);
|
||||||
bool blend = glIsEnabled(GL_BLEND);
|
bool blend = glIsEnabled(pp::renderer::gl::blend_state());
|
||||||
glEnable(GL_BLEND);
|
glEnable(pp::renderer::gl::blend_state());
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
auto& tex = *brush->m_tip_texture;
|
auto& tex = *brush->m_tip_texture;
|
||||||
tex.bind();
|
tex.bind();
|
||||||
Canvas::I->m_sampler_brush.bind(0);
|
Canvas::I->m_sampler_brush.bind(0);
|
||||||
Canvas::I->m_plane.draw_fill();
|
Canvas::I->m_plane.draw_fill();
|
||||||
tex.unbind();
|
tex.unbind();
|
||||||
if (!blend) glDisable(GL_BLEND);
|
if (!blend) glDisable(pp::renderer::gl::blend_state());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,8 +720,8 @@ void CanvasModeMaskFree::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
|
|||||||
m_selection_cam = Canvas::I->get_camera();
|
m_selection_cam = Canvas::I->get_camera();
|
||||||
//m_points2d = poly_intersect(poly_remove_duplicate(m_points2d), Canvas::I->face_to_shape2D(0));
|
//m_points2d = poly_intersect(poly_remove_duplicate(m_points2d), Canvas::I->face_to_shape2D(0));
|
||||||
auto drawer = [this](const glm::mat4& camera, const glm::mat4& proj) {
|
auto drawer = [this](const glm::mat4& camera, const glm::mat4& proj) {
|
||||||
//glEnable(GL_BLEND);
|
// blending state intentionally left unchanged here.
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(pp::renderer::gl::depth_test_state());
|
||||||
ShaderManager::use(kShader::Color);
|
ShaderManager::use(kShader::Color);
|
||||||
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera);
|
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera);
|
||||||
ShaderManager::u_vec4(kShaderUniform::Col,
|
ShaderManager::u_vec4(kShaderUniform::Col,
|
||||||
@@ -783,8 +799,8 @@ void CanvasModeMaskFree::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
|
|||||||
|
|
||||||
void CanvasModeMaskFree::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const glm::mat4& camera)
|
void CanvasModeMaskFree::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const glm::mat4& camera)
|
||||||
{
|
{
|
||||||
bool depth = glIsEnabled(GL_DEPTH_TEST);
|
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(pp::renderer::gl::depth_test_state());
|
||||||
if (m_points.size() > 3)
|
if (m_points.size() > 3)
|
||||||
{
|
{
|
||||||
if (m_dragging)
|
if (m_dragging)
|
||||||
@@ -803,7 +819,7 @@ void CanvasModeMaskFree::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
|
|||||||
// m_shape.draw_stroke();
|
// m_shape.draw_stroke();
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
if (depth) glEnable(GL_DEPTH_TEST);
|
if (depth) glEnable(pp::renderer::gl::depth_test_state());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -840,7 +856,7 @@ void CanvasModeMaskLine::leave(kCanvasMode next)
|
|||||||
if (!m_points.empty())
|
if (!m_points.empty())
|
||||||
{
|
{
|
||||||
auto drawer = [this](const glm::mat4& camera, const glm::mat4& proj) {
|
auto drawer = [this](const glm::mat4& camera, const glm::mat4& proj) {
|
||||||
//glEnable(GL_BLEND);
|
// blending state intentionally left unchanged here.
|
||||||
ShaderManager::use(kShader::Color);
|
ShaderManager::use(kShader::Color);
|
||||||
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera);
|
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera);
|
||||||
ShaderManager::u_vec4(kShaderUniform::Col, {1, 1, 1, 1});
|
ShaderManager::u_vec4(kShaderUniform::Col, {1, 1, 1, 1});
|
||||||
@@ -1248,7 +1264,9 @@ void CanvasModeTransform::enter(kCanvasMode prev)
|
|||||||
Canvas::I->m_layers[Canvas::I->m_current_layer_idx]->rtt(plane).bindFramebuffer();
|
Canvas::I->m_layers[Canvas::I->m_current_layer_idx]->rtt(plane).bindFramebuffer();
|
||||||
m_tex[plane].create(bb_sz.x, bb_sz.y);
|
m_tex[plane].create(bb_sz.x, bb_sz.y);
|
||||||
m_tex[plane].bind();
|
m_tex[plane].bind();
|
||||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
|
glCopyTexSubImage2D(
|
||||||
|
pp::renderer::gl::texture_2d_target(),
|
||||||
|
0, 0, 0, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
|
||||||
m_tex[plane].unbind();
|
m_tex[plane].unbind();
|
||||||
Canvas::I->m_layers[Canvas::I->m_current_layer_idx]->rtt(plane).unbindFramebuffer();
|
Canvas::I->m_layers[Canvas::I->m_current_layer_idx]->rtt(plane).unbindFramebuffer();
|
||||||
});
|
});
|
||||||
@@ -1307,15 +1325,22 @@ void CanvasModeTransform::enter(kCanvasMode prev)
|
|||||||
App::I->render_task([&]
|
App::I->render_task([&]
|
||||||
{
|
{
|
||||||
glViewport(0, 0, layer->w, layer->h);
|
glViewport(0, 0, layer->w, layer->h);
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(pp::renderer::gl::depth_test_state());
|
||||||
glDisable(GL_BLEND);
|
glDisable(pp::renderer::gl::blend_state());
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
ShaderManager::use(kShader::Color);
|
ShaderManager::use(kShader::Color);
|
||||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
||||||
ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 0 });
|
ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 0 });
|
||||||
layer->rtt(i).bindFramebuffer();
|
layer->rtt(i).bindFramebuffer();
|
||||||
// copy framebuffer to action data
|
// copy framebuffer to action data
|
||||||
glReadPixels(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
|
glReadPixels(
|
||||||
|
bb_min.x,
|
||||||
|
bb_min.y,
|
||||||
|
bb_sz.x,
|
||||||
|
bb_sz.y,
|
||||||
|
pp::renderer::gl::rgba_pixel_format(),
|
||||||
|
pp::renderer::gl::unsigned_byte_component_type(),
|
||||||
|
action->m_image[i].get());
|
||||||
for (int j = 0; j < 6; j++)
|
for (int j = 0; j < 6; j++)
|
||||||
m_shape[j].draw_fill();
|
m_shape[j].draw_fill();
|
||||||
layer->rtt(i).unbindFramebuffer();
|
layer->rtt(i).unbindFramebuffer();
|
||||||
@@ -1407,19 +1432,28 @@ void CanvasModeTransform::leave(kCanvasMode next)
|
|||||||
{
|
{
|
||||||
layer->rtt(i).bindFramebuffer();
|
layer->rtt(i).bindFramebuffer();
|
||||||
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(pp::renderer::gl::depth_test_state());
|
||||||
glDisable(GL_BLEND);
|
glDisable(pp::renderer::gl::blend_state());
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
glViewport(0, 0, layer->rtt(i).getWidth(), layer->rtt(i).getHeight());
|
glViewport(0, 0, layer->rtt(i).getWidth(), layer->rtt(i).getHeight());
|
||||||
|
|
||||||
// save fb content for history
|
// save fb content for history
|
||||||
glReadPixels(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
|
glReadPixels(
|
||||||
|
bb_min.x,
|
||||||
|
bb_min.y,
|
||||||
|
bb_sz.x,
|
||||||
|
bb_sz.y,
|
||||||
|
pp::renderer::gl::rgba_pixel_format(),
|
||||||
|
pp::renderer::gl::unsigned_byte_component_type(),
|
||||||
|
action->m_image[i].get());
|
||||||
// copy fb content to texture for blending
|
// copy fb content to texture for blending
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
Canvas::I->m_tex2[i].bind();
|
Canvas::I->m_tex2[i].bind();
|
||||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, bb_min.x, bb_min.y, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
|
glCopyTexSubImage2D(
|
||||||
|
pp::renderer::gl::texture_2d_target(),
|
||||||
|
0, bb_min.x, bb_min.y, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
|
||||||
// slot for m_tex
|
// slot for m_tex
|
||||||
glActiveTexture(GL_TEXTURE1);
|
set_active_texture_unit(1);
|
||||||
for (int j = 0; j < 6; j++)
|
for (int j = 0; j < 6; j++)
|
||||||
{
|
{
|
||||||
ShaderManager::use(kShader::CompDraw);
|
ShaderManager::use(kShader::CompDraw);
|
||||||
@@ -1456,10 +1490,10 @@ void CanvasModeTransform::leave(kCanvasMode next)
|
|||||||
|
|
||||||
void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const glm::mat4& camera)
|
void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const glm::mat4& camera)
|
||||||
{
|
{
|
||||||
bool depth = glIsEnabled(GL_DEPTH_TEST);
|
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(pp::renderer::gl::depth_test_state());
|
||||||
|
|
||||||
glEnable(GL_BLEND);
|
glEnable(pp::renderer::gl::blend_state());
|
||||||
for (int i = 0; i < 6; i++)
|
for (int i = 0; i < 6; i++)
|
||||||
{
|
{
|
||||||
ShaderManager::use(kShader::Color);
|
ShaderManager::use(kShader::Color);
|
||||||
@@ -1470,7 +1504,7 @@ void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
|
|||||||
ShaderManager::use(kShader::Texture);
|
ShaderManager::use(kShader::Texture);
|
||||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||||
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera * m_xform * m_xform_local);
|
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera * m_xform * m_xform_local);
|
||||||
glActiveTexture(GL_TEXTURE0);
|
set_active_texture_unit(0);
|
||||||
m_tex[i].bind();
|
m_tex[i].bind();
|
||||||
Canvas::I->m_sampler_linear.bind(0);
|
Canvas::I->m_sampler_linear.bind(0);
|
||||||
m_shape[i].draw_fill();
|
m_shape[i].draw_fill();
|
||||||
@@ -1499,7 +1533,7 @@ void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
|
|||||||
m_circle.draw_stroke();
|
m_circle.draw_stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (depth) glEnable(GL_DEPTH_TEST);
|
if (depth) glEnable(pp::renderer::gl::depth_test_state());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CanvasModeTransform::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
|
void CanvasModeTransform::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
|
||||||
|
|||||||
915
src/document/document.cpp
Normal file
915
src/document/document.cpp
Normal file
@@ -0,0 +1,915 @@
|
|||||||
|
#include "document/document.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace pp::document {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_config(DocumentConfig config) noexcept
|
||||||
|
{
|
||||||
|
if (config.width == 0 || config.height == 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("document dimensions must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.width > max_canvas_dimension || config.height > max_canvas_dimension) {
|
||||||
|
return pp::foundation::Status::out_of_range("document dimensions exceed the configured limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.layer_count == 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.layer_count > max_layer_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("document layer count exceeds the configured limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::string default_layer_name(std::size_t index)
|
||||||
|
{
|
||||||
|
return "Layer " + std::to_string(index + 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint64_t frame_duration_sum(std::span<const AnimationFrame> frames) noexcept
|
||||||
|
{
|
||||||
|
std::uint64_t duration = 0;
|
||||||
|
for (const auto& frame : frames) {
|
||||||
|
duration += frame.duration_ms;
|
||||||
|
}
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_layer_name(std::string_view name) noexcept
|
||||||
|
{
|
||||||
|
if (name.empty()) {
|
||||||
|
return pp::foundation::Status::invalid_argument("layer name must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.size() > max_layer_name_length) {
|
||||||
|
return pp::foundation::Status::out_of_range("layer name length exceeds the configured limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_layer_opacity(float opacity) noexcept
|
||||||
|
{
|
||||||
|
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
|
||||||
|
return pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_blend_mode(pp::paint::BlendMode blend_mode) noexcept
|
||||||
|
{
|
||||||
|
switch (blend_mode) {
|
||||||
|
case pp::paint::BlendMode::normal:
|
||||||
|
case pp::paint::BlendMode::multiply:
|
||||||
|
case pp::paint::BlendMode::screen:
|
||||||
|
case pp::paint::BlendMode::color_dodge:
|
||||||
|
case pp::paint::BlendMode::overlay:
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::invalid_argument("layer blend mode is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_frame_duration(std::uint32_t duration_ms) noexcept
|
||||||
|
{
|
||||||
|
if (duration_ms < min_frame_duration_ms) {
|
||||||
|
return pp::foundation::Status::invalid_argument("frame duration must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_layer_index(std::size_t index, std::size_t layer_count) noexcept
|
||||||
|
{
|
||||||
|
if (index >= layer_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_frame_index(std::size_t index, std::size_t frame_count) noexcept
|
||||||
|
{
|
||||||
|
if (index >= frame_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("frame index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::size_t> byte_size(
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height,
|
||||||
|
std::uint32_t components,
|
||||||
|
const char* dimensions_overflow_message,
|
||||||
|
const char* byte_size_overflow_message,
|
||||||
|
const char* payload_limit_message,
|
||||||
|
const char* addressable_memory_message) noexcept
|
||||||
|
{
|
||||||
|
const auto width64 = static_cast<std::uint64_t>(width);
|
||||||
|
const auto height64 = static_cast<std::uint64_t>(height);
|
||||||
|
if (width64 > std::numeric_limits<std::uint64_t>::max() / height64) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range(dimensions_overflow_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pixels = width64 * height64;
|
||||||
|
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range(byte_size_overflow_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto bytes = pixels * components;
|
||||||
|
if (bytes > max_face_pixel_payload_bytes) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range(payload_limit_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range(addressable_memory_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height) noexcept
|
||||||
|
{
|
||||||
|
return byte_size(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rgba8_components,
|
||||||
|
"face pixel dimensions overflow",
|
||||||
|
"face pixel byte size overflows",
|
||||||
|
"face pixel payload exceeds the configured limit",
|
||||||
|
"face pixel payload exceeds addressable memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::size_t> alpha8_byte_size(
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height) noexcept
|
||||||
|
{
|
||||||
|
return byte_size(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
alpha8_components,
|
||||||
|
"selection mask dimensions overflow",
|
||||||
|
"selection mask byte size overflows",
|
||||||
|
"selection mask payload exceeds the configured limit",
|
||||||
|
"selection mask payload exceeds addressable memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_face_pixels(
|
||||||
|
LayerFacePixels pixels,
|
||||||
|
std::uint32_t document_width,
|
||||||
|
std::uint32_t document_height) noexcept
|
||||||
|
{
|
||||||
|
if (pixels.face_index >= cube_face_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("cube face index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixels.width == 0 || pixels.height == 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("face pixel dimensions must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixels.x > document_width || pixels.width > document_width - pixels.x
|
||||||
|
|| pixels.y > document_height || pixels.height > document_height - pixels.y) {
|
||||||
|
return pp::foundation::Status::out_of_range("face pixel rectangle is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto expected_bytes = rgba8_byte_size(pixels.width, pixels.height);
|
||||||
|
if (!expected_bytes) {
|
||||||
|
return expected_bytes.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixels.rgba8.size() != expected_bytes.value()) {
|
||||||
|
return pp::foundation::Status::invalid_argument("face pixel payload byte size does not match dimensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_selection_mask(
|
||||||
|
SelectionMask mask,
|
||||||
|
std::uint32_t document_width,
|
||||||
|
std::uint32_t document_height) noexcept
|
||||||
|
{
|
||||||
|
if (mask.face_index >= cube_face_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask.width == 0 || mask.height == 0) {
|
||||||
|
return pp::foundation::Status::invalid_argument("selection mask dimensions must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask.x > document_width || mask.width > document_width - mask.x
|
||||||
|
|| mask.y > document_height || mask.height > document_height - mask.y) {
|
||||||
|
return pp::foundation::Status::out_of_range("selection mask rectangle is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto expected_bytes = alpha8_byte_size(mask.width, mask.height);
|
||||||
|
if (!expected_bytes) {
|
||||||
|
return expected_bytes.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask.alpha8.size() != expected_bytes.value()) {
|
||||||
|
return pp::foundation::Status::invalid_argument("selection mask byte size does not match dimensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status validate_frame_face_pixels(
|
||||||
|
std::span<const AnimationFrame> frames,
|
||||||
|
std::uint32_t document_width,
|
||||||
|
std::uint32_t document_height) noexcept
|
||||||
|
{
|
||||||
|
for (const auto& frame : frames) {
|
||||||
|
std::array<bool, cube_face_count> seen_faces {};
|
||||||
|
for (const auto& pixels : frame.face_pixels) {
|
||||||
|
const auto pixels_status = validate_face_pixels(pixels, document_width, document_height);
|
||||||
|
if (!pixels_status.ok()) {
|
||||||
|
return pixels_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen_faces[pixels.face_index]) {
|
||||||
|
return pp::foundation::Status::invalid_argument(
|
||||||
|
"snapshot contains duplicate face pixel payloads for a cube face");
|
||||||
|
}
|
||||||
|
seen_faces[pixels.face_index] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
|
||||||
|
{
|
||||||
|
const auto status = validate_config(config);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasDocument document;
|
||||||
|
document.width_ = config.width;
|
||||||
|
document.height_ = config.height;
|
||||||
|
document.frames_.push_back(AnimationFrame {});
|
||||||
|
document.layers_.reserve(config.layer_count);
|
||||||
|
for (std::uint32_t i = 0; i < config.layer_count; ++i) {
|
||||||
|
document.layers_.push_back(Layer {
|
||||||
|
.name = default_layer_name(i),
|
||||||
|
.frames = document.frames_,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<CanvasDocument>::success(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(DocumentSnapshotConfig config)
|
||||||
|
{
|
||||||
|
const auto status = validate_config(DocumentConfig {
|
||||||
|
.width = config.width,
|
||||||
|
.height = config.height,
|
||||||
|
.layer_count = static_cast<std::uint32_t>(config.layers.size()),
|
||||||
|
});
|
||||||
|
if (!status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.frames.empty()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document must contain at least one frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.frames.size() > max_frame_count) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasDocument document;
|
||||||
|
document.width_ = config.width;
|
||||||
|
document.height_ = config.height;
|
||||||
|
document.layers_.reserve(config.layers.size());
|
||||||
|
for (const auto& layer_config : config.layers) {
|
||||||
|
const auto name_status = validate_layer_name(layer_config.name);
|
||||||
|
if (!name_status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(name_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto opacity_status = validate_layer_opacity(layer_config.opacity);
|
||||||
|
if (!opacity_status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(opacity_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto blend_status = validate_blend_mode(layer_config.blend_mode);
|
||||||
|
if (!blend_status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(blend_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto layer_frames = layer_config.frames.empty() ? config.frames : layer_config.frames;
|
||||||
|
if (layer_frames.empty()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document layer must contain at least one frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer_frames.size() > max_frame_count) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document layer frame count exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto face_pixels_status = validate_frame_face_pixels(layer_frames, config.width, config.height);
|
||||||
|
if (!face_pixels_status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(face_pixels_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& frame_config : layer_frames) {
|
||||||
|
const auto duration_status = validate_frame_duration(frame_config.duration_ms);
|
||||||
|
if (!duration_status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(duration_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.layers_.push_back(Layer {
|
||||||
|
.name = std::string(layer_config.name),
|
||||||
|
.visible = layer_config.visible,
|
||||||
|
.alpha_locked = layer_config.alpha_locked,
|
||||||
|
.opacity = layer_config.opacity,
|
||||||
|
.blend_mode = layer_config.blend_mode,
|
||||||
|
.frames = {},
|
||||||
|
});
|
||||||
|
document.layers_.back().frames.assign(layer_frames.begin(), layer_frames.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
document.frames_.reserve(config.frames.size());
|
||||||
|
for (const auto& frame_config : config.frames) {
|
||||||
|
const auto duration_status = validate_frame_duration(frame_config.duration_ms);
|
||||||
|
if (!duration_status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(duration_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto face_pixels_status = validate_frame_face_pixels(
|
||||||
|
std::span<const AnimationFrame>(&frame_config, 1),
|
||||||
|
config.width,
|
||||||
|
config.height);
|
||||||
|
if (!face_pixels_status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(face_pixels_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.frames_.push_back(frame_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<bool, cube_face_count> seen_selection_masks {};
|
||||||
|
for (const auto& mask : config.selection_masks) {
|
||||||
|
const auto mask_status = validate_selection_mask(mask, document.width_, document.height_);
|
||||||
|
if (!mask_status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(mask_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen_selection_masks[mask.face_index]) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument(
|
||||||
|
"snapshot contains duplicate selection masks for a cube face"));
|
||||||
|
}
|
||||||
|
seen_selection_masks[mask.face_index] = true;
|
||||||
|
document.selection_masks_.push_back(mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<CanvasDocument>::success(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t CanvasDocument::width() const noexcept
|
||||||
|
{
|
||||||
|
return width_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t CanvasDocument::height() const noexcept
|
||||||
|
{
|
||||||
|
return height_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t CanvasDocument::active_layer_index() const noexcept
|
||||||
|
{
|
||||||
|
return active_layer_index_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t CanvasDocument::active_frame_index() const noexcept
|
||||||
|
{
|
||||||
|
return active_frame_index_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t CanvasDocument::animation_duration_ms() const noexcept
|
||||||
|
{
|
||||||
|
std::uint64_t duration = frame_duration_sum(frames_);
|
||||||
|
for (const auto& layer : layers_) {
|
||||||
|
duration = std::max(duration, frame_duration_sum(layer.frames));
|
||||||
|
}
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<std::uint64_t> CanvasDocument::layer_animation_duration_ms(std::size_t index) const noexcept
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(index, layers_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<std::uint64_t>::failure(index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<std::uint64_t>::success(frame_duration_sum(layers_[index].frames));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t CanvasDocument::face_pixel_payload_count() const noexcept
|
||||||
|
{
|
||||||
|
std::size_t count = 0;
|
||||||
|
for (const auto& layer : layers_) {
|
||||||
|
for (const auto& frame : layer.frames) {
|
||||||
|
count += frame.face_pixels.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t CanvasDocument::selection_mask_payload_count() const noexcept
|
||||||
|
{
|
||||||
|
return selection_masks_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const Layer> CanvasDocument::layers() const noexcept
|
||||||
|
{
|
||||||
|
return layers_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const AnimationFrame> CanvasDocument::frames() const noexcept
|
||||||
|
{
|
||||||
|
return frames_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const SelectionMask> CanvasDocument::selection_masks() const noexcept
|
||||||
|
{
|
||||||
|
return selection_masks_;
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view name)
|
||||||
|
{
|
||||||
|
if (layers_.size() >= max_layer_count) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document layer count exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Layer layer;
|
||||||
|
if (name.empty()) {
|
||||||
|
layer.name = default_layer_name(layers_.size());
|
||||||
|
} else {
|
||||||
|
const auto name_status = validate_layer_name(name);
|
||||||
|
if (!name_status.ok()) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(name_status);
|
||||||
|
}
|
||||||
|
layer.name = std::string(name);
|
||||||
|
}
|
||||||
|
layer.frames = frames_;
|
||||||
|
layers_.push_back(layer);
|
||||||
|
active_layer_index_ = layers_.size() - 1U;
|
||||||
|
return pp::foundation::Result<std::size_t>::success(active_layer_index_);
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::remove_layer(std::size_t index)
|
||||||
|
{
|
||||||
|
if (index >= layers_.size()) {
|
||||||
|
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layers_.size() == 1U) {
|
||||||
|
return pp::foundation::Status::invalid_argument("document must keep at least one layer");
|
||||||
|
}
|
||||||
|
|
||||||
|
layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
|
if (active_layer_index_ >= layers_.size()) {
|
||||||
|
active_layer_index_ = layers_.size() - 1U;
|
||||||
|
} else if (active_layer_index_ > index) {
|
||||||
|
--active_layer_index_;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::move_layer(std::size_t from, std::size_t to)
|
||||||
|
{
|
||||||
|
if (from >= layers_.size() || to >= layers_.size()) {
|
||||||
|
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from == to) {
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto layer = layers_[from];
|
||||||
|
layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(from));
|
||||||
|
layers_.insert(layers_.begin() + static_cast<std::ptrdiff_t>(to), layer);
|
||||||
|
|
||||||
|
if (active_layer_index_ == from) {
|
||||||
|
active_layer_index_ = to;
|
||||||
|
} else if (from < active_layer_index_ && active_layer_index_ <= to) {
|
||||||
|
--active_layer_index_;
|
||||||
|
} else if (to <= active_layer_index_ && active_layer_index_ < from) {
|
||||||
|
++active_layer_index_;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::set_active_layer(std::size_t index) noexcept
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(index, layers_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
active_layer_index_ = index;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::rename_layer(std::size_t index, std::string_view name)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(index, layers_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto name_status = validate_layer_name(name);
|
||||||
|
if (!name_status.ok()) {
|
||||||
|
return name_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
layers_[index].name = std::string(name);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::set_layer_visible(std::size_t index, bool visible) noexcept
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(index, layers_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
layers_[index].visible = visible;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::set_layer_alpha_locked(std::size_t index, bool alpha_locked) noexcept
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(index, layers_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
layers_[index].alpha_locked = alpha_locked;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::set_layer_opacity(std::size_t index, float opacity) noexcept
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(index, layers_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto opacity_status = validate_layer_opacity(opacity);
|
||||||
|
if (!opacity_status.ok()) {
|
||||||
|
return opacity_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
layers_[index].opacity = opacity;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept
|
||||||
|
{
|
||||||
|
const auto index_status = validate_layer_index(index, layers_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto blend_status = validate_blend_mode(blend_mode);
|
||||||
|
if (!blend_status.ok()) {
|
||||||
|
return blend_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
layers_[index].blend_mode = blend_mode;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t duration_ms)
|
||||||
|
{
|
||||||
|
if (frames_.size() >= max_frame_count) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto duration_status = validate_frame_duration(duration_ms);
|
||||||
|
if (!duration_status.ok()) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
duration_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
frames_.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
|
||||||
|
for (auto& layer : layers_) {
|
||||||
|
layer.frames.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
|
||||||
|
}
|
||||||
|
active_frame_index_ = frames_.size() - 1U;
|
||||||
|
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<std::size_t> CanvasDocument::duplicate_frame(std::size_t index)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_frame_index(index, frames_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
index_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frames_.size() >= max_frame_count) {
|
||||||
|
return pp::foundation::Result<std::size_t>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto insert_at = index + 1U;
|
||||||
|
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(insert_at), frames_[index]);
|
||||||
|
for (auto& layer : layers_) {
|
||||||
|
if (index < layer.frames.size()) {
|
||||||
|
layer.frames.insert(
|
||||||
|
layer.frames.begin() + static_cast<std::ptrdiff_t>(insert_at),
|
||||||
|
layer.frames[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
active_frame_index_ = insert_at;
|
||||||
|
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::remove_frame(std::size_t index)
|
||||||
|
{
|
||||||
|
const auto index_status = validate_frame_index(index, frames_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frames_.size() == 1U) {
|
||||||
|
return pp::foundation::Status::invalid_argument("document must keep at least one frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
|
for (auto& layer : layers_) {
|
||||||
|
if (index < layer.frames.size() && layer.frames.size() > 1U) {
|
||||||
|
layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (active_frame_index_ >= frames_.size()) {
|
||||||
|
active_frame_index_ = frames_.size() - 1U;
|
||||||
|
} else if (active_frame_index_ > index) {
|
||||||
|
--active_frame_index_;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::move_frame(std::size_t from, std::size_t to)
|
||||||
|
{
|
||||||
|
if (from >= frames_.size() || to >= frames_.size()) {
|
||||||
|
return pp::foundation::Status::out_of_range("frame index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from == to) {
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto frame = frames_[from];
|
||||||
|
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(from));
|
||||||
|
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(to), frame);
|
||||||
|
for (auto& layer : layers_) {
|
||||||
|
if (from < layer.frames.size() && to < layer.frames.size()) {
|
||||||
|
const auto layer_frame = layer.frames[from];
|
||||||
|
layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(from));
|
||||||
|
layer.frames.insert(layer.frames.begin() + static_cast<std::ptrdiff_t>(to), layer_frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active_frame_index_ == from) {
|
||||||
|
active_frame_index_ = to;
|
||||||
|
} else if (from < active_frame_index_ && active_frame_index_ <= to) {
|
||||||
|
--active_frame_index_;
|
||||||
|
} else if (to <= active_frame_index_ && active_frame_index_ < from) {
|
||||||
|
++active_frame_index_;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept
|
||||||
|
{
|
||||||
|
const auto index_status = validate_frame_index(index, frames_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto duration_status = validate_frame_duration(duration_ms);
|
||||||
|
if (!duration_status.ok()) {
|
||||||
|
return duration_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
frames_[index].duration_ms = duration_ms;
|
||||||
|
for (auto& layer : layers_) {
|
||||||
|
if (index < layer.frames.size()) {
|
||||||
|
layer.frames[index].duration_ms = duration_ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::set_active_frame(std::size_t index) noexcept
|
||||||
|
{
|
||||||
|
const auto index_status = validate_frame_index(index, frames_.size());
|
||||||
|
if (!index_status.ok()) {
|
||||||
|
return index_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
active_frame_index_ = index;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::set_layer_frame_face_pixels(
|
||||||
|
std::size_t layer_index,
|
||||||
|
std::size_t frame_index,
|
||||||
|
LayerFacePixels pixels)
|
||||||
|
{
|
||||||
|
const auto layer_status = validate_layer_index(layer_index, layers_.size());
|
||||||
|
if (!layer_status.ok()) {
|
||||||
|
return layer_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto frame_status = validate_frame_index(frame_index, layers_[layer_index].frames.size());
|
||||||
|
if (!frame_status.ok()) {
|
||||||
|
return frame_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pixels_status = validate_face_pixels(pixels, width_, height_);
|
||||||
|
if (!pixels_status.ok()) {
|
||||||
|
return pixels_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& faces = layers_[layer_index].frames[frame_index].face_pixels;
|
||||||
|
const auto existing = std::find_if(
|
||||||
|
faces.begin(),
|
||||||
|
faces.end(),
|
||||||
|
[face_index = pixels.face_index](const LayerFacePixels& face) {
|
||||||
|
return face.face_index == face_index;
|
||||||
|
});
|
||||||
|
if (existing == faces.end()) {
|
||||||
|
faces.push_back(std::move(pixels));
|
||||||
|
} else {
|
||||||
|
*existing = std::move(pixels);
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::set_selection_mask(SelectionMask mask)
|
||||||
|
{
|
||||||
|
const auto mask_status = validate_selection_mask(mask, width_, height_);
|
||||||
|
if (!mask_status.ok()) {
|
||||||
|
return mask_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto existing = std::find_if(
|
||||||
|
selection_masks_.begin(),
|
||||||
|
selection_masks_.end(),
|
||||||
|
[face_index = mask.face_index](const SelectionMask& candidate) {
|
||||||
|
return candidate.face_index == face_index;
|
||||||
|
});
|
||||||
|
if (existing == selection_masks_.end()) {
|
||||||
|
selection_masks_.push_back(std::move(mask));
|
||||||
|
} else {
|
||||||
|
*existing = std::move(mask);
|
||||||
|
}
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status CanvasDocument::clear_selection_mask(std::uint32_t face_index) noexcept
|
||||||
|
{
|
||||||
|
if (face_index >= cube_face_count) {
|
||||||
|
return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto existing = std::find_if(
|
||||||
|
selection_masks_.begin(),
|
||||||
|
selection_masks_.end(),
|
||||||
|
[face_index](const SelectionMask& candidate) {
|
||||||
|
return candidate.face_index == face_index;
|
||||||
|
});
|
||||||
|
if (existing == selection_masks_.end()) {
|
||||||
|
return pp::foundation::Status::out_of_range("selection mask face is not present");
|
||||||
|
}
|
||||||
|
|
||||||
|
selection_masks_.erase(existing);
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<DocumentHistory> DocumentHistory::create(
|
||||||
|
CanvasDocument initial_document,
|
||||||
|
std::size_t max_entries)
|
||||||
|
{
|
||||||
|
if (max_entries < min_document_history_entries) {
|
||||||
|
return pp::foundation::Result<DocumentHistory>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document history must keep at least two entries"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max_entries > max_document_history_entries) {
|
||||||
|
return pp::foundation::Result<DocumentHistory>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document history entry limit exceeds the configured limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentHistory history;
|
||||||
|
history.max_entries_ = max_entries;
|
||||||
|
history.entries_.reserve(max_entries);
|
||||||
|
history.entries_.push_back(initial_document);
|
||||||
|
return pp::foundation::Result<DocumentHistory>::success(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CanvasDocument& DocumentHistory::current() const noexcept
|
||||||
|
{
|
||||||
|
return entries_[current_index_];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t DocumentHistory::size() const noexcept
|
||||||
|
{
|
||||||
|
return entries_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t DocumentHistory::current_index() const noexcept
|
||||||
|
{
|
||||||
|
return current_index_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DocumentHistory::can_undo() const noexcept
|
||||||
|
{
|
||||||
|
return current_index_ > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DocumentHistory::can_redo() const noexcept
|
||||||
|
{
|
||||||
|
return current_index_ + 1U < entries_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status DocumentHistory::apply(CanvasDocument next_document)
|
||||||
|
{
|
||||||
|
if (entries_.empty()) {
|
||||||
|
return pp::foundation::Status::invalid_argument("document history is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (can_redo()) {
|
||||||
|
entries_.erase(entries_.begin() + static_cast<std::ptrdiff_t>(current_index_ + 1U), entries_.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
entries_.push_back(next_document);
|
||||||
|
if (entries_.size() > max_entries_) {
|
||||||
|
entries_.erase(entries_.begin());
|
||||||
|
} else {
|
||||||
|
++current_index_;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_index_ = entries_.size() - 1U;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status DocumentHistory::undo() noexcept
|
||||||
|
{
|
||||||
|
if (!can_undo()) {
|
||||||
|
return pp::foundation::Status::out_of_range("document history has no undo entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
--current_index_;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status DocumentHistory::redo() noexcept
|
||||||
|
{
|
||||||
|
if (!can_redo()) {
|
||||||
|
return pp::foundation::Status::out_of_range("document history has no redo entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
++current_index_;
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
153
src/document/document.h
Normal file
153
src/document/document.h
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
#include "paint/blend.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pp::document {
|
||||||
|
|
||||||
|
constexpr std::uint32_t max_canvas_dimension = 131072;
|
||||||
|
constexpr std::uint32_t max_layer_count = 1024;
|
||||||
|
constexpr std::uint32_t max_frame_count = 100000;
|
||||||
|
constexpr std::uint32_t min_frame_duration_ms = 1;
|
||||||
|
constexpr std::size_t min_document_history_entries = 2;
|
||||||
|
constexpr std::size_t max_document_history_entries = 10000;
|
||||||
|
constexpr std::size_t max_layer_name_length = 128;
|
||||||
|
constexpr std::uint32_t cube_face_count = 6;
|
||||||
|
constexpr std::uint32_t rgba8_components = 4;
|
||||||
|
constexpr std::uint32_t alpha8_components = 1;
|
||||||
|
constexpr std::uint64_t max_face_pixel_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
|
||||||
|
|
||||||
|
struct DocumentConfig {
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::uint32_t layer_count = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LayerFacePixels {
|
||||||
|
std::uint32_t face_index = 0;
|
||||||
|
std::uint32_t x = 0;
|
||||||
|
std::uint32_t y = 0;
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::vector<std::uint8_t> rgba8;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SelectionMask {
|
||||||
|
std::uint32_t face_index = 0;
|
||||||
|
std::uint32_t x = 0;
|
||||||
|
std::uint32_t y = 0;
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::vector<std::uint8_t> alpha8;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AnimationFrame {
|
||||||
|
std::uint32_t duration_ms = 100;
|
||||||
|
std::vector<LayerFacePixels> face_pixels;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Layer {
|
||||||
|
std::string name;
|
||||||
|
bool visible = true;
|
||||||
|
bool alpha_locked = false;
|
||||||
|
float opacity = 1.0F;
|
||||||
|
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
||||||
|
std::vector<AnimationFrame> frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentLayerConfig {
|
||||||
|
std::string_view name;
|
||||||
|
bool visible = true;
|
||||||
|
bool alpha_locked = false;
|
||||||
|
float opacity = 1.0F;
|
||||||
|
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
||||||
|
std::span<const AnimationFrame> frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentSnapshotConfig {
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::span<const DocumentLayerConfig> layers;
|
||||||
|
std::span<const AnimationFrame> frames;
|
||||||
|
std::span<const SelectionMask> selection_masks;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CanvasDocument {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create(DocumentConfig config);
|
||||||
|
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create_from_snapshot(DocumentSnapshotConfig config);
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint32_t width() const noexcept;
|
||||||
|
[[nodiscard]] std::uint32_t height() const noexcept;
|
||||||
|
[[nodiscard]] std::size_t active_layer_index() const noexcept;
|
||||||
|
[[nodiscard]] std::size_t active_frame_index() const noexcept;
|
||||||
|
[[nodiscard]] std::uint64_t animation_duration_ms() const noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::uint64_t> layer_animation_duration_ms(std::size_t index) const noexcept;
|
||||||
|
[[nodiscard]] std::size_t face_pixel_payload_count() const noexcept;
|
||||||
|
[[nodiscard]] std::size_t selection_mask_payload_count() const noexcept;
|
||||||
|
[[nodiscard]] std::span<const Layer> layers() const noexcept;
|
||||||
|
[[nodiscard]] std::span<const AnimationFrame> frames() const noexcept;
|
||||||
|
[[nodiscard]] std::span<const SelectionMask> selection_masks() const noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::size_t> add_layer(std::string_view name);
|
||||||
|
[[nodiscard]] pp::foundation::Status remove_layer(std::size_t index);
|
||||||
|
[[nodiscard]] pp::foundation::Status move_layer(std::size_t from, std::size_t to);
|
||||||
|
[[nodiscard]] pp::foundation::Status set_active_layer(std::size_t index) noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Status rename_layer(std::size_t index, std::string_view name);
|
||||||
|
[[nodiscard]] pp::foundation::Status set_layer_visible(std::size_t index, bool visible) noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Status set_layer_alpha_locked(std::size_t index, bool alpha_locked) noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Status set_layer_opacity(std::size_t index, float opacity) noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Status set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::size_t> add_frame(std::uint32_t duration_ms);
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::size_t> duplicate_frame(std::size_t index);
|
||||||
|
[[nodiscard]] pp::foundation::Status remove_frame(std::size_t index);
|
||||||
|
[[nodiscard]] pp::foundation::Status move_frame(std::size_t from, std::size_t to);
|
||||||
|
[[nodiscard]] pp::foundation::Status set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Status set_active_frame(std::size_t index) noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Status set_layer_frame_face_pixels(
|
||||||
|
std::size_t layer_index,
|
||||||
|
std::size_t frame_index,
|
||||||
|
LayerFacePixels pixels);
|
||||||
|
[[nodiscard]] pp::foundation::Status set_selection_mask(SelectionMask mask);
|
||||||
|
[[nodiscard]] pp::foundation::Status clear_selection_mask(std::uint32_t face_index) noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::uint32_t width_ = 0;
|
||||||
|
std::uint32_t height_ = 0;
|
||||||
|
std::size_t active_layer_index_ = 0;
|
||||||
|
std::size_t active_frame_index_ = 0;
|
||||||
|
std::vector<Layer> layers_;
|
||||||
|
std::vector<AnimationFrame> frames_;
|
||||||
|
std::vector<SelectionMask> selection_masks_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DocumentHistory {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] static pp::foundation::Result<DocumentHistory> create(
|
||||||
|
CanvasDocument initial_document,
|
||||||
|
std::size_t max_entries = 256);
|
||||||
|
|
||||||
|
[[nodiscard]] const CanvasDocument& current() const noexcept;
|
||||||
|
[[nodiscard]] std::size_t size() const noexcept;
|
||||||
|
[[nodiscard]] std::size_t current_index() const noexcept;
|
||||||
|
[[nodiscard]] bool can_undo() const noexcept;
|
||||||
|
[[nodiscard]] bool can_redo() const noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Status apply(CanvasDocument next_document);
|
||||||
|
[[nodiscard]] pp::foundation::Status undo() noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Status redo() noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::size_t max_entries_ = 0;
|
||||||
|
std::size_t current_index_ = 0;
|
||||||
|
std::vector<CanvasDocument> entries_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
107
src/document/ppi_export.cpp
Normal file
107
src/document/ppi_export.cpp
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#include "document/ppi_export.h"
|
||||||
|
|
||||||
|
#include "assets/image_pixels.h"
|
||||||
|
#include "assets/ppi_header.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace pp::document {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::uint32_t> ppi_blend_mode(
|
||||||
|
pp::paint::BlendMode blend_mode) noexcept
|
||||||
|
{
|
||||||
|
switch (blend_mode) {
|
||||||
|
case pp::paint::BlendMode::normal:
|
||||||
|
return pp::foundation::Result<std::uint32_t>::success(0);
|
||||||
|
case pp::paint::BlendMode::multiply:
|
||||||
|
return pp::foundation::Result<std::uint32_t>::success(1);
|
||||||
|
case pp::paint::BlendMode::screen:
|
||||||
|
return pp::foundation::Result<std::uint32_t>::success(2);
|
||||||
|
case pp::paint::BlendMode::color_dodge:
|
||||||
|
return pp::foundation::Result<std::uint32_t>::success(3);
|
||||||
|
case pp::paint::BlendMode::overlay:
|
||||||
|
return pp::foundation::Result<std::uint32_t>::success(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<std::uint32_t>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document layer blend mode cannot be exported to PPI"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<std::vector<std::byte>> export_ppi_project_document(
|
||||||
|
const CanvasDocument& document)
|
||||||
|
{
|
||||||
|
std::vector<std::vector<pp::assets::PpiFrameConfig>> frame_configs;
|
||||||
|
frame_configs.reserve(document.layers().size());
|
||||||
|
std::vector<pp::assets::PpiLayerConfig> layer_configs;
|
||||||
|
layer_configs.reserve(document.layers().size());
|
||||||
|
std::vector<std::vector<std::byte>> payloads;
|
||||||
|
payloads.reserve(document.face_pixel_payload_count());
|
||||||
|
std::vector<pp::assets::PpiDirtyFacePayloadConfig> dirty_faces;
|
||||||
|
dirty_faces.reserve(document.face_pixel_payload_count());
|
||||||
|
|
||||||
|
for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) {
|
||||||
|
const auto& layer = document.layers()[layer_index];
|
||||||
|
const auto blend_mode = ppi_blend_mode(layer.blend_mode);
|
||||||
|
if (!blend_mode) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(blend_mode.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& frames = frame_configs.emplace_back();
|
||||||
|
frames.reserve(layer.frames.size());
|
||||||
|
for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) {
|
||||||
|
const auto& frame = layer.frames[frame_index];
|
||||||
|
frames.push_back(pp::assets::PpiFrameConfig {
|
||||||
|
.duration_ms = frame.duration_ms,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const auto& face : frame.face_pixels) {
|
||||||
|
const auto encoded = pp::assets::encode_png_rgba8(
|
||||||
|
face.width,
|
||||||
|
face.height,
|
||||||
|
face.rgba8);
|
||||||
|
if (!encoded) {
|
||||||
|
return pp::foundation::Result<std::vector<std::byte>>::failure(encoded.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
payloads.push_back(encoded.value());
|
||||||
|
const auto& payload = payloads.back();
|
||||||
|
dirty_faces.push_back(pp::assets::PpiDirtyFacePayloadConfig {
|
||||||
|
.layer_index = static_cast<std::uint32_t>(layer_index),
|
||||||
|
.frame_index = static_cast<std::uint32_t>(frame_index),
|
||||||
|
.face_index = face.face_index,
|
||||||
|
.x = face.x,
|
||||||
|
.y = face.y,
|
||||||
|
.width = face.width,
|
||||||
|
.height = face.height,
|
||||||
|
.png_rgba8 = std::span<const std::byte>(payload.data(), payload.size()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer_configs.push_back(pp::assets::PpiLayerConfig {
|
||||||
|
.name = layer.name,
|
||||||
|
.metadata = pp::assets::PpiLayerMetadataConfig {
|
||||||
|
.opacity = layer.opacity,
|
||||||
|
.blend_mode = blend_mode.value(),
|
||||||
|
.alpha_locked = layer.alpha_locked,
|
||||||
|
.visible = layer.visible,
|
||||||
|
},
|
||||||
|
.frames = std::span<const pp::assets::PpiFrameConfig>(frames.data(), frames.size()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::assets::create_ppi_project(pp::assets::PpiProjectConfig {
|
||||||
|
.width = document.width(),
|
||||||
|
.height = document.height(),
|
||||||
|
.layers = std::span<const pp::assets::PpiLayerConfig>(layer_configs.data(), layer_configs.size()),
|
||||||
|
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(dirty_faces.data(), dirty_faces.size()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
14
src/document/ppi_export.h
Normal file
14
src/document/ppi_export.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "document/document.h"
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pp::document {
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> export_ppi_project_document(
|
||||||
|
const CanvasDocument& document);
|
||||||
|
|
||||||
|
}
|
||||||
117
src/document/ppi_import.cpp
Normal file
117
src/document/ppi_import.cpp
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#include "document/ppi_import.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pp::document {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<pp::paint::BlendMode> ppi_layer_blend_mode(
|
||||||
|
std::uint32_t blend_mode) noexcept
|
||||||
|
{
|
||||||
|
switch (blend_mode) {
|
||||||
|
case 0:
|
||||||
|
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::normal);
|
||||||
|
case 1:
|
||||||
|
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::multiply);
|
||||||
|
case 2:
|
||||||
|
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::screen);
|
||||||
|
case 3:
|
||||||
|
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::color_dodge);
|
||||||
|
case 4:
|
||||||
|
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::overlay);
|
||||||
|
default:
|
||||||
|
return pp::foundation::Result<pp::paint::BlendMode>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI layer blend mode is not supported by pp_document"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<CanvasDocument> document_from_ppi_index(
|
||||||
|
const pp::assets::PpiProjectIndex& project)
|
||||||
|
{
|
||||||
|
if (project.body.layers.empty()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI project has no layers"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& reference_frames = project.body.layers.front().frames;
|
||||||
|
if (reference_frames.empty()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("PPI project has no frames"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<AnimationFrame> frames;
|
||||||
|
frames.reserve(reference_frames.size());
|
||||||
|
for (const auto& frame : reference_frames) {
|
||||||
|
frames.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::vector<AnimationFrame>> layer_frames;
|
||||||
|
layer_frames.reserve(project.body.layers.size());
|
||||||
|
std::vector<DocumentLayerConfig> layers;
|
||||||
|
layers.reserve(project.body.layers.size());
|
||||||
|
for (const auto& layer : project.body.layers) {
|
||||||
|
const auto blend_mode = ppi_layer_blend_mode(layer.blend_mode);
|
||||||
|
if (!blend_mode) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(blend_mode.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& frame_list = layer_frames.emplace_back();
|
||||||
|
frame_list.reserve(layer.frames.size());
|
||||||
|
for (const auto& frame : layer.frames) {
|
||||||
|
frame_list.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
layers.push_back(DocumentLayerConfig {
|
||||||
|
.name = layer.name,
|
||||||
|
.visible = layer.visible,
|
||||||
|
.alpha_locked = layer.alpha_locked,
|
||||||
|
.opacity = layer.opacity,
|
||||||
|
.blend_mode = blend_mode.value(),
|
||||||
|
.frames = std::span<const AnimationFrame>(frame_list.data(), frame_list.size()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||||
|
.width = project.body.summary.width,
|
||||||
|
.height = project.body.summary.height,
|
||||||
|
.layers = layers,
|
||||||
|
.frames = frames,
|
||||||
|
.selection_masks = {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<CanvasDocument> import_ppi_project_document(
|
||||||
|
const pp::assets::PpiDecodedProjectImages& project)
|
||||||
|
{
|
||||||
|
auto document = document_from_ppi_index(project.project);
|
||||||
|
if (!document) {
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto value = document.value();
|
||||||
|
for (const auto& face : project.faces) {
|
||||||
|
const auto status = value.set_layer_frame_face_pixels(
|
||||||
|
face.layer_index,
|
||||||
|
face.frame_index,
|
||||||
|
LayerFacePixels {
|
||||||
|
.face_index = face.face_index,
|
||||||
|
.x = face.descriptor.x0,
|
||||||
|
.y = face.descriptor.y0,
|
||||||
|
.width = face.image.width,
|
||||||
|
.height = face.image.height,
|
||||||
|
.rgba8 = face.image.pixels,
|
||||||
|
});
|
||||||
|
if (!status.ok()) {
|
||||||
|
return pp::foundation::Result<CanvasDocument>::failure(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<CanvasDocument>::success(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
src/document/ppi_import.h
Normal file
11
src/document/ppi_import.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "assets/ppi_header.h"
|
||||||
|
#include "document/document.h"
|
||||||
|
|
||||||
|
namespace pp::document {
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<CanvasDocument> import_ppi_project_document(
|
||||||
|
const pp::assets::PpiDecodedProjectImages& project);
|
||||||
|
|
||||||
|
}
|
||||||
229
src/font.cpp
229
src/font.cpp
@@ -5,6 +5,152 @@
|
|||||||
#include "asset.h"
|
#include "asset.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "renderer_gl/opengl_capabilities.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] GLint font_atlas_internal_format() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).internal_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLint font_atlas_pixel_format() noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).pixel_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GLenum texture_unit(std::uint32_t unit_index) noexcept
|
||||||
|
{
|
||||||
|
return static_cast<GLenum>(pp::renderer::gl::active_texture_unit(unit_index));
|
||||||
|
}
|
||||||
|
|
||||||
|
void gen_buffers_adapter(std::uint32_t count, std::uint32_t* ids) noexcept
|
||||||
|
{
|
||||||
|
glGenBuffers(static_cast<GLsizei>(count), ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind_buffer_adapter(std::uint32_t target, std::uint32_t buffer) noexcept
|
||||||
|
{
|
||||||
|
glBindBuffer(static_cast<GLenum>(target), static_cast<GLuint>(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
void buffer_data_adapter(
|
||||||
|
std::uint32_t target,
|
||||||
|
std::intptr_t byte_count,
|
||||||
|
const void* data,
|
||||||
|
std::uint32_t usage) noexcept
|
||||||
|
{
|
||||||
|
glBufferData(static_cast<GLenum>(target), static_cast<GLsizeiptr>(byte_count), data, static_cast<GLenum>(usage));
|
||||||
|
}
|
||||||
|
|
||||||
|
void gen_vertex_arrays_adapter(std::uint32_t count, std::uint32_t* ids) noexcept
|
||||||
|
{
|
||||||
|
glGenVertexArrays(static_cast<GLsizei>(count), ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind_vertex_array_adapter(std::uint32_t vertex_array) noexcept
|
||||||
|
{
|
||||||
|
glBindVertexArray(static_cast<GLuint>(vertex_array));
|
||||||
|
}
|
||||||
|
|
||||||
|
void enable_vertex_attrib_array_adapter(std::uint32_t index) noexcept
|
||||||
|
{
|
||||||
|
glEnableVertexAttribArray(static_cast<GLuint>(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex_attrib_pointer_adapter(
|
||||||
|
std::uint32_t index,
|
||||||
|
std::int32_t component_count,
|
||||||
|
std::uint32_t component_type,
|
||||||
|
std::uint8_t normalized,
|
||||||
|
std::int32_t stride,
|
||||||
|
const void* offset) noexcept
|
||||||
|
{
|
||||||
|
glVertexAttribPointer(
|
||||||
|
static_cast<GLuint>(index),
|
||||||
|
static_cast<GLint>(component_count),
|
||||||
|
static_cast<GLenum>(component_type),
|
||||||
|
static_cast<GLboolean>(normalized),
|
||||||
|
static_cast<GLsizei>(stride),
|
||||||
|
offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_elements_adapter(
|
||||||
|
std::uint32_t mode,
|
||||||
|
std::int32_t count,
|
||||||
|
std::uint32_t index_type,
|
||||||
|
const void* index_offset) noexcept
|
||||||
|
{
|
||||||
|
glDrawElements(
|
||||||
|
static_cast<GLenum>(mode),
|
||||||
|
static_cast<GLsizei>(count),
|
||||||
|
static_cast<GLenum>(index_type),
|
||||||
|
index_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_arrays_adapter(std::uint32_t mode, std::int32_t first, std::int32_t count) noexcept
|
||||||
|
{
|
||||||
|
glDrawArrays(static_cast<GLenum>(mode), static_cast<GLint>(first), static_cast<GLsizei>(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::span<const pp::renderer::gl::OpenGlVertexAttribute> text_mesh_vertex_attributes() noexcept
|
||||||
|
{
|
||||||
|
static const std::array<pp::renderer::gl::OpenGlVertexAttribute, 2> attributes {
|
||||||
|
pp::renderer::gl::OpenGlVertexAttribute {
|
||||||
|
.index = 0U,
|
||||||
|
.component_count = 2,
|
||||||
|
.component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
|
||||||
|
.normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
|
||||||
|
.stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
|
||||||
|
.offset = 0U,
|
||||||
|
},
|
||||||
|
pp::renderer::gl::OpenGlVertexAttribute {
|
||||||
|
.index = 1U,
|
||||||
|
.component_count = 2,
|
||||||
|
.component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
|
||||||
|
.normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
|
||||||
|
.stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
|
||||||
|
.offset = static_cast<std::uintptr_t>(sizeof(float) * 2),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::renderer::gl::OpenGlMeshCreateDispatch text_mesh_create_dispatch() noexcept
|
||||||
|
{
|
||||||
|
return pp::renderer::gl::OpenGlMeshCreateDispatch {
|
||||||
|
.gen_buffers = gen_buffers_adapter,
|
||||||
|
.bind_buffer = bind_buffer_adapter,
|
||||||
|
.buffer_data = buffer_data_adapter,
|
||||||
|
.gen_vertex_arrays = gen_vertex_arrays_adapter,
|
||||||
|
.bind_vertex_array = bind_vertex_array_adapter,
|
||||||
|
.enable_vertex_attrib_array = enable_vertex_attrib_array_adapter,
|
||||||
|
.vertex_attrib_pointer = vertex_attrib_pointer_adapter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::renderer::gl::OpenGlBufferUploadDispatch text_buffer_upload_dispatch() noexcept
|
||||||
|
{
|
||||||
|
return pp::renderer::gl::OpenGlBufferUploadDispatch {
|
||||||
|
.bind_buffer = bind_buffer_adapter,
|
||||||
|
.buffer_data = buffer_data_adapter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] pp::renderer::gl::OpenGlMeshDrawDispatch text_mesh_draw_dispatch() noexcept
|
||||||
|
{
|
||||||
|
return pp::renderer::gl::OpenGlMeshDrawDispatch {
|
||||||
|
.bind_vertex_array = bind_vertex_array_adapter,
|
||||||
|
.draw_elements = draw_elements_adapter,
|
||||||
|
.draw_arrays = draw_arrays_adapter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
std::map<std::string, Font> FontManager::m_fonts;
|
std::map<std::string, Font> FontManager::m_fonts;
|
||||||
Sampler FontManager::m_sampler;
|
Sampler FontManager::m_sampler;
|
||||||
@@ -52,7 +198,7 @@ bool Font::load(const std::string& ttf, int font_size, float font_scale)
|
|||||||
// offset = 0;
|
// offset = 0;
|
||||||
stbtt_BakeFontBitmap(file.m_data, 0, (float)font_size*scale, bitmap.get(), w, h, start_char, num_chars, chars.data());
|
stbtt_BakeFontBitmap(file.m_data, 0, (float)font_size*scale, bitmap.get(), w, h, start_char, num_chars, chars.data());
|
||||||
calc_bounds();
|
calc_bounds();
|
||||||
font_tex.create(w, h, GL_R8, GL_RED, bitmap.get());
|
font_tex.create(w, h, font_atlas_internal_format(), font_atlas_pixel_format(), bitmap.get());
|
||||||
file.close();
|
file.close();
|
||||||
size = font_size;
|
size = font_size;
|
||||||
return true;
|
return true;
|
||||||
@@ -155,18 +301,22 @@ bool TextMesh::create()
|
|||||||
{
|
{
|
||||||
App::I->render_task([this]
|
App::I->render_task([this]
|
||||||
{
|
{
|
||||||
glGenBuffers(2, font_buffers);
|
const auto mesh = pp::renderer::gl::create_opengl_mesh_objects(
|
||||||
#if USE_VBO
|
pp::renderer::gl::OpenGlMeshUpload {
|
||||||
glGenVertexArrays(1, &font_array);
|
.vertex_data = nullptr,
|
||||||
glBindVertexArray(font_array);
|
.vertex_byte_count = 0,
|
||||||
glEnableVertexAttribArray(0);
|
.index_data = nullptr,
|
||||||
glEnableVertexAttribArray(1);
|
.index_byte_count = 0,
|
||||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
|
.indexed = true,
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
|
.vertex_array_count = 1U,
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)0);
|
.attributes = text_mesh_vertex_attributes(),
|
||||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)(sizeof(float) * 2));
|
},
|
||||||
glBindVertexArray(0);
|
text_mesh_create_dispatch());
|
||||||
#endif // USE_VBO
|
if (mesh.ok()) {
|
||||||
|
font_buffers[0] = static_cast<GLuint>(mesh.value().vertex_buffer);
|
||||||
|
font_buffers[1] = static_cast<GLuint>(mesh.value().index_buffer);
|
||||||
|
font_array = static_cast<GLuint>(mesh.value().vertex_arrays[0]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -254,12 +404,24 @@ void TextMesh::update(const std::string& text, const std::string& font, int size
|
|||||||
font_array_count = (int)idx.size();
|
font_array_count = (int)idx.size();
|
||||||
App::I->render_task([&]
|
App::I->render_task([&]
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
|
(void)pp::renderer::gl::upload_opengl_buffer_data(
|
||||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size() * sizeof(GLushort), idx.data(), GL_STATIC_DRAW);
|
pp::renderer::gl::OpenGlBufferUpload {
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
|
.target = pp::renderer::gl::element_array_buffer_target(),
|
||||||
glBufferData(GL_ARRAY_BUFFER, v.size() * sizeof(glm::vec4), v.data(), GL_STATIC_DRAW);
|
.buffer_id = font_buffers[1],
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
.data = idx.data(),
|
||||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
.byte_count = static_cast<std::intptr_t>(idx.size() * sizeof(GLushort)),
|
||||||
|
.usage = pp::renderer::gl::static_draw_buffer_usage(),
|
||||||
|
},
|
||||||
|
text_buffer_upload_dispatch());
|
||||||
|
(void)pp::renderer::gl::upload_opengl_buffer_data(
|
||||||
|
pp::renderer::gl::OpenGlBufferUpload {
|
||||||
|
.target = pp::renderer::gl::array_buffer_target(),
|
||||||
|
.buffer_id = font_buffers[0],
|
||||||
|
.data = v.data(),
|
||||||
|
.byte_count = static_cast<std::intptr_t>(v.size() * sizeof(glm::vec4)),
|
||||||
|
.usage = pp::renderer::gl::static_draw_buffer_usage(),
|
||||||
|
},
|
||||||
|
text_buffer_upload_dispatch());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,27 +431,20 @@ void TextMesh::draw()
|
|||||||
auto& f = FontManager::get(font, size, weight, italic);
|
auto& f = FontManager::get(font, size, weight, italic);
|
||||||
if (f.font_tex.ready())
|
if (f.font_tex.ready())
|
||||||
{
|
{
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(texture_unit(0U));
|
||||||
f.font_tex.bind();
|
f.font_tex.bind();
|
||||||
FontManager::m_sampler.bind(0);
|
FontManager::m_sampler.bind(0);
|
||||||
|
|
||||||
#if USE_VBO
|
(void)pp::renderer::gl::draw_opengl_mesh(
|
||||||
glBindVertexArray(font_array);
|
pp::renderer::gl::OpenGlMeshDraw {
|
||||||
glDrawElements(GL_TRIANGLES, font_array_count, GL_UNSIGNED_SHORT, 0);
|
.vertex_array = font_array,
|
||||||
glBindVertexArray(0);
|
.mode = pp::renderer::gl::primitive_mode_for_fill_count(3U),
|
||||||
#else
|
.count = font_array_count,
|
||||||
glEnableVertexAttribArray(0);
|
.indexed = true,
|
||||||
glEnableVertexAttribArray(1);
|
.index_type = pp::renderer::gl::index_type_for_index_size(sizeof(GLushort)),
|
||||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
|
.index_offset = nullptr,
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
|
},
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)0);
|
text_mesh_draw_dispatch());
|
||||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)(sizeof(float) * 2));
|
|
||||||
glDrawElements(GL_TRIANGLES, font_array_count, GL_UNSIGNED_SHORT, 0);
|
|
||||||
glDisableVertexAttribArray(0);
|
|
||||||
glDisableVertexAttribArray(1);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
||||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
|
||||||
#endif // USE_VBO
|
|
||||||
|
|
||||||
f.font_tex.unbind();
|
f.font_tex.unbind();
|
||||||
FontManager::m_sampler.unbind();
|
FontManager::m_sampler.unbind();
|
||||||
|
|||||||
169
src/foundation/binary_stream.cpp
Normal file
169
src/foundation/binary_stream.cpp
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
#include "foundation/binary_stream.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] bool overlaps_backing_storage(
|
||||||
|
const std::vector<std::byte>& backing,
|
||||||
|
std::span<const std::byte> bytes) noexcept
|
||||||
|
{
|
||||||
|
if (backing.empty() || bytes.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto backing_begin = reinterpret_cast<std::uintptr_t>(backing.data());
|
||||||
|
const auto backing_end = backing_begin + backing.size();
|
||||||
|
const auto bytes_begin = reinterpret_cast<std::uintptr_t>(bytes.data());
|
||||||
|
const auto bytes_end = bytes_begin + bytes.size();
|
||||||
|
return bytes_begin < backing_end && backing_begin < bytes_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteReader::ByteReader(std::span<const std::byte> bytes) noexcept
|
||||||
|
: bytes_(bytes)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t ByteReader::position() const noexcept
|
||||||
|
{
|
||||||
|
return position_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t ByteReader::size() const noexcept
|
||||||
|
{
|
||||||
|
return bytes_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t ByteReader::remaining() const noexcept
|
||||||
|
{
|
||||||
|
return bytes_.size() - position_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ByteReader::empty() const noexcept
|
||||||
|
{
|
||||||
|
return remaining() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status ByteReader::seek(std::size_t position) noexcept
|
||||||
|
{
|
||||||
|
if (position > bytes_.size()) {
|
||||||
|
return Status::out_of_range("seek position is outside the stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
position_ = position;
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::uint8_t> ByteReader::read_u8() noexcept
|
||||||
|
{
|
||||||
|
const auto bytes = read_bytes(1);
|
||||||
|
if (!bytes) {
|
||||||
|
return Result<std::uint8_t>::failure(bytes.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result<std::uint8_t>::success(static_cast<std::uint8_t>(bytes.value()[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::uint16_t> ByteReader::read_u16_le() noexcept
|
||||||
|
{
|
||||||
|
const auto bytes = read_bytes(2);
|
||||||
|
if (!bytes) {
|
||||||
|
return Result<std::uint16_t>::failure(bytes.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto b0 = static_cast<std::uint16_t>(bytes.value()[0]);
|
||||||
|
const auto b1 = static_cast<std::uint16_t>(bytes.value()[1]);
|
||||||
|
return Result<std::uint16_t>::success(static_cast<std::uint16_t>(b0 | (b1 << 8U)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::uint32_t> ByteReader::read_u32_le() noexcept
|
||||||
|
{
|
||||||
|
const auto bytes = read_bytes(4);
|
||||||
|
if (!bytes) {
|
||||||
|
return Result<std::uint32_t>::failure(bytes.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto b0 = static_cast<std::uint32_t>(bytes.value()[0]);
|
||||||
|
const auto b1 = static_cast<std::uint32_t>(bytes.value()[1]);
|
||||||
|
const auto b2 = static_cast<std::uint32_t>(bytes.value()[2]);
|
||||||
|
const auto b3 = static_cast<std::uint32_t>(bytes.value()[3]);
|
||||||
|
return Result<std::uint32_t>::success(b0 | (b1 << 8U) | (b2 << 16U) | (b3 << 24U));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::span<const std::byte>> ByteReader::read_bytes(std::size_t count) noexcept
|
||||||
|
{
|
||||||
|
if (count > remaining()) {
|
||||||
|
return Result<std::span<const std::byte>>::failure(
|
||||||
|
Status::out_of_range("read would move beyond the end of the stream"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto start = position_;
|
||||||
|
position_ += count;
|
||||||
|
return Result<std::span<const std::byte>>::success(bytes_.subspan(start, count));
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteWriter::ByteWriter(std::vector<std::byte>& bytes) noexcept
|
||||||
|
: bytes_(&bytes)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t ByteWriter::size() const noexcept
|
||||||
|
{
|
||||||
|
return bytes_ == nullptr ? 0 : bytes_->size();
|
||||||
|
}
|
||||||
|
|
||||||
|
Status ByteWriter::write_u8(std::uint8_t value)
|
||||||
|
{
|
||||||
|
if (bytes_ == nullptr) {
|
||||||
|
return Status::invalid_argument("writer has no backing storage");
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes_->push_back(static_cast<std::byte>(value));
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Status ByteWriter::write_u16_le(std::uint16_t value)
|
||||||
|
{
|
||||||
|
if (bytes_ == nullptr) {
|
||||||
|
return Status::invalid_argument("writer has no backing storage");
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes_->push_back(static_cast<std::byte>(value & 0xffU));
|
||||||
|
bytes_->push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Status ByteWriter::write_u32_le(std::uint32_t value)
|
||||||
|
{
|
||||||
|
if (bytes_ == nullptr) {
|
||||||
|
return Status::invalid_argument("writer has no backing storage");
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes_->push_back(static_cast<std::byte>(value & 0xffU));
|
||||||
|
bytes_->push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
||||||
|
bytes_->push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
|
||||||
|
bytes_->push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Status ByteWriter::write_bytes(std::span<const std::byte> bytes)
|
||||||
|
{
|
||||||
|
if (bytes_ == nullptr) {
|
||||||
|
return Status::invalid_argument("writer has no backing storage");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlaps_backing_storage(*bytes_, bytes)) {
|
||||||
|
const std::vector<std::byte> copy(bytes.begin(), bytes.end());
|
||||||
|
bytes_->insert(bytes_->end(), copy.begin(), copy.end());
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes_->insert(bytes_->end(), bytes.begin(), bytes.end());
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
src/foundation/binary_stream.h
Normal file
46
src/foundation/binary_stream.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
class ByteReader {
|
||||||
|
public:
|
||||||
|
explicit ByteReader(std::span<const std::byte> bytes) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t position() const noexcept;
|
||||||
|
[[nodiscard]] std::size_t size() const noexcept;
|
||||||
|
[[nodiscard]] std::size_t remaining() const noexcept;
|
||||||
|
[[nodiscard]] bool empty() const noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] Status seek(std::size_t position) noexcept;
|
||||||
|
[[nodiscard]] Result<std::uint8_t> read_u8() noexcept;
|
||||||
|
[[nodiscard]] Result<std::uint16_t> read_u16_le() noexcept;
|
||||||
|
[[nodiscard]] Result<std::uint32_t> read_u32_le() noexcept;
|
||||||
|
[[nodiscard]] Result<std::span<const std::byte>> read_bytes(std::size_t count) noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::span<const std::byte> bytes_;
|
||||||
|
std::size_t position_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ByteWriter {
|
||||||
|
public:
|
||||||
|
explicit ByteWriter(std::vector<std::byte>& bytes) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t size() const noexcept;
|
||||||
|
[[nodiscard]] Status write_u8(std::uint8_t value);
|
||||||
|
[[nodiscard]] Status write_u16_le(std::uint16_t value);
|
||||||
|
[[nodiscard]] Status write_u32_le(std::uint32_t value);
|
||||||
|
[[nodiscard]] Status write_bytes(std::span<const std::byte> bytes);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::byte>* bytes_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
97
src/foundation/event.cpp
Normal file
97
src/foundation/event.cpp
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#include "foundation/event.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
EventDispatcher::EventDispatcher(std::size_t max_subscriptions) noexcept
|
||||||
|
: max_subscriptions_(max_subscriptions)
|
||||||
|
{
|
||||||
|
subscriptions_.reserve(std::min(max_subscriptions_, max_event_subscriptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t EventDispatcher::size() const noexcept
|
||||||
|
{
|
||||||
|
return subscriptions_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventDispatcher::empty() const noexcept
|
||||||
|
{
|
||||||
|
return subscriptions_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t EventDispatcher::max_subscriptions() const noexcept
|
||||||
|
{
|
||||||
|
return max_subscriptions_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::uint64_t> EventDispatcher::subscribe(std::uint32_t type, EventCallback callback, void* user_data)
|
||||||
|
{
|
||||||
|
if (max_subscriptions_ == 0U || max_subscriptions_ > max_event_subscriptions) {
|
||||||
|
return Result<std::uint64_t>::failure(
|
||||||
|
Status::out_of_range("event dispatcher capacity is outside the configured range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == 0U) {
|
||||||
|
return Result<std::uint64_t>::failure(Status::invalid_argument("event type must not be zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback == nullptr) {
|
||||||
|
return Result<std::uint64_t>::failure(Status::invalid_argument("event callback must not be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptions_.size() >= max_subscriptions_) {
|
||||||
|
return Result<std::uint64_t>::failure(Status::out_of_range("event dispatcher is full"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto id = next_subscription_id_++;
|
||||||
|
subscriptions_.push_back(EventSubscription {
|
||||||
|
.id = id,
|
||||||
|
.type = type,
|
||||||
|
.callback = callback,
|
||||||
|
.user_data = user_data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Result<std::uint64_t>::success(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Status EventDispatcher::unsubscribe(std::uint64_t subscription_id) noexcept
|
||||||
|
{
|
||||||
|
const auto found = std::find_if(
|
||||||
|
subscriptions_.begin(),
|
||||||
|
subscriptions_.end(),
|
||||||
|
[subscription_id](const EventSubscription& subscription) {
|
||||||
|
return subscription.id == subscription_id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (found == subscriptions_.end()) {
|
||||||
|
return Status::out_of_range("event subscription id was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions_.erase(found);
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t EventDispatcher::publish(const Event& event) const noexcept
|
||||||
|
{
|
||||||
|
if (event.type == 0U) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t delivered = 0;
|
||||||
|
for (const auto& subscription : subscriptions_) {
|
||||||
|
if (subscription.type == event.type) {
|
||||||
|
subscription.callback(event, subscription.user_data);
|
||||||
|
++delivered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return delivered;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventDispatcher::clear() noexcept
|
||||||
|
{
|
||||||
|
subscriptions_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
src/foundation/event.h
Normal file
48
src/foundation/event.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
constexpr std::size_t max_event_subscriptions = 65536;
|
||||||
|
|
||||||
|
struct Event {
|
||||||
|
std::uint32_t type = 0;
|
||||||
|
std::uint64_t source_id = 0;
|
||||||
|
std::uint64_t frame_id = 0;
|
||||||
|
std::uint64_t payload_u64 = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
using EventCallback = void (*)(const Event& event, void* user_data) noexcept;
|
||||||
|
|
||||||
|
struct EventSubscription {
|
||||||
|
std::uint64_t id = 0;
|
||||||
|
std::uint32_t type = 0;
|
||||||
|
EventCallback callback = nullptr;
|
||||||
|
void* user_data = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EventDispatcher {
|
||||||
|
public:
|
||||||
|
explicit EventDispatcher(std::size_t max_subscriptions = max_event_subscriptions) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t size() const noexcept;
|
||||||
|
[[nodiscard]] bool empty() const noexcept;
|
||||||
|
[[nodiscard]] std::size_t max_subscriptions() const noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] Result<std::uint64_t> subscribe(std::uint32_t type, EventCallback callback, void* user_data);
|
||||||
|
[[nodiscard]] Status unsubscribe(std::uint64_t subscription_id) noexcept;
|
||||||
|
[[nodiscard]] std::size_t publish(const Event& event) const noexcept;
|
||||||
|
void clear() noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::size_t max_subscriptions_ = max_event_subscriptions;
|
||||||
|
std::uint64_t next_subscription_id_ = 1;
|
||||||
|
std::vector<EventSubscription> subscriptions_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
93
src/foundation/log.cpp
Normal file
93
src/foundation/log.cpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#include "foundation/log.h"
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] bool should_write(LogLevel level, LogLevel min_level) noexcept
|
||||||
|
{
|
||||||
|
return static_cast<std::uint8_t>(level) >= static_cast<std::uint8_t>(min_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::Logger(ILogSink& sink) noexcept
|
||||||
|
: sink_(&sink)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::set_min_level(LogLevel level) noexcept
|
||||||
|
{
|
||||||
|
min_level_ = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogLevel Logger::min_level() const noexcept
|
||||||
|
{
|
||||||
|
return min_level_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status Logger::write(
|
||||||
|
LogLevel level,
|
||||||
|
std::string_view component,
|
||||||
|
std::string_view message,
|
||||||
|
std::uint64_t frame_id,
|
||||||
|
std::uint64_t stroke_id,
|
||||||
|
std::uint64_t thread_id) noexcept
|
||||||
|
{
|
||||||
|
if (component.empty()) {
|
||||||
|
return Status::invalid_argument("log component must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.empty()) {
|
||||||
|
return Status::invalid_argument("log message must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!should_write(level, min_level_)) {
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
sink_->write(LogRecord {
|
||||||
|
.level = level,
|
||||||
|
.component = std::string(component),
|
||||||
|
.message = std::string(message),
|
||||||
|
.frame_id = frame_id,
|
||||||
|
.stroke_id = stroke_id,
|
||||||
|
.thread_id = thread_id,
|
||||||
|
});
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryLogSink::write(const LogRecord& record) noexcept
|
||||||
|
{
|
||||||
|
records_.push_back(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<LogRecord>& MemoryLogSink::records() const noexcept
|
||||||
|
{
|
||||||
|
return records_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryLogSink::clear() noexcept
|
||||||
|
{
|
||||||
|
records_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* log_level_name(LogLevel level) noexcept
|
||||||
|
{
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel::trace:
|
||||||
|
return "trace";
|
||||||
|
case LogLevel::debug:
|
||||||
|
return "debug";
|
||||||
|
case LogLevel::info:
|
||||||
|
return "info";
|
||||||
|
case LogLevel::warning:
|
||||||
|
return "warning";
|
||||||
|
case LogLevel::error:
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
67
src/foundation/log.h
Normal file
67
src/foundation/log.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
enum class LogLevel : std::uint8_t {
|
||||||
|
trace,
|
||||||
|
debug,
|
||||||
|
info,
|
||||||
|
warning,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LogRecord {
|
||||||
|
LogLevel level = LogLevel::info;
|
||||||
|
std::string component;
|
||||||
|
std::string message;
|
||||||
|
std::uint64_t frame_id = 0;
|
||||||
|
std::uint64_t stroke_id = 0;
|
||||||
|
std::uint64_t thread_id = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ILogSink {
|
||||||
|
public:
|
||||||
|
virtual ~ILogSink() = default;
|
||||||
|
virtual void write(const LogRecord& record) noexcept = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
public:
|
||||||
|
explicit Logger(ILogSink& sink) noexcept;
|
||||||
|
|
||||||
|
void set_min_level(LogLevel level) noexcept;
|
||||||
|
[[nodiscard]] LogLevel min_level() const noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] Status write(
|
||||||
|
LogLevel level,
|
||||||
|
std::string_view component,
|
||||||
|
std::string_view message,
|
||||||
|
std::uint64_t frame_id = 0,
|
||||||
|
std::uint64_t stroke_id = 0,
|
||||||
|
std::uint64_t thread_id = 0) noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ILogSink* sink_ = nullptr;
|
||||||
|
LogLevel min_level_ = LogLevel::trace;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MemoryLogSink final : public ILogSink {
|
||||||
|
public:
|
||||||
|
void write(const LogRecord& record) noexcept override;
|
||||||
|
[[nodiscard]] const std::vector<LogRecord>& records() const noexcept;
|
||||||
|
void clear() noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<LogRecord> records_;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] const char* log_level_name(LogLevel level) noexcept;
|
||||||
|
|
||||||
|
}
|
||||||
37
src/foundation/parse.cpp
Normal file
37
src/foundation/parse.cpp
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#include "foundation/parse.h"
|
||||||
|
|
||||||
|
#include <charconv>
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
Result<std::uint32_t> parse_u32(std::string_view text) noexcept
|
||||||
|
{
|
||||||
|
if (text.empty()) {
|
||||||
|
return Result<std::uint32_t>::failure(
|
||||||
|
Status::invalid_argument("value must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.front() == '-' || text.front() == '+') {
|
||||||
|
return Result<std::uint32_t>::failure(
|
||||||
|
Status::invalid_argument("value must be an unsigned integer without a sign"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t value = 0;
|
||||||
|
const auto* begin = text.data();
|
||||||
|
const auto* end = text.data() + text.size();
|
||||||
|
const auto [ptr, ec] = std::from_chars(begin, end, value);
|
||||||
|
|
||||||
|
if (ec == std::errc::result_out_of_range) {
|
||||||
|
return Result<std::uint32_t>::failure(
|
||||||
|
Status::out_of_range("value is outside the uint32 range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ec != std::errc {} || ptr != end) {
|
||||||
|
return Result<std::uint32_t>::failure(
|
||||||
|
Status::invalid_argument("value must contain only decimal digits"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result<std::uint32_t>::success(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
src/foundation/parse.h
Normal file
12
src/foundation/parse.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
[[nodiscard]] Result<std::uint32_t> parse_u32(std::string_view text) noexcept;
|
||||||
|
|
||||||
|
}
|
||||||
87
src/foundation/result.h
Normal file
87
src/foundation/result.h
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
enum class StatusCode {
|
||||||
|
ok,
|
||||||
|
invalid_argument,
|
||||||
|
out_of_range,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Status {
|
||||||
|
StatusCode code = StatusCode::ok;
|
||||||
|
const char* message = "ok";
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr bool ok() const noexcept
|
||||||
|
{
|
||||||
|
return code == StatusCode::ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static constexpr Status success() noexcept
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static constexpr Status invalid_argument(const char* message) noexcept
|
||||||
|
{
|
||||||
|
return { StatusCode::invalid_argument, message };
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static constexpr Status out_of_range(const char* message) noexcept
|
||||||
|
{
|
||||||
|
return { StatusCode::out_of_range, message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class Result {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] static constexpr Result success(T value) noexcept
|
||||||
|
{
|
||||||
|
return Result(std::move(value), Status::success());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static constexpr Result failure(Status status) noexcept
|
||||||
|
{
|
||||||
|
return Result(T{}, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr bool ok() const noexcept
|
||||||
|
{
|
||||||
|
return status_.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr explicit operator bool() const noexcept
|
||||||
|
{
|
||||||
|
return ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr const T& value() const noexcept
|
||||||
|
{
|
||||||
|
return value_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr T& value() noexcept
|
||||||
|
{
|
||||||
|
return value_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr Status status() const noexcept
|
||||||
|
{
|
||||||
|
return status_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
constexpr Result(T value, Status status) noexcept
|
||||||
|
: value_(std::move(value))
|
||||||
|
, status_(status)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
T value_{};
|
||||||
|
Status status_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
83
src/foundation/task_queue.cpp
Normal file
83
src/foundation/task_queue.cpp
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#include "foundation/task_queue.h"
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
TaskQueue::TaskQueue(std::size_t max_entries) noexcept
|
||||||
|
: max_entries_(max_entries)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t TaskQueue::size() const noexcept
|
||||||
|
{
|
||||||
|
return tasks_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskQueue::empty() const noexcept
|
||||||
|
{
|
||||||
|
return tasks_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t TaskQueue::max_entries() const noexcept
|
||||||
|
{
|
||||||
|
return max_entries_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status TaskQueue::push(TaskItem task)
|
||||||
|
{
|
||||||
|
if (max_entries_ == 0U || max_entries_ > max_task_queue_entries) {
|
||||||
|
return Status::out_of_range("task queue capacity is outside the configured range");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.callback == nullptr) {
|
||||||
|
return Status::invalid_argument("task callback must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks_.size() >= max_entries_) {
|
||||||
|
return Status::out_of_range("task queue is full");
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks_.push_back(task);
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<TaskItem> TaskQueue::pop() noexcept
|
||||||
|
{
|
||||||
|
if (tasks_.empty()) {
|
||||||
|
return Result<TaskItem>::failure(Status::out_of_range("task queue is empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto task = tasks_.front();
|
||||||
|
tasks_.pop_front();
|
||||||
|
return Result<TaskItem>::success(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
Status TaskQueue::run_next() noexcept
|
||||||
|
{
|
||||||
|
auto task = pop();
|
||||||
|
if (!task.ok()) {
|
||||||
|
return task.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
task.value().callback(task.value().user_data);
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t TaskQueue::run_all() noexcept
|
||||||
|
{
|
||||||
|
std::size_t count = 0;
|
||||||
|
while (!tasks_.empty()) {
|
||||||
|
const auto status = run_next();
|
||||||
|
if (!status.ok()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskQueue::clear() noexcept
|
||||||
|
{
|
||||||
|
tasks_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
40
src/foundation/task_queue.h
Normal file
40
src/foundation/task_queue.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
namespace pp::foundation {
|
||||||
|
|
||||||
|
constexpr std::size_t max_task_queue_entries = 65536;
|
||||||
|
|
||||||
|
using TaskCallback = void (*)(void* user_data) noexcept;
|
||||||
|
|
||||||
|
struct TaskItem {
|
||||||
|
TaskCallback callback = nullptr;
|
||||||
|
void* user_data = nullptr;
|
||||||
|
std::uint64_t id = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TaskQueue {
|
||||||
|
public:
|
||||||
|
explicit TaskQueue(std::size_t max_entries = max_task_queue_entries) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t size() const noexcept;
|
||||||
|
[[nodiscard]] bool empty() const noexcept;
|
||||||
|
[[nodiscard]] std::size_t max_entries() const noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] Status push(TaskItem task);
|
||||||
|
[[nodiscard]] Result<TaskItem> pop() noexcept;
|
||||||
|
[[nodiscard]] Status run_next() noexcept;
|
||||||
|
[[nodiscard]] std::size_t run_all() noexcept;
|
||||||
|
void clear() noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::size_t max_entries_ = max_task_queue_entries;
|
||||||
|
std::deque<TaskItem> tasks_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user