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/.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\\Wintab32.dll" );
|
||||
LOG("calling LoadLibrary");
|
||||
ghWintab = LoadLibrary(L"Wintab32.dll");
|
||||
ghWintab = LoadLibraryW(L"Wintab32.dll");
|
||||
LOG("LoadLibrary called");
|
||||
|
||||
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_progress_bar.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__
|
||||
#include <Foundation/Foundation.h>
|
||||
@@ -12,32 +19,156 @@
|
||||
#endif
|
||||
#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
|
||||
|
||||
std::deque<AppTask> App::render_tasklist;
|
||||
std::mutex App::render_task_mutex;
|
||||
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::id App::render_thread_id;
|
||||
bool App::render_running = false;
|
||||
@@ -57,15 +188,14 @@ void App::create()
|
||||
|
||||
void App::open_document(std::string path)
|
||||
{
|
||||
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
|
||||
std::smatch m;
|
||||
if (!std::regex_search(path, m, r))
|
||||
const auto route = pp::app::classify_document_open_path(path);
|
||||
if (!route)
|
||||
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);
|
||||
mb->on_submit = [this, path] (Node* target) {
|
||||
@@ -73,7 +203,7 @@ void App::open_document(std::string path)
|
||||
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);
|
||||
mb->on_submit = [this, path] (Node* target) {
|
||||
@@ -83,6 +213,8 @@ void App::open_document(std::string path)
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string base = route.value().directory;
|
||||
const std::string name = route.value().name;
|
||||
auto open_action = [this, path, base, name] {
|
||||
doc_name = name;
|
||||
doc_dir = base;
|
||||
@@ -109,7 +241,7 @@ void App::open_document(std::string path)
|
||||
});
|
||||
ActionManager::clear();
|
||||
};
|
||||
if (!Canvas::I->m_unsaved)
|
||||
if (open_plan == pp::app::DocumentOpenPlanAction::open_project_now)
|
||||
{
|
||||
open_action();
|
||||
}
|
||||
@@ -127,25 +259,19 @@ void App::open_document(std::string path)
|
||||
bool App::request_close()
|
||||
{
|
||||
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;
|
||||
if (!dialog_already_opened)
|
||||
if (close_decision == pp::app::CloseRequestDecision::show_unsaved_prompt)
|
||||
{
|
||||
auto* m = layout[main_id]->add_child<NodeMessageBox>();
|
||||
m->m_title->set_text("Unsaved document");
|
||||
m->m_message->set_text("Do you want to close without saving?");
|
||||
m->btn_ok->m_text->set_text("Yes");
|
||||
m->btn_ok->on_click = [this](Node*) {
|
||||
#ifdef _WIN32
|
||||
destroy_window();
|
||||
//PostQuitMessage(0);
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[osx_view close];
|
||||
});
|
||||
#elif __LINUX__
|
||||
glfwSetWindowShouldClose(glfw_window, GLFW_TRUE);
|
||||
#endif
|
||||
request_app_close();
|
||||
Canvas::I->m_unsaved = false;
|
||||
};
|
||||
m->btn_cancel->m_text->set_text("No");
|
||||
@@ -160,8 +286,13 @@ bool App::request_close()
|
||||
|
||||
void App::clear()
|
||||
{
|
||||
glClearColor(.1f, .1f, .1f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
const auto status = pp::renderer::gl::clear_panopainter_default_target(
|
||||
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()
|
||||
@@ -170,9 +301,9 @@ void App::initAssets()
|
||||
FontManager::init();
|
||||
|
||||
LOG("initializing assets create sampler");
|
||||
sampler.create(GL_NEAREST);
|
||||
sampler_stencil.create(GL_LINEAR, GL_REPEAT);
|
||||
sampler_linear.create(GL_LINEAR);
|
||||
sampler.create(nearest_texture_filter());
|
||||
sampler_stencil.create(linear_texture_filter(), repeat_texture_wrap());
|
||||
sampler_linear.create(linear_texture_filter());
|
||||
m_face_plane.create<1>(2, 2);
|
||||
sphere.create<8, 8>(1);
|
||||
LOG("initializing assets load uvs texture");
|
||||
@@ -181,78 +312,16 @@ void App::initAssets()
|
||||
|
||||
void App::initLog()
|
||||
{
|
||||
#if defined(__IOS__)
|
||||
[ios_view init_dirs];
|
||||
#elif defined(__OSX__)
|
||||
[osx_app init_dirs];
|
||||
#elif defined(_WIN32)
|
||||
//CHAR my_documents[MAX_PATH];
|
||||
//HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
|
||||
|
||||
//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
|
||||
const auto paths = prepare_storage_paths();
|
||||
if (!paths.data_path.empty())
|
||||
data_path = paths.data_path;
|
||||
if (!paths.recording_path.empty())
|
||||
rec_path = paths.recording_path;
|
||||
if (!paths.temporary_path.empty())
|
||||
tmp_path = paths.temporary_path;
|
||||
|
||||
// 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.file_init();
|
||||
@@ -385,56 +454,29 @@ void App::upload(std::string filename, std::string name, std::function<void(floa
|
||||
#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()
|
||||
{
|
||||
#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);
|
||||
|
||||
render_task([]
|
||||
{
|
||||
LOG("GL version: %s", glGetString(GL_VERSION));
|
||||
LOG("GL vendor: %s", glGetString(GL_VENDOR));
|
||||
LOG("GL renderer: %s", glGetString(GL_RENDERER));
|
||||
LOG("GLSL version: %s", glGetString(GL_SHADING_LANGUAGE_VERSION));
|
||||
App::I->install_render_debug_callback();
|
||||
const auto runtime_info_result = pp::renderer::gl::query_opengl_runtime_info(
|
||||
pp::renderer::gl::OpenGlRuntimeInfoDispatch {
|
||||
.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;
|
||||
//glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
|
||||
@@ -447,13 +489,16 @@ void App::init()
|
||||
// }
|
||||
//}
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
#if defined(_WIN32) || defined(__OSX__)
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
glEnable(GL_LINE_SMOOTH);
|
||||
#endif
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX);
|
||||
App::I->apply_render_platform_hints();
|
||||
const auto startup_state_status = pp::renderer::gl::apply_panopainter_initial_state(
|
||||
pp::renderer::gl::OpenGlStateDispatch {
|
||||
.enable = enable_opengl_state,
|
||||
.disable = disable_opengl_state,
|
||||
.blend_func = set_opengl_blend_func,
|
||||
.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;
|
||||
@@ -468,7 +513,7 @@ void App::init()
|
||||
initLayout();
|
||||
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))
|
||||
rec_start();
|
||||
@@ -482,18 +527,7 @@ void App::init()
|
||||
|
||||
void App::async_start()
|
||||
{
|
||||
#if __OSX__
|
||||
[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
|
||||
acquire_render_context();
|
||||
}
|
||||
|
||||
void App::async_redraw()
|
||||
@@ -504,30 +538,12 @@ void App::async_redraw()
|
||||
|
||||
void App::async_end()
|
||||
{
|
||||
#if __OSX__
|
||||
[osx_view async_unlock];
|
||||
#elif __IOS__
|
||||
[ios_view async_unlock];
|
||||
#elif __ANDROID__
|
||||
android_async_unlock();
|
||||
#elif _WIN32
|
||||
async_unlock();
|
||||
#endif
|
||||
release_render_context();
|
||||
}
|
||||
|
||||
void App::async_swap()
|
||||
{
|
||||
#if __OSX__
|
||||
[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
|
||||
present_render_context();
|
||||
}
|
||||
|
||||
bool App::update_ui_observer(Node *n)
|
||||
@@ -569,7 +585,13 @@ bool App::update_ui_observer(Node *n)
|
||||
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;
|
||||
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();
|
||||
return true;
|
||||
}
|
||||
@@ -588,32 +610,36 @@ void App::draw(float dt)
|
||||
{
|
||||
uirtt.bindFramebuffer();
|
||||
uirtt.clear();
|
||||
glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight());
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
|
||||
.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++)
|
||||
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++)
|
||||
layout_designer[main_id]->m_children[i]->watch(observer);
|
||||
//msgbox->watch(observer);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
apply_app_scissor_test(false);
|
||||
uirtt.unbindFramebuffer();
|
||||
}
|
||||
|
||||
if (!vr_only)
|
||||
{
|
||||
#if __IOS__
|
||||
[ios_view->glview bindDrawable];
|
||||
#else
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
#endif
|
||||
glViewport(off_x, off_y, (GLsizei)width, (GLsizei)height);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
bind_main_render_target();
|
||||
apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
|
||||
.x = static_cast<std::int32_t>(off_x),
|
||||
.y = static_cast<std::int32_t>(off_y),
|
||||
.width = static_cast<std::int32_t>(width),
|
||||
.height = static_cast<std::int32_t>(height),
|
||||
});
|
||||
apply_app_scissor_test(true);
|
||||
for (int i = 0; i < layout[main_id]->m_children.size(); i++)
|
||||
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++)
|
||||
layout_designer[main_id]->m_children[i]->watch(observer);
|
||||
//msgbox->watch(observer);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
apply_app_scissor_test(false);
|
||||
}
|
||||
|
||||
redraw = false;
|
||||
@@ -636,25 +662,26 @@ void App::update(float dt)
|
||||
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;
|
||||
|
||||
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);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(canvas->m_canvas->m_touch_lock);
|
||||
const auto toolbar = pp::app::plan_canvas_tool_button_state(
|
||||
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-erase")->set_active(mode == kCanvasMode::Erase);
|
||||
layout[main_id]->find<NodeButton>("btn-cam")->set_active(mode == kCanvasMode::Camera);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(mode == kCanvasMode::Line);
|
||||
layout[main_id]->find<NodeButton>("btn-grid")->set_active(mode == kCanvasMode::Grid);
|
||||
layout[main_id]->find<NodeButton>("btn-copy")->set_active(mode == kCanvasMode::Copy);
|
||||
layout[main_id]->find<NodeButton>("btn-cut")->set_active(mode == kCanvasMode::Cut);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(mode == kCanvasMode::MaskFree);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(mode == kCanvasMode::MaskLine);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(mode == kCanvasMode::FloodFill);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(toolbar.pen_active);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(toolbar.erase_active);
|
||||
layout[main_id]->find<NodeButton>("btn-cam")->set_active(toolbar.camera_active);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(toolbar.line_active);
|
||||
layout[main_id]->find<NodeButton>("btn-grid")->set_active(toolbar.grid_active);
|
||||
layout[main_id]->find<NodeButton>("btn-copy")->set_active(toolbar.copy_active);
|
||||
layout[main_id]->find<NodeButton>("btn-cut")->set_active(toolbar.cut_active);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(toolbar.mask_free_active);
|
||||
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(toolbar.mask_line_active);
|
||||
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"))
|
||||
{
|
||||
static char buffer[128];
|
||||
sprintf(buffer, "History memory: %.2f Mb", bytes / 1024.f / 1024.f);
|
||||
txt->set_text(buffer);
|
||||
const auto label = pp::app::make_history_memory_label(bytes);
|
||||
txt->set_text(label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,91 +724,102 @@ void App::update_rec_frames()
|
||||
{
|
||||
if (auto txt = layout[main_id]->find<NodeText>("txt-rec"))
|
||||
{
|
||||
if (rec_running && Canvas::I->m_encoder)
|
||||
{
|
||||
static char buffer[128];
|
||||
sprintf(buffer, "Recorded %d frames", Canvas::I->m_encoder->frames_count());
|
||||
txt->set_text(buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
txt->set_text("");
|
||||
}
|
||||
const auto label = pp::app::make_recording_frame_label(
|
||||
rec_running,
|
||||
Canvas::I->m_encoder != nullptr,
|
||||
Canvas::I->m_encoder ? Canvas::I->m_encoder->frames_count() : 0);
|
||||
txt->set_text(label.text.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
#if __WIN__
|
||||
win32_renderdoc_frame_start();
|
||||
#endif
|
||||
begin_render_capture_frame();
|
||||
}
|
||||
|
||||
void App::renderdoc_frame_end()
|
||||
{
|
||||
#if __WIN__
|
||||
win32_renderdoc_frame_end();
|
||||
#endif
|
||||
end_render_capture_frame();
|
||||
}
|
||||
|
||||
void App::rec_clear()
|
||||
{
|
||||
rec_stop();
|
||||
#if defined(__IOS__) || defined(__OSX__)
|
||||
delete_all_files_in_path(rec_path);
|
||||
#endif
|
||||
rec_count = 0;
|
||||
const auto plan = pp::app::plan_recording_clear(
|
||||
rec_running,
|
||||
platform_deletes_recorded_files_on_clear()
|
||||
);
|
||||
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();
|
||||
}
|
||||
|
||||
void App::rec_start()
|
||||
{
|
||||
if (!rec_running)
|
||||
const auto plan = pp::app::plan_recording_start(rec_running);
|
||||
switch (plan)
|
||||
{
|
||||
update_rec_frames();
|
||||
rec_thread = std::thread(&App::rec_loop, this);
|
||||
case pp::app::RecordingStartAction::start_thread:
|
||||
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()
|
||||
{
|
||||
if (rec_running)
|
||||
const auto plan = pp::app::plan_recording_stop(rec_running);
|
||||
switch (plan)
|
||||
{
|
||||
rec_running = false;
|
||||
rec_cv.notify_all();
|
||||
if (rec_thread.joinable())
|
||||
rec_thread.join();
|
||||
update_rec_frames();
|
||||
case pp::app::RecordingStopAction::stop_thread:
|
||||
break;
|
||||
case pp::app::RecordingStopAction::no_op_not_running:
|
||||
return;
|
||||
}
|
||||
|
||||
rec_running = false;
|
||||
rec_cv.notify_all();
|
||||
if (rec_thread.joinable())
|
||||
rec_thread.join();
|
||||
update_rec_frames();
|
||||
}
|
||||
|
||||
void App::rec_export(std::string path)
|
||||
{
|
||||
int progress = 0;
|
||||
int tot = rec_count;
|
||||
const auto plan = pp::app::plan_recording_export(static_cast<std::size_t>(rec_count));
|
||||
auto pb = layout[main_id]->add_child<NodeProgressBar>();
|
||||
pb->m_progress->SetWidthP(0);
|
||||
pb->m_title->set_text("Exporting MP4 movie");
|
||||
pb->m_total = plan.progress_total;
|
||||
pb->m_count = 0;
|
||||
|
||||
/*
|
||||
#if defined(__IOS__) || defined(__OSX__)
|
||||
export_mp4(rec_path, width, height, rec_count, ^(float) {
|
||||
pb->m_progress->SetWidthP((float)progress / tot * 100.f);
|
||||
pb->increment();
|
||||
});
|
||||
#endif
|
||||
*/
|
||||
@@ -915,7 +952,7 @@ void App::ui_thread_tick()
|
||||
update(0);
|
||||
render_task([this]
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
bind_default_render_target();
|
||||
clear();
|
||||
draw(0);
|
||||
async_swap();
|
||||
@@ -931,9 +968,7 @@ void App::ui_thread_main()
|
||||
ui_thread_id = std::this_thread::get_id();
|
||||
ui_running = true;
|
||||
|
||||
#if __ANDROID__
|
||||
android_attach_jni();
|
||||
#endif
|
||||
attach_ui_thread();
|
||||
|
||||
LOG("ui thread init()");
|
||||
init();
|
||||
@@ -971,10 +1006,7 @@ void App::ui_thread_main()
|
||||
float dt = std::chrono::duration<float>(t_now - t_start).count();
|
||||
t_start = t_now;
|
||||
|
||||
#ifdef _WIN32
|
||||
extern void win32_update_stylus(float dt);
|
||||
win32_update_stylus(dt);
|
||||
#endif
|
||||
update_platform_frame(dt);
|
||||
|
||||
// increment timers
|
||||
t_frame += dt;
|
||||
@@ -982,33 +1014,28 @@ void App::ui_thread_main()
|
||||
|
||||
if (t_fps_counter > 1.f)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
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
|
||||
report_rendered_frames(rendered_frames);
|
||||
t_fps_counter = 0;
|
||||
rendered_frames = 0;
|
||||
}
|
||||
|
||||
#if /*_DEBUG &&*/ (_WIN32 || __OSX__)
|
||||
t_reloader += dt;
|
||||
if (t_reloader > 1.0)
|
||||
if (platform_enables_live_asset_reloading())
|
||||
{
|
||||
t_reloader = 0;
|
||||
if (ShaderManager::reload())
|
||||
t_reloader += dt;
|
||||
if (t_reloader > 1.0)
|
||||
{
|
||||
stroke->update_controls();
|
||||
redraw = true;
|
||||
t_reloader = 0;
|
||||
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);
|
||||
|
||||
@@ -1017,7 +1044,7 @@ void App::ui_thread_main()
|
||||
update(t_frame);
|
||||
render_task([this, t_frame]
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
bind_default_render_target();
|
||||
clear();
|
||||
draw(t_frame);
|
||||
async_swap();
|
||||
@@ -1026,9 +1053,7 @@ void App::ui_thread_main()
|
||||
rendered_frames++;
|
||||
}
|
||||
}
|
||||
#if __ANDROID__
|
||||
android_detach_jni();
|
||||
#endif
|
||||
detach_ui_thread();
|
||||
}
|
||||
|
||||
void App::render_thread_start()
|
||||
|
||||
33
src/app.h
33
src/app.h
@@ -24,6 +24,12 @@
|
||||
#include "node_input_box.h"
|
||||
#include "node_panel_animation.h"
|
||||
#include "layout.h"
|
||||
#include "app_core/document_session.h"
|
||||
|
||||
namespace pp::platform {
|
||||
class PlatformServices;
|
||||
struct PlatformStoragePaths;
|
||||
}
|
||||
|
||||
#if defined(__OBJC__) && defined(__IOS__)
|
||||
#import <Foundation/Foundation.h>
|
||||
@@ -155,6 +161,7 @@ public:
|
||||
float display_density = 1.f;
|
||||
float zoom = 1.f;
|
||||
int idle_ms = 100;
|
||||
pp::platform::PlatformServices* platform_services_ = nullptr;
|
||||
|
||||
#if defined(__IOS__) && defined(__OBJC__)
|
||||
GameViewController* ios_view;
|
||||
@@ -183,6 +190,30 @@ public:
|
||||
void pick_dir(std::function<void(std::string path)> callback);
|
||||
void display_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 hideKeyboard();
|
||||
void initLog();
|
||||
@@ -248,6 +279,8 @@ public:
|
||||
void dialog_usermanual();
|
||||
void dialog_changelog();
|
||||
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_save();
|
||||
void dialog_save_ver();
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/document_cloud.h"
|
||||
#include "util.h"
|
||||
#include "node_progress_bar.h"
|
||||
#include "node_dialog_cloud.h"
|
||||
|
||||
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;
|
||||
if (Canvas::I->m_newdoc)
|
||||
{
|
||||
case pp::app::CloudUploadAction::show_save_required_warning:
|
||||
message_box("Warning", "This document needs to be saved before upload.");
|
||||
return;
|
||||
case pp::app::CloudUploadAction::prompt_publish:
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
auto upload_thread = [this] {
|
||||
BT_SetTerminate();
|
||||
|
||||
@@ -42,7 +52,6 @@ void App::cloud_upload()
|
||||
m->btn_cancel->on_click = [this, m, upload_thread](Node*) {
|
||||
m->destroy();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void App::cloud_upload_all()
|
||||
@@ -51,22 +60,23 @@ void App::cloud_upload_all()
|
||||
BT_SetTerminate();
|
||||
|
||||
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;
|
||||
std::shared_ptr<NodeProgressBar> pb;
|
||||
if (layout.m_loaded)
|
||||
pb = show_progress("Export Pano Image", names.size());
|
||||
if (plan.show_progress)
|
||||
pb = show_progress("Export Pano Image", plan.progress_total);
|
||||
|
||||
for (const auto& n : names)
|
||||
{
|
||||
std::string path = data_path + "/" + n;
|
||||
upload(path);
|
||||
|
||||
if (layout.m_loaded)
|
||||
if (plan.show_progress)
|
||||
pb->increment();
|
||||
}
|
||||
|
||||
if (layout.m_loaded)
|
||||
if (plan.show_progress)
|
||||
pb->destroy();
|
||||
|
||||
}).detach();
|
||||
@@ -74,8 +84,14 @@ void App::cloud_upload_all()
|
||||
|
||||
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;
|
||||
case pp::app::CloudBrowseAction::show_browser:
|
||||
break;
|
||||
}
|
||||
|
||||
// load thumbnail test
|
||||
auto dialog = std::make_shared<NodeDialogCloud>();
|
||||
@@ -88,7 +104,8 @@ void App::cloud_browse()
|
||||
|
||||
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;
|
||||
dialog->destroy();
|
||||
std::thread([this, dialog] {
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
#include "pch.h"
|
||||
#include "app.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)
|
||||
{
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glBlendEquation(GL_FUNC_ADD);
|
||||
glDisable(depth_test_state());
|
||||
glEnable(program_point_size_state());
|
||||
glBlendFunc(source_alpha_blend_factor(), one_minus_source_alpha_blend_factor());
|
||||
glBlendEquation(add_blend_equation());
|
||||
|
||||
Canvas* canvas = new Canvas;
|
||||
canvas->create(CANVAS_RES, CANVAS_RES);
|
||||
canvas->project_open_thread(pano_path);
|
||||
canvas->export_equirectangular_thread(out_path);
|
||||
Canvas* command_canvas = new Canvas;
|
||||
command_canvas->create(CANVAS_RES, CANVAS_RES);
|
||||
command_canvas->project_open_thread(pano_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 "app.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 "node_dialog_open.h"
|
||||
#include "node_dialog_browse.h"
|
||||
@@ -21,11 +25,33 @@
|
||||
#include "oculus_vr.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
|
||||
|
||||
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*/)
|
||||
{
|
||||
auto pb = std::make_shared<NodeProgressBar>();
|
||||
@@ -105,6 +131,41 @@ void App::dialog_about()
|
||||
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()
|
||||
{
|
||||
auto show_dialog = [this] {
|
||||
@@ -122,25 +183,32 @@ void App::dialog_newdoc()
|
||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||
{
|
||||
std::string name = dialog->input->m_text;
|
||||
std::string path = work_path + "/" + name + ".ppi";
|
||||
|
||||
if (name.empty())
|
||||
const auto plan = pp::app::plan_new_document(
|
||||
work_path,
|
||||
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;
|
||||
}
|
||||
|
||||
auto action = [this, dialog, name, path] {
|
||||
std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
|
||||
int res = resolutions[dialog->m_resolution->m_current_index];
|
||||
doc_name = name;
|
||||
doc_path = path;
|
||||
doc_filename = name + ".ppi";
|
||||
doc_dir = work_path;
|
||||
auto action = [this, dialog, plan = plan.value()] {
|
||||
doc_name = plan.target.name;
|
||||
doc_path = plan.target.path;
|
||||
doc_filename = plan.target.name + ".ppi";
|
||||
doc_dir = plan.target.directory;
|
||||
|
||||
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();
|
||||
ActionManager::clear();
|
||||
|
||||
@@ -154,7 +222,7 @@ void App::dialog_newdoc()
|
||||
App::I->hideKeyboard();
|
||||
};
|
||||
|
||||
if (Asset::exist(path))
|
||||
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
|
||||
{
|
||||
// ask confirm is file already exist
|
||||
auto msgbox = new NodeMessageBox();
|
||||
@@ -181,34 +249,7 @@ void App::dialog_newdoc()
|
||||
};
|
||||
};
|
||||
|
||||
if (canvas)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
continue_document_workflow_after_optional_save(show_dialog);
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
@@ -242,34 +283,7 @@ void App::dialog_open()
|
||||
};
|
||||
};
|
||||
|
||||
if (canvas)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
continue_document_workflow_after_optional_save(show_dialog);
|
||||
}
|
||||
|
||||
void App::dialog_browse()
|
||||
@@ -299,34 +313,7 @@ void App::dialog_browse()
|
||||
};
|
||||
};
|
||||
|
||||
if (canvas)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
continue_document_workflow_after_optional_save(show_dialog);
|
||||
}
|
||||
|
||||
void App::dialog_save_ver()
|
||||
@@ -337,35 +324,45 @@ void App::dialog_save_ver()
|
||||
return;
|
||||
}
|
||||
|
||||
int current = 0;
|
||||
std::string next = doc_name + ".01";
|
||||
std::string base = doc_name;
|
||||
|
||||
std::regex r(R"((.*)\.(\w{2})$)");
|
||||
std::smatch m;
|
||||
if (std::regex_search(doc_name, m, r))
|
||||
{
|
||||
base = m[1].str();
|
||||
current = atoi(m[2].str().c_str());
|
||||
const auto target = pp::app::find_next_document_version_target(
|
||||
doc_dir,
|
||||
doc_name,
|
||||
[](const std::string& path) {
|
||||
return Asset::exist(path);
|
||||
});
|
||||
if (!target) {
|
||||
message_box("Saving Error", target.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = current + 1; i < 99; i++)
|
||||
{
|
||||
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";
|
||||
doc_name = target.value().name;
|
||||
doc_path = target.value().path;
|
||||
canvas->m_canvas->m_unsaved = true;
|
||||
title_update();
|
||||
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()
|
||||
{
|
||||
if (!check_license())
|
||||
@@ -388,32 +385,36 @@ void App::dialog_save()
|
||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||
{
|
||||
std::string name = dialog->input->m_text;
|
||||
std::string path = work_path + "/" + name + ".ppi";
|
||||
|
||||
if (name.empty())
|
||||
const auto plan = pp::app::plan_document_file_save(
|
||||
work_path,
|
||||
name,
|
||||
[](const std::string& path) {
|
||||
return Asset::exist(path);
|
||||
});
|
||||
if (!plan)
|
||||
{
|
||||
message_box("Warning", "You need to specify a name to file.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto action = [this, dialog, name, path] {
|
||||
canvas->m_canvas->project_save(path);
|
||||
doc_name = name;
|
||||
doc_path = path;
|
||||
doc_dir = work_path;
|
||||
auto action = [this, dialog, plan = plan.value()] {
|
||||
canvas->m_canvas->project_save(plan.target.path);
|
||||
doc_name = plan.target.name;
|
||||
doc_path = plan.target.path;
|
||||
doc_dir = plan.target.directory;
|
||||
title_update();
|
||||
dialog->destroy();
|
||||
App::I->hideKeyboard();
|
||||
};
|
||||
|
||||
if (Asset::exist(path))
|
||||
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
|
||||
{
|
||||
// ask confirm is file already exist
|
||||
auto msgbox = new NodeMessageBox();
|
||||
msgbox->set_manager(&layout);
|
||||
msgbox->init();
|
||||
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*) {
|
||||
action();
|
||||
msgbox->destroy();
|
||||
@@ -437,120 +438,145 @@ void App::dialog_save()
|
||||
|
||||
void App::dialog_export(std::string ext)
|
||||
{
|
||||
if (!check_license())
|
||||
{
|
||||
message_box("License", "This function is disabled in demo mode.");
|
||||
if (!can_start_document_export(*this, true))
|
||||
return;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (canvas)
|
||||
{
|
||||
// TODO: use picker
|
||||
auto path = work_path + "/" + doc_name + ext;
|
||||
auto name = doc_name + ext;
|
||||
canvas->m_canvas->export_equirectangular(path, [this, path, name]{
|
||||
canvas->m_canvas->export_equirectangular(target.value().path, [this, target = target.value()]{
|
||||
#if defined(__IOS__)
|
||||
message_box("Export Equirectangular", "Image exported to Photos");
|
||||
message_box("Export Equirectangular", "Image exported to Photos");
|
||||
#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)
|
||||
message_box("Export Equirectangular", "Image exported to " + work_path);
|
||||
message_box("Export Equirectangular", "Image exported to " + work_path);
|
||||
#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__
|
||||
ui_task([=]{
|
||||
webgl_pick_file_save(path, name, [](bool success){ });
|
||||
});
|
||||
#endif
|
||||
ui_task([=]{
|
||||
save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { });
|
||||
});
|
||||
}
|
||||
#endif
|
||||
});
|
||||
}
|
||||
|
||||
void App::dialog_export_layers()
|
||||
{
|
||||
if (!check_license())
|
||||
{
|
||||
message_box("License", "This function is disabled in demo mode.");
|
||||
if (!can_start_document_export(*this, true))
|
||||
return;
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
if (canvas)
|
||||
if (Asset::create_dir(target.value().directory))
|
||||
{
|
||||
#if defined(__IOS__)
|
||||
auto dir = work_path + "/" + doc_name + "_layers";
|
||||
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);
|
||||
});
|
||||
canvas->m_canvas->export_layers(target.value().stem_path, [this] {
|
||||
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
|
||||
});
|
||||
#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()
|
||||
{
|
||||
if (!check_license())
|
||||
{
|
||||
message_box("License", "This function is disabled in demo mode.");
|
||||
if (!can_start_document_export(*this, true))
|
||||
return;
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
if (canvas)
|
||||
if (Asset::create_dir(target.value().directory))
|
||||
{
|
||||
#if defined(__IOS__)
|
||||
auto dir = work_path + "/" + doc_name + "_frames";
|
||||
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);
|
||||
});
|
||||
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] {
|
||||
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
|
||||
});
|
||||
#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()
|
||||
{
|
||||
if (!check_license())
|
||||
{
|
||||
message_box("License", "This function is disabled in demo mode.");
|
||||
if (!can_start_document_export(*this, true))
|
||||
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__)
|
||||
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__)
|
||||
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)
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
@@ -561,29 +587,35 @@ void App::dialog_resize()
|
||||
|
||||
dialog->btn_ok->on_click = [this,dialog](Node*)
|
||||
{
|
||||
int res = dialog->get_resolution();
|
||||
if (canvas)
|
||||
canvas->m_canvas->resize(res, res);
|
||||
App::I->title_update();
|
||||
ActionManager::clear();
|
||||
const auto plan = pp::app::plan_document_resize(
|
||||
dialog->combo ? dialog->combo->m_current_index : 0);
|
||||
if (!plan)
|
||||
{
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
||||
void App::dialog_export_cube_faces()
|
||||
{
|
||||
if (canvas)
|
||||
{
|
||||
canvas->m_canvas->export_cube_faces(doc_name, [this] {
|
||||
if (!can_start_document_export(*this, false))
|
||||
return;
|
||||
|
||||
canvas->m_canvas->export_cube_faces(doc_name, [this] {
|
||||
#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__)
|
||||
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)
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void App::dialog_layer_rename()
|
||||
@@ -601,6 +633,17 @@ void App::dialog_layer_rename()
|
||||
|
||||
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
|
||||
{
|
||||
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 = 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));
|
||||
layer_node->set_name(dialog->get_name().c_str());
|
||||
layer->m_name = dialog->get_name();
|
||||
ActionManager::add(new ActionLayerRename(
|
||||
plan.value().old_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();
|
||||
App::I->hideKeyboard();
|
||||
};
|
||||
@@ -681,8 +728,17 @@ void App::dialog_ppbr_export()
|
||||
|
||||
void App::dialog_timelapse_export()
|
||||
{
|
||||
if (!can_start_document_export(*this, false))
|
||||
return;
|
||||
|
||||
#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) {
|
||||
rec_export(path);
|
||||
},
|
||||
@@ -703,8 +759,17 @@ void App::dialog_timelapse_export()
|
||||
|
||||
void App::dialog_export_mp4()
|
||||
{
|
||||
if (!can_start_document_export(*this, false))
|
||||
return;
|
||||
|
||||
#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) {
|
||||
export_anim_mp4(path);
|
||||
},
|
||||
|
||||
@@ -1,76 +1,90 @@
|
||||
#include "pch.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__
|
||||
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
|
||||
namespace {
|
||||
|
||||
[[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()
|
||||
{
|
||||
#if _WIN32
|
||||
return win32_clipboard_get_text();
|
||||
#elif __IOS__
|
||||
return [ios_view clipboard_get_string];
|
||||
#elif __OSX__
|
||||
return [osx_view clipboard_get_string];
|
||||
#elif __ANDROID__
|
||||
return android_get_clipboard();
|
||||
#endif
|
||||
if (pp::app::plan_clipboard_read() != pp::app::ClipboardReadAction::read_text)
|
||||
return {};
|
||||
|
||||
return active_platform_services().clipboard_text();
|
||||
}
|
||||
|
||||
bool App::clipboard_set_text(const std::string& s)
|
||||
{
|
||||
#if _WIN32
|
||||
return win32_clipboard_set_text(s);
|
||||
#elif __IOS__
|
||||
return [ios_view clipboard_set_string:s];
|
||||
#elif __OSX__
|
||||
return [osx_view clipboard_set_string:s];
|
||||
#elif __ANDROID__
|
||||
return android_set_clipboard(s);
|
||||
#endif
|
||||
if (pp::app::plan_clipboard_write(s) != pp::app::ClipboardWriteAction::write_text)
|
||||
return false;
|
||||
|
||||
return active_platform_services().set_clipboard_text(s);
|
||||
}
|
||||
|
||||
void App::stacktrace()
|
||||
{
|
||||
#if __OSX__
|
||||
NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
|
||||
LOG("callstack:\n%s", [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
#endif
|
||||
active_platform_services().log_stacktrace();
|
||||
}
|
||||
|
||||
void App::crash_test()
|
||||
{
|
||||
#ifdef __IOS__
|
||||
[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
|
||||
active_platform_services().trigger_crash_test();
|
||||
}
|
||||
|
||||
void App::tick(float dt)
|
||||
@@ -84,7 +98,7 @@ void App::tick(float dt)
|
||||
void App::resize(float w, float 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;
|
||||
width = w;
|
||||
height = h;
|
||||
@@ -92,123 +106,50 @@ void App::resize(float w, float h)
|
||||
|
||||
void App::show_cursor()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
win32_show_cursor(true);
|
||||
#elif __OSX__
|
||||
[osx_view show_cursor:true];
|
||||
#endif
|
||||
if (!should_dispatch_cursor_visibility(true))
|
||||
return;
|
||||
|
||||
active_platform_services().set_cursor_visible(true);
|
||||
}
|
||||
|
||||
void App::hide_cursor()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
win32_show_cursor(false);
|
||||
#elif __OSX__
|
||||
[osx_view show_cursor:false];
|
||||
#endif
|
||||
if (!should_dispatch_cursor_visibility(false))
|
||||
return;
|
||||
|
||||
active_platform_services().set_cursor_visible(false);
|
||||
}
|
||||
|
||||
void App::showKeyboard()
|
||||
{
|
||||
LOG("show keyboard");
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view show_keyboard];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
displayKeyboard(true);
|
||||
#endif
|
||||
if (!should_dispatch_keyboard_visibility(true))
|
||||
return;
|
||||
|
||||
active_platform_services().set_virtual_keyboard_visible(true);
|
||||
}
|
||||
|
||||
void App::hideKeyboard()
|
||||
{
|
||||
LOG("hide keyboard");
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view hide_keyboard];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
displayKeyboard(false);
|
||||
#endif
|
||||
if (!should_dispatch_keyboard_visibility(false))
|
||||
return;
|
||||
|
||||
active_platform_services().set_virtual_keyboard_visible(false);
|
||||
}
|
||||
|
||||
void App::pick_image(std::function<void(std::string path)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
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
|
||||
active_platform_services().pick_image(std::move(callback));
|
||||
}
|
||||
|
||||
void App::pick_file(std::vector<std::string> types, std::function<void (std::string)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
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
|
||||
active_platform_services().pick_file(std::move(types), std::move(callback));
|
||||
}
|
||||
|
||||
#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::thread([=]{
|
||||
writer(path);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view pick_file_save:path];
|
||||
});
|
||||
callback(path, true);
|
||||
save_prepared_file(path, default_name + ext, callback);
|
||||
}).detach();
|
||||
}
|
||||
#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;
|
||||
LOG("App::pick_file_save %s", path.c_str());
|
||||
writer(path);
|
||||
webgl_pick_file_save(path, default_name + "." + type, callback);
|
||||
save_prepared_file(path, default_name + "." + type, std::move(callback));
|
||||
}
|
||||
#else
|
||||
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#if __OSX__
|
||||
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
|
||||
active_platform_services().pick_save_file(std::move(types), std::move(callback));
|
||||
}
|
||||
#endif
|
||||
|
||||
void App::pick_dir(std::function<void(std::string path)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
// 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
|
||||
active_platform_services().pick_directory(std::move(callback));
|
||||
}
|
||||
|
||||
void App::display_file(std::string path)
|
||||
{
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view 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
|
||||
if (pp::app::plan_display_file(path) == pp::app::DisplayFileAction::ignore_empty_path)
|
||||
return;
|
||||
|
||||
active_platform_services().display_file(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.");
|
||||
return;
|
||||
}
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view share_file:[NSString stringWithUTF8String:path.c_str()]];
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[osx_view share_file:[NSString stringWithUTF8String:path.c_str()]];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
#elif _WIN32
|
||||
// not implemented
|
||||
#endif
|
||||
active_platform_services().share_file(path);
|
||||
}
|
||||
|
||||
void App::request_app_close()
|
||||
{
|
||||
active_platform_services().request_app_close();
|
||||
}
|
||||
|
||||
void App::attach_ui_thread()
|
||||
{
|
||||
active_platform_services().attach_ui_thread();
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 "app.h"
|
||||
#include "renderer_api/shader_catalog.h"
|
||||
#include "renderer_gl/opengl_capabilities.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()
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
if (!check_uniform_uniqueness())
|
||||
std::logic_error("check_uniform_uniqueness() failed");
|
||||
LOG("check_uniform_uniqueness() failed");
|
||||
#endif // _DEBUG
|
||||
|
||||
render_task([] {
|
||||
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++)
|
||||
{
|
||||
std::string ext = (const char*)glGetStringi(GL_EXTENSIONS, i);
|
||||
if (ext.find("shader_framebuffer_fetch") != std::string::npos)
|
||||
ShaderManager::ext_framebuffer_fetch = true;
|
||||
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());
|
||||
extension_storage.emplace_back((const char*)glGetStringi(extension_string_name(), i));
|
||||
extension_views.push_back(extension_storage.back());
|
||||
LOG("EXT: %s", extension_storage.back().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__
|
||||
@@ -41,60 +75,25 @@ void App::initShaders()
|
||||
ShaderManager::ext_float32 = true;
|
||||
ShaderManager::ext_float16 = true;
|
||||
#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("initializing shaders");
|
||||
if (!ShaderManager::load(kShader::Texture, "data/shaders/texture.glsl"))
|
||||
LOG("Failed to create shader Texture");
|
||||
if (!ShaderManager::load(kShader::TextureAlpha, "data/shaders/texture-alpha.glsl"))
|
||||
LOG("Failed to create shader TextureAlpha");
|
||||
if (!ShaderManager::load(kShader::TextureMask, "data/shaders/texture-mask.glsl"))
|
||||
LOG("Failed to create shader TextureMask");
|
||||
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"))
|
||||
LOG("Failed to create shader TextureBlend");
|
||||
if (!ShaderManager::load(kShader::StrokePreview, "data/shaders/stroke-preview.glsl"))
|
||||
LOG("Failed to create shader StrokePreview");
|
||||
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");
|
||||
const auto shader_catalog = pp::renderer::panopainter_shader_catalog();
|
||||
const auto catalog_status = pp::renderer::validate_shader_catalog(shader_catalog);
|
||||
if (!catalog_status.ok())
|
||||
{
|
||||
LOG("Shader catalog validation failed: %s", catalog_status.message);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& shader : shader_catalog)
|
||||
{
|
||||
if (!ShaderManager::load(static_cast<kShader>(const_hash(shader.name)), shader.path))
|
||||
LOG("Failed to create shader %s", shader.name);
|
||||
}
|
||||
LOG("shaders initialized");
|
||||
}
|
||||
|
||||
|
||||
160
src/app_vr.cpp
160
src/app_vr.cpp
@@ -1,13 +1,98 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "app.h"
|
||||
#include "util.h"
|
||||
#include "shape.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
bool win32_vr_start();
|
||||
void win32_vr_stop();
|
||||
#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;
|
||||
cbuffer<glm::vec3> controller_points(10);
|
||||
glm::vec3 controller_last_point;
|
||||
@@ -37,13 +122,16 @@ void App::vr_draw_ui()
|
||||
{
|
||||
uirtt.bindFramebuffer();
|
||||
uirtt.clear();
|
||||
glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight());
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect {
|
||||
.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);
|
||||
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
|
||||
layout[main_id]->m_children[i]->watch(observer);
|
||||
//msgbox->watch(observer);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
apply_vr_ui_scissor_test(false);
|
||||
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));
|
||||
vr_rot = glm::lookAt({ 0, 0, 0 }, origin, { 0, 1, 0 });
|
||||
|
||||
auto blend = glIsEnabled(GL_BLEND);
|
||||
auto depth = glIsEnabled(GL_DEPTH_TEST);
|
||||
auto blend = glIsEnabled(pp::renderer::gl::blend_state());
|
||||
auto depth = glIsEnabled(pp::renderer::gl::depth_test_state());
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), false);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
|
||||
clear_vr_depth_buffer();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), true);
|
||||
clear_vr_depth_buffer();
|
||||
|
||||
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::Mask, canvas->m_canvas->m_smask_active);
|
||||
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();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
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();
|
||||
m_face_plane.draw_fill();
|
||||
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();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
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)
|
||||
@@ -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_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();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
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();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
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->bind() :
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
unbind_texture_2d();
|
||||
m_face_plane.draw_fill();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
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();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
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();
|
||||
}
|
||||
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_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();
|
||||
m_face_plane.draw_fill();
|
||||
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();
|
||||
}
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
|
||||
// draw the brush
|
||||
/*
|
||||
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::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
|
||||
);
|
||||
glEnable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||
set_active_texture_unit(0);
|
||||
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
|
||||
tex.bind();
|
||||
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::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
uirtt.bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
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::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
|
||||
);
|
||||
glEnable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||
set_active_texture_unit(0);
|
||||
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
|
||||
tex.bind();
|
||||
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)
|
||||
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)
|
||||
{
|
||||
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);
|
||||
*/
|
||||
|
||||
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
||||
depth ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), blend != 0U);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), depth != 0U);
|
||||
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);
|
||||
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}
|
||||
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;
|
||||
void stroke_draw();
|
||||
void stroke_end();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "canvas.h"
|
||||
#include "canvas_actions.h"
|
||||
#include "node_panel_layer.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
void ActionStroke::undo()
|
||||
{
|
||||
@@ -36,8 +37,21 @@ void ActionStroke::undo()
|
||||
{
|
||||
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();
|
||||
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();
|
||||
});
|
||||
}
|
||||
@@ -76,11 +90,22 @@ Action* ActionStroke::get_redo()
|
||||
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)
|
||||
{
|
||||
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([&]
|
||||
{
|
||||
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();
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "canvas_layer.h"
|
||||
#include "app.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "rtt.h"
|
||||
|
||||
uint32_t Layer::s_count = 0;
|
||||
@@ -44,7 +45,7 @@ TextureCube Layer::gen_cube()
|
||||
{
|
||||
ret.bind();
|
||||
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();
|
||||
});
|
||||
}
|
||||
@@ -70,7 +71,7 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
|
||||
latlong.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();
|
||||
|
||||
@@ -78,8 +79,8 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
|
||||
|
||||
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||
glBindTexture(pp::renderer::gl::texture_cube_map_target(), cube.m_cubetex_id);
|
||||
|
||||
ShaderManager::use(kShader::Equirect);
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
@@ -115,13 +116,13 @@ PBO Layer::gen_equirect_pbo(glm::ivec2 size /*= { 0, 0 }*/)
|
||||
|
||||
App::I->render_task([&]
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
|
||||
latlong.bindFramebuffer();
|
||||
|
||||
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||
glBindTexture(pp::renderer::gl::texture_cube_map_target(), cube.m_cubetex_id);
|
||||
|
||||
ShaderManager::use(kShader::Equirect);
|
||||
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
|
||||
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);
|
||||
|
||||
bool erase = (c.a == 0.f);
|
||||
@@ -466,7 +467,7 @@ void LayerFrame::clear(const glm::vec4& c)
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
m_rtt[i].bindFramebuffer();
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
|
||||
m_rtt[i].unbindFramebuffer();
|
||||
|
||||
if (erase)
|
||||
@@ -530,9 +531,11 @@ void LayerFrame::restore(const Snapshot& snap)
|
||||
|
||||
m_rtt[i].bindTexture();
|
||||
glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0,
|
||||
m_dirty_box[i].x, m_dirty_box[i].y,
|
||||
box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
glTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0,
|
||||
static_cast<int>(m_dirty_box[i].x), static_cast<int>(m_dirty_box[i].y),
|
||||
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].unbindTexture();
|
||||
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();
|
||||
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,
|
||||
box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, snap.image[i].get());
|
||||
glReadPixels(static_cast<int>(snap.m_dirty_box[i].x), static_cast<int>(snap.m_dirty_box[i].y),
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "log.h"
|
||||
#include "canvas_modes.h"
|
||||
#include "layout.h"
|
||||
@@ -7,9 +10,19 @@
|
||||
#include "node_canvas.h"
|
||||
#include "app.h"
|
||||
#include "util.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
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)
|
||||
{
|
||||
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,
|
||||
((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;
|
||||
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);
|
||||
@@ -303,15 +319,15 @@ void CanvasModePen::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const
|
||||
glm::eulerAngleZ(tip_angle) *
|
||||
glm::scale(glm::vec3(tip_scale, 1))
|
||||
);
|
||||
bool blend = glIsEnabled(GL_BLEND);
|
||||
glEnable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
bool blend = glIsEnabled(pp::renderer::gl::blend_state());
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
set_active_texture_unit(0);
|
||||
auto& tex = *brush->m_tip_texture;
|
||||
tex.bind();
|
||||
Canvas::I->m_sampler_brush.bind(0);
|
||||
Canvas::I->m_plane.draw_fill();
|
||||
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::scale(glm::vec3(tip_scale, 1))
|
||||
);
|
||||
bool blend = glIsEnabled(GL_BLEND);
|
||||
glEnable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
bool blend = glIsEnabled(pp::renderer::gl::blend_state());
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
set_active_texture_unit(0);
|
||||
auto& tex = *brush->m_tip_texture;
|
||||
tex.bind();
|
||||
Canvas::I->m_sampler_brush.bind(0);
|
||||
Canvas::I->m_plane.draw_fill();
|
||||
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_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) {
|
||||
//glEnable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
// blending state intentionally left unchanged here.
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
ShaderManager::use(kShader::Color);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera);
|
||||
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)
|
||||
{
|
||||
bool depth = glIsEnabled(GL_DEPTH_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
if (m_points.size() > 3)
|
||||
{
|
||||
if (m_dragging)
|
||||
@@ -803,7 +819,7 @@ void CanvasModeMaskFree::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
|
||||
// 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())
|
||||
{
|
||||
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::u_mat4(kShaderUniform::MVP, proj * camera);
|
||||
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();
|
||||
m_tex[plane].create(bb_sz.x, bb_sz.y);
|
||||
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();
|
||||
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([&]
|
||||
{
|
||||
glViewport(0, 0, layer->w, layer->h);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
set_active_texture_unit(0);
|
||||
ShaderManager::use(kShader::Color);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 0 });
|
||||
layer->rtt(i).bindFramebuffer();
|
||||
// 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++)
|
||||
m_shape[j].draw_fill();
|
||||
layer->rtt(i).unbindFramebuffer();
|
||||
@@ -1407,19 +1432,28 @@ void CanvasModeTransform::leave(kCanvasMode next)
|
||||
{
|
||||
layer->rtt(i).bindFramebuffer();
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
set_active_texture_unit(0);
|
||||
glViewport(0, 0, layer->rtt(i).getWidth(), layer->rtt(i).getHeight());
|
||||
|
||||
// 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
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
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
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
for (int j = 0; j < 6; j++)
|
||||
{
|
||||
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)
|
||||
{
|
||||
bool depth = glIsEnabled(GL_DEPTH_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
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::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera * m_xform * m_xform_local);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex[i].bind();
|
||||
Canvas::I->m_sampler_linear.bind(0);
|
||||
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();
|
||||
}
|
||||
|
||||
if (depth) glEnable(GL_DEPTH_TEST);
|
||||
if (depth) glEnable(pp::renderer::gl::depth_test_state());
|
||||
}
|
||||
|
||||
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 "util.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;
|
||||
Sampler FontManager::m_sampler;
|
||||
@@ -52,7 +198,7 @@ bool Font::load(const std::string& ttf, int font_size, float font_scale)
|
||||
// offset = 0;
|
||||
stbtt_BakeFontBitmap(file.m_data, 0, (float)font_size*scale, bitmap.get(), w, h, start_char, num_chars, chars.data());
|
||||
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();
|
||||
size = font_size;
|
||||
return true;
|
||||
@@ -155,18 +301,22 @@ bool TextMesh::create()
|
||||
{
|
||||
App::I->render_task([this]
|
||||
{
|
||||
glGenBuffers(2, font_buffers);
|
||||
#if USE_VBO
|
||||
glGenVertexArrays(1, &font_array);
|
||||
glBindVertexArray(font_array);
|
||||
glEnableVertexAttribArray(0);
|
||||
glEnableVertexAttribArray(1);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)0);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)(sizeof(float) * 2));
|
||||
glBindVertexArray(0);
|
||||
#endif // USE_VBO
|
||||
const auto mesh = pp::renderer::gl::create_opengl_mesh_objects(
|
||||
pp::renderer::gl::OpenGlMeshUpload {
|
||||
.vertex_data = nullptr,
|
||||
.vertex_byte_count = 0,
|
||||
.index_data = nullptr,
|
||||
.index_byte_count = 0,
|
||||
.indexed = true,
|
||||
.vertex_array_count = 1U,
|
||||
.attributes = text_mesh_vertex_attributes(),
|
||||
},
|
||||
text_mesh_create_dispatch());
|
||||
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;
|
||||
}
|
||||
@@ -254,12 +404,24 @@ void TextMesh::update(const std::string& text, const std::string& font, int size
|
||||
font_array_count = (int)idx.size();
|
||||
App::I->render_task([&]
|
||||
{
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size() * sizeof(GLushort), idx.data(), GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
|
||||
glBufferData(GL_ARRAY_BUFFER, v.size() * sizeof(glm::vec4), v.data(), GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
(void)pp::renderer::gl::upload_opengl_buffer_data(
|
||||
pp::renderer::gl::OpenGlBufferUpload {
|
||||
.target = pp::renderer::gl::element_array_buffer_target(),
|
||||
.buffer_id = font_buffers[1],
|
||||
.data = idx.data(),
|
||||
.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);
|
||||
if (f.font_tex.ready())
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glActiveTexture(texture_unit(0U));
|
||||
f.font_tex.bind();
|
||||
FontManager::m_sampler.bind(0);
|
||||
|
||||
#if USE_VBO
|
||||
glBindVertexArray(font_array);
|
||||
glDrawElements(GL_TRIANGLES, font_array_count, GL_UNSIGNED_SHORT, 0);
|
||||
glBindVertexArray(0);
|
||||
#else
|
||||
glEnableVertexAttribArray(0);
|
||||
glEnableVertexAttribArray(1);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)0);
|
||||
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
|
||||
(void)pp::renderer::gl::draw_opengl_mesh(
|
||||
pp::renderer::gl::OpenGlMeshDraw {
|
||||
.vertex_array = font_array,
|
||||
.mode = pp::renderer::gl::primitive_mode_for_fill_count(3U),
|
||||
.count = font_array_count,
|
||||
.indexed = true,
|
||||
.index_type = pp::renderer::gl::index_type_for_index_size(sizeof(GLushort)),
|
||||
.index_offset = nullptr,
|
||||
},
|
||||
text_mesh_draw_dispatch());
|
||||
|
||||
f.font_tex.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