Compare commits
245 Commits
ad255a6ddf
...
cee5f141a3
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
353
CMakeLists.txt
353
CMakeLists.txt
@@ -20,6 +20,7 @@ endif()
|
||||
include(PanoPainterWarnings)
|
||||
include(PanoPainterSources)
|
||||
include(PanoPainterVersion)
|
||||
include(PanoPainterRuntime)
|
||||
|
||||
if(PP_ENABLE_CLANG_TIDY)
|
||||
find_program(PP_CLANG_TIDY_EXE NAMES clang-tidy)
|
||||
@@ -136,6 +137,7 @@ target_link_libraries(pp_paint
|
||||
|
||||
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
|
||||
@@ -150,7 +152,9 @@ target_link_libraries(pp_document
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_renderer_api STATIC
|
||||
src/renderer_api/renderer_api.cpp)
|
||||
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")
|
||||
@@ -161,6 +165,22 @@ target_link_libraries(pp_renderer_api
|
||||
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
|
||||
@@ -190,6 +210,52 @@ target_link_libraries(pp_ui_core
|
||||
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()
|
||||
@@ -201,15 +267,207 @@ endif()
|
||||
|
||||
if(PP_BUILD_APP)
|
||||
if(WIN32)
|
||||
add_library(pp_legacy_app STATIC
|
||||
${PP_LEGACY_APP_SOURCES}
|
||||
add_library(pp_legacy_vendor OBJECT
|
||||
${PP_VENDOR_SOURCES})
|
||||
|
||||
target_link_libraries(pp_legacy_app
|
||||
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_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_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
|
||||
@@ -230,19 +488,40 @@ if(PP_BUILD_APP)
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
target_precompile_headers(pp_legacy_app PRIVATE src/pch.h)
|
||||
set_source_files_properties(${PP_VENDOR_SOURCES}
|
||||
PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
|
||||
set_source_files_properties(src/version.cpp
|
||||
PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
|
||||
|
||||
add_executable(PanoPainter WIN32
|
||||
${PP_WINDOWS_APP_SOURCES})
|
||||
|
||||
target_link_libraries(PanoPainter
|
||||
PRIVATE
|
||||
pp_project_options
|
||||
pp_project_warnings
|
||||
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>"
|
||||
@@ -257,34 +536,28 @@ if(PP_BUILD_APP)
|
||||
shell32
|
||||
shlwapi
|
||||
user32
|
||||
wbemuuid)
|
||||
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)
|
||||
|
||||
target_precompile_headers(PanoPainter REUSE_FROM pp_legacy_app)
|
||||
set_target_properties(PanoPainter PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
pp_add_version_generation(PanoPainter "$<IF:$<CONFIG:Debug>,debug,release>")
|
||||
|
||||
add_custom_command(TARGET PanoPainter POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||
"$<TARGET_FILE_DIR:PanoPainter>/data"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.dll"
|
||||
"$<TARGET_FILE_DIR:PanoPainter>/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:PanoPainter>/"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/libyuv.dll"
|
||||
"$<TARGET_FILE_DIR:PanoPainter>/libyuv.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.dll"
|
||||
"$<TARGET_FILE_DIR:PanoPainter>/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:PanoPainter>/openh264-2.0.0-win64.dll"
|
||||
VERBATIM)
|
||||
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()
|
||||
|
||||
@@ -200,6 +200,22 @@
|
||||
{
|
||||
"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": [
|
||||
@@ -250,6 +266,30 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -1,6 +1,67 @@
|
||||
set(PP_LEGACY_APP_SOURCES
|
||||
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
|
||||
@@ -9,32 +70,17 @@ set(PP_LEGACY_APP_SOURCES
|
||||
src/app_layout.cpp
|
||||
src/app_shaders.cpp
|
||||
src/app_vr.cpp
|
||||
src/asset.cpp
|
||||
src/bezier.cpp
|
||||
src/binary_stream.cpp
|
||||
src/brush.cpp
|
||||
src/canvas.cpp
|
||||
src/canvas_actions.cpp
|
||||
src/canvas_layer.cpp
|
||||
src/canvas_modes.cpp
|
||||
src/event.cpp
|
||||
src/font.cpp
|
||||
src/hmd.cpp
|
||||
src/image.cpp
|
||||
src/layout.cpp
|
||||
src/log.cpp
|
||||
src/mp4enc.cpp
|
||||
src/node.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_border.cpp
|
||||
src/node_button.cpp
|
||||
src/node_button_custom.cpp
|
||||
src/node_canvas.cpp
|
||||
src/node_changelog.cpp
|
||||
src/node_checkbox.cpp
|
||||
src/node_color_quad.cpp
|
||||
src/node_colorwheel.cpp
|
||||
src/node_combobox.cpp
|
||||
src/node_dialog_browse.cpp
|
||||
src/node_dialog_cloud.cpp
|
||||
src/node_dialog_export_ppbr.cpp
|
||||
@@ -42,10 +88,6 @@ set(PP_LEGACY_APP_SOURCES
|
||||
src/node_dialog_open.cpp
|
||||
src/node_dialog_picker.cpp
|
||||
src/node_dialog_resize.cpp
|
||||
src/node_icon.cpp
|
||||
src/node_image.cpp
|
||||
src/node_image_texture.cpp
|
||||
src/node_input_box.cpp
|
||||
src/node_message_box.cpp
|
||||
src/node_metadata.cpp
|
||||
src/node_panel_animation.cpp
|
||||
@@ -56,33 +98,19 @@ set(PP_LEGACY_APP_SOURCES
|
||||
src/node_panel_layer.cpp
|
||||
src/node_panel_quick.cpp
|
||||
src/node_panel_stroke.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_stroke_preview.cpp
|
||||
src/node_text.cpp
|
||||
src/node_text_input.cpp
|
||||
src/node_tool_bucket.cpp
|
||||
src/node_usermanual.cpp
|
||||
src/node_viewport.cpp
|
||||
src/pch.cpp
|
||||
src/rtt.cpp
|
||||
src/serializer.cpp
|
||||
src/settings.cpp
|
||||
src/shader.cpp
|
||||
src/shape.cpp
|
||||
src/texture.cpp
|
||||
src/util.cpp
|
||||
src/version.cpp
|
||||
src/wacom.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
|
||||
src/main.cpp
|
||||
PanoPainter.rc
|
||||
)
|
||||
|
||||
@@ -140,4 +168,3 @@ set(PP_LEGACY_INCLUDE_DIRS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/wacom"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/yoga"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ function(pp_configure_project_warnings target)
|
||||
/W4
|
||||
/permissive-
|
||||
/Zc:__cplusplus
|
||||
/Zc:preprocessor)
|
||||
/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()
|
||||
@@ -15,7 +17,9 @@ function(pp_configure_project_warnings target)
|
||||
-Wpedantic
|
||||
-Wconversion
|
||||
-Wshadow
|
||||
-Wnull-dereference)
|
||||
-Wnull-dereference
|
||||
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
|
||||
-Wno-unused-parameter)
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_ASAN)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Build And Platform Inventory
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-01
|
||||
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.
|
||||
@@ -10,7 +10,7 @@ Keep it updated as platform paths move to shared CMake targets.
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -61,6 +61,8 @@ These commands are the current local baseline.
|
||||
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
|
||||
@@ -88,40 +90,452 @@ Known local toolchain state:
|
||||
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_paint_renderer`,
|
||||
`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 event/logging/task queue coverage, PNG metadata and
|
||||
decode, PPI header/layout, settings document, document
|
||||
snapshot/per-layer-frame/move/duration/face-pixel coverage, paint
|
||||
brush/stroke/stroke-script coverage, renderer shader descriptor coverage,
|
||||
UI color parsing, and layout XML parse coverage.
|
||||
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_assets_image_pixels_tests` decodes PNG payloads to RGBA8 and rejects
|
||||
corrupt image payloads.
|
||||
- `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.
|
||||
@@ -129,6 +543,40 @@ Known local toolchain state:
|
||||
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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# PanoPainter Capability Map
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-05-31
|
||||
Last updated: 2026-06-02
|
||||
|
||||
This map is the preservation checklist for the modernization. When a component
|
||||
is extracted, update the relevant rows with the owning component, test label,
|
||||
@@ -12,17 +12,19 @@ and validation command.
|
||||
| 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_panopainter_ui`, `pp_platform_*` | UI automation and platform smoke |
|
||||
| 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` | `pp_assets`, `pp_paint_renderer` | Golden output tolerance |
|
||||
| Equirectangular import/export | `Canvas`, shaders, RTT | `pp_paint_renderer` | Tiny cube/equirect golden |
|
||||
| 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 |
|
||||
|
||||
@@ -34,8 +36,8 @@ and validation command.
|
||||
| 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` | CPU reference and GPU golden |
|
||||
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | CPU reference vectors and GPU parity |
|
||||
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference and GPU golden |
|
||||
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors plus GPU parity |
|
||||
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
|
||||
|
||||
## Layers And Animation
|
||||
@@ -46,7 +48,7 @@ and validation command.
|
||||
| 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 | `pp_assets`, `pp_paint_renderer`, app | Smoke export and cancellation |
|
||||
| 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
|
||||
|
||||
@@ -63,10 +65,10 @@ and validation command.
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Mouse/keyboard/touch/gestures | `App`, platform entrypoints | `pp_platform_*`, app | Synthetic event playback |
|
||||
| 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 | `App` platform methods | `pp_platform_*` | Platform smoke or mocked service |
|
||||
| Virtual keyboard | platform entrypoints | `pp_platform_*` | Platform smoke |
|
||||
| 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 |
|
||||
@@ -75,9 +77,8 @@ and validation command.
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Upload/download/browse | `app_cloud`, CURL helpers | app service, `pp_platform_*` | Mocked HTTP and timeout 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 |
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -19,16 +19,25 @@ if ($NoApp) {
|
||||
& 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
|
||||
|
||||
@@ -44,6 +53,10 @@ $elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
name = "shader-validation"
|
||||
exitCode = $shaderExitCode
|
||||
},
|
||||
[ordered]@{
|
||||
name = "renderer-boundary"
|
||||
exitCode = $rendererBoundaryExitCode
|
||||
}
|
||||
)
|
||||
elapsedMs = $elapsed
|
||||
|
||||
@@ -1,20 +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}],"elapsedMs":%s}\n' "$preset" "$exit_code" "$configure_exit_code" "$shader_exit_code" "$elapsed_ms"
|
||||
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"
|
||||
|
||||
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"
|
||||
@@ -2,11 +2,210 @@
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[string]$Configuration = "Debug",
|
||||
[string]$Target = "PanoPainter"
|
||||
[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
|
||||
@@ -20,6 +219,7 @@ if ($buildExitCode -ne 0) {
|
||||
stage = "build"
|
||||
exitCode = $buildExitCode
|
||||
elapsedMs = $elapsed
|
||||
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
|
||||
} | ConvertTo-Json -Compress
|
||||
exit $buildExitCode
|
||||
}
|
||||
@@ -43,6 +243,7 @@ $elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsedMs
|
||||
checks = $checks
|
||||
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
|
||||
} | ConvertTo-Json -Compress -Depth 5
|
||||
|
||||
exit $exitCode
|
||||
|
||||
@@ -6,13 +6,91 @@ 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 ))"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"build","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$build_exit" "$elapsed_ms"
|
||||
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
|
||||
|
||||
@@ -24,5 +102,6 @@ fi
|
||||
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","artifact":"%s","exists":%s,"exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$artifact" "$([ "$exit_code" -eq 0 ] && printf true || printf false)" "$exit_code" "$elapsed_ms"
|
||||
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"
|
||||
|
||||
@@ -1,7 +1,54 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Presets = @("android-arm64"),
|
||||
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_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_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_color_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests")
|
||||
[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"
|
||||
|
||||
@@ -3,7 +3,7 @@ set -u
|
||||
|
||||
preset="${1:-android-arm64}"
|
||||
shift || true
|
||||
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_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_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}"
|
||||
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"
|
||||
|
||||
589
src/app.cpp
589
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()
|
||||
{
|
||||
const auto plan = pp::app::plan_recording_clear(
|
||||
rec_running,
|
||||
platform_deletes_recorded_files_on_clear()
|
||||
);
|
||||
if (plan.stop_running_recording)
|
||||
rec_stop();
|
||||
#if defined(__IOS__) || defined(__OSX__)
|
||||
delete_all_files_in_path(rec_path);
|
||||
#endif
|
||||
rec_count = 0;
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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,18 +1014,13 @@ 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__)
|
||||
if (platform_enables_live_asset_reloading())
|
||||
{
|
||||
t_reloader += dt;
|
||||
if (t_reloader > 1.0)
|
||||
{
|
||||
@@ -1008,7 +1035,7 @@ void App::ui_thread_main()
|
||||
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();
|
||||
|
||||
@@ -43,7 +53,6 @@ void App::cloud_upload()
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
171
src/app_core/brush_ui.h
Normal file
171
src/app_core/brush_ui.h
Normal file
@@ -0,0 +1,171 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#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,
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
[[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::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::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");
|
||||
}
|
||||
|
||||
} // 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,18 +438,17 @@ 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");
|
||||
#elif defined(__OSX__)
|
||||
@@ -459,83 +459,83 @@ void App::dialog_export(std::string ext)
|
||||
//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){ });
|
||||
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] {
|
||||
canvas->m_canvas->export_layers(target.value().stem_path, [this] {
|
||||
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);
|
||||
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] {
|
||||
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] {
|
||||
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);
|
||||
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] {
|
||||
#if defined(__IOS__)
|
||||
@@ -547,10 +547,36 @@ void App::dialog_export_depth()
|
||||
#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,19 +587,26 @@ 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)
|
||||
{
|
||||
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");
|
||||
@@ -584,7 +617,6 @@ void App::dialog_export_cube_faces()
|
||||
#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()]];
|
||||
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);
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[osx_view share_file:[NSString stringWithUTF8String:path.c_str()]];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
#elif _WIN32
|
||||
// not implemented
|
||||
#endif
|
||||
}
|
||||
|
||||
bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser)
|
||||
|
||||
1429
src/app_layout.cpp
1429
src/app_layout.cpp
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,60 @@
|
||||
#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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
#if __GL__
|
||||
@@ -45,56 +67,19 @@ void App::initShaders()
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#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 {
|
||||
@@ -33,6 +37,17 @@ namespace {
|
||||
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)
|
||||
@@ -91,4 +106,54 @@ pp::foundation::Result<Rgba8Image> decode_png_rgba8(std::span<const std::byte> b
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,4 +18,9 @@ struct Rgba8Image {
|
||||
[[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);
|
||||
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
#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 {
|
||||
@@ -43,6 +47,26 @@ namespace {
|
||||
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
|
||||
@@ -68,6 +92,18 @@ namespace {
|
||||
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);
|
||||
@@ -303,6 +339,11 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
|
||||
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"));
|
||||
@@ -342,6 +383,11 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
|
||||
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;
|
||||
@@ -616,4 +662,227 @@ pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(std::s
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::assets {
|
||||
@@ -118,6 +119,52 @@ struct PpiDecodedProjectImages {
|
||||
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;
|
||||
|
||||
@@ -143,4 +190,10 @@ struct PpiDecodedProjectImages {
|
||||
[[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);
|
||||
|
||||
}
|
||||
|
||||
471
src/canvas.cpp
471
src/canvas.cpp
@@ -4,8 +4,10 @@
|
||||
#include "app.h"
|
||||
#include "texture.h"
|
||||
#include "node_progress_bar.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <numeric>
|
||||
|
||||
#ifdef __APPLE__
|
||||
@@ -15,6 +17,124 @@
|
||||
void webgl_sync();
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
GLint current_canvas_stroke_internal_format()
|
||||
{
|
||||
if (ShaderManager::ext_float32_linear)
|
||||
return static_cast<GLint>(pp::renderer::gl::rgba32f_internal_format());
|
||||
if (ShaderManager::ext_float16)
|
||||
return static_cast<GLint>(pp::renderer::gl::rgba16f_internal_format());
|
||||
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
|
||||
}
|
||||
|
||||
GLint rgba8_internal_format()
|
||||
{
|
||||
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
|
||||
}
|
||||
|
||||
GLenum texture_2d_target()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::texture_2d_target());
|
||||
}
|
||||
|
||||
GLenum rgba_pixel_format()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::rgba_pixel_format());
|
||||
}
|
||||
|
||||
GLenum unsigned_byte_component_type()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::unsigned_byte_component_type());
|
||||
}
|
||||
|
||||
GLenum viewport_query()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::viewport_query());
|
||||
}
|
||||
|
||||
GLenum color_clear_value_query()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::color_clear_value_query());
|
||||
}
|
||||
|
||||
GLenum depth_test_state()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::depth_test_state());
|
||||
}
|
||||
|
||||
GLenum scissor_test_state()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::scissor_test_state());
|
||||
}
|
||||
|
||||
GLenum blend_state()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::blend_state());
|
||||
}
|
||||
|
||||
GLenum renderbuffer_target()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::renderbuffer_target());
|
||||
}
|
||||
|
||||
GLenum depth_component24_format()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::depth_component24_format());
|
||||
}
|
||||
|
||||
GLenum framebuffer_target()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::framebuffer_target());
|
||||
}
|
||||
|
||||
GLenum framebuffer_depth_attachment()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::framebuffer_depth_attachment());
|
||||
}
|
||||
|
||||
GLint texture_filter_linear()
|
||||
{
|
||||
return static_cast<GLint>(pp::renderer::gl::linear_texture_filter());
|
||||
}
|
||||
|
||||
GLint texture_filter_linear_mipmap_linear()
|
||||
{
|
||||
return static_cast<GLint>(pp::renderer::gl::linear_mipmap_linear_texture_filter());
|
||||
}
|
||||
|
||||
GLint texture_filter_nearest()
|
||||
{
|
||||
return static_cast<GLint>(pp::renderer::gl::nearest_texture_filter());
|
||||
}
|
||||
|
||||
GLint texture_wrap_repeat()
|
||||
{
|
||||
return static_cast<GLint>(pp::renderer::gl::repeat_texture_wrap());
|
||||
}
|
||||
|
||||
GLint texture_wrap_clamp_to_border()
|
||||
{
|
||||
return static_cast<GLint>(pp::renderer::gl::clamp_to_border_texture_wrap());
|
||||
}
|
||||
|
||||
pp::renderer::gl::OpenGlPixelFormat texture_format_for_image_channels(int channel_count)
|
||||
{
|
||||
return pp::renderer::gl::texture_format_for_channel_count(static_cast<std::uint32_t>(channel_count));
|
||||
}
|
||||
|
||||
void set_active_texture_unit(std::uint32_t unit_index)
|
||||
{
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
|
||||
}
|
||||
|
||||
void unbind_texture_2d()
|
||||
{
|
||||
glBindTexture(texture_2d_target(), 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Canvas* Canvas::I;
|
||||
std::vector<CanvasMode*> Canvas::modes[] = {
|
||||
@@ -86,7 +206,14 @@ void Canvas::pick_update(int plane)
|
||||
m_layers_merge.rtt(i).bindFramebuffer();
|
||||
if (!m_pick_data[plane])
|
||||
m_pick_data[plane] = std::make_unique<glm::u8vec4[]>(m_width * m_height);
|
||||
glReadPixels(0, 0, m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, m_pick_data[plane].get());
|
||||
glReadPixels(
|
||||
0,
|
||||
0,
|
||||
m_width,
|
||||
m_height,
|
||||
rgba_pixel_format(),
|
||||
unsigned_byte_component_type(),
|
||||
m_pick_data[plane].get());
|
||||
m_layers_merge.rtt(i).unbindFramebuffer();
|
||||
});
|
||||
|
||||
@@ -160,9 +287,9 @@ void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz)
|
||||
m_mixer.bindFramebuffer();
|
||||
|
||||
glViewport(0, 0, m_mixer.getWidth(), m_mixer.getHeight());
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(depth_test_state());
|
||||
glEnable(scissor_test_state());
|
||||
glDisable(blend_state());
|
||||
|
||||
glScissor(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
|
||||
|
||||
@@ -200,17 +327,17 @@ void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz)
|
||||
ShaderManager::u_int(kShaderUniform::UsePattern, false);
|
||||
ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[plane_index].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_smask.rtt(plane_index).bindTexture();
|
||||
m_node->m_face_plane.draw_fill();
|
||||
m_smask.rtt(plane_index).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[plane_index].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||
}
|
||||
m_sampler.unbind();
|
||||
@@ -298,7 +425,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
|
||||
{
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing)
|
||||
}
|
||||
|
||||
@@ -316,7 +443,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
|
||||
glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(m_width, m_height) - tex_pos));
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, tex_pos.x, tex_pos.y,
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, tex_pos.x, tex_pos.y,
|
||||
tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y);
|
||||
}
|
||||
|
||||
@@ -344,7 +471,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
|
||||
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tex[i].unbind();
|
||||
}
|
||||
|
||||
@@ -441,8 +568,8 @@ void Canvas::stroke_draw()
|
||||
|
||||
GLint vp[4];
|
||||
GLfloat cc[4];
|
||||
glGetIntegerv(GL_VIEWPORT, vp);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
||||
glGetIntegerv(viewport_query(), vp);
|
||||
glGetFloatv(color_clear_value_query(), cc);
|
||||
|
||||
const auto& brush = m_current_stroke->m_brush;
|
||||
const auto& dual_brush = m_dual_stroke->m_brush;
|
||||
@@ -460,7 +587,7 @@ void Canvas::stroke_draw()
|
||||
if (brush->m_pattern_flipx) patt_scale.x *= -1.f;
|
||||
if (brush->m_pattern_flipy) patt_scale.y *= -1.f;
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(blend_state());
|
||||
ShaderManager::use(kShader::Stroke);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0); // brush
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
@@ -485,13 +612,13 @@ void Canvas::stroke_draw()
|
||||
|
||||
// DRAW MAIN BRUSH
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
brush->m_tip_texture->bind();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
brush->m_pattern_texture ?
|
||||
brush->m_pattern_texture->bind() :
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
unbind_texture_2d();
|
||||
set_active_texture_unit(3);
|
||||
m_mixer.bindTexture();
|
||||
|
||||
auto frames = stroke_draw_compute(*m_current_stroke);
|
||||
@@ -532,9 +659,9 @@ void Canvas::stroke_draw()
|
||||
}
|
||||
}
|
||||
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
m_mixer.unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
brush->m_tip_texture->unbind();
|
||||
|
||||
// pad stroke
|
||||
@@ -550,7 +677,7 @@ void Canvas::stroke_draw()
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, pad_color);
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
ShaderManager::u_int(kShaderUniform::TexBG, 1);
|
||||
}
|
||||
for (int i = 0; i < 6; i++)
|
||||
@@ -584,14 +711,14 @@ void Canvas::stroke_draw()
|
||||
glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o;
|
||||
m_tex[i].bind();
|
||||
if (sz.x > 0 && sz.y > 0)
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, o.x, o.y, o.x, o.y, sz.x, sz.y);
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, o.x, o.y, o.x, o.y, sz.x, sz.y);
|
||||
}
|
||||
m_brush_shape.draw_fill();
|
||||
m_tmp[i].unbindFramebuffer();
|
||||
}
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
unbind_texture_2d();
|
||||
}
|
||||
|
||||
// DRAW DUAL BRUSH
|
||||
@@ -604,10 +731,10 @@ void Canvas::stroke_draw()
|
||||
ShaderManager::u_float(kShaderUniform::Wet, 0);
|
||||
ShaderManager::u_float(kShaderUniform::Noise, 0);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
dual_brush->m_tip_texture ?
|
||||
dual_brush->m_tip_texture->bind() :
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
unbind_texture_2d();
|
||||
auto frames_dual = stroke_draw_compute(*m_dual_stroke);
|
||||
for (auto& f : frames_dual)
|
||||
{
|
||||
@@ -747,9 +874,9 @@ void Canvas::stroke_commit()
|
||||
// save viewport and clear color states
|
||||
GLint vp[4];
|
||||
GLfloat cc[4];
|
||||
glGetIntegerv(GL_VIEWPORT, vp);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
||||
GLboolean blend = glIsEnabled(GL_BLEND);
|
||||
glGetIntegerv(viewport_query(), vp);
|
||||
glGetFloatv(color_clear_value_query(), cc);
|
||||
auto blend = glIsEnabled(blend_state());
|
||||
|
||||
// allocate action to add to history
|
||||
auto action = new ActionStroke;
|
||||
@@ -760,7 +887,7 @@ void Canvas::stroke_commit()
|
||||
|
||||
// prepare common states
|
||||
glViewport(0, 0, m_width, m_height);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(blend_state());
|
||||
|
||||
const auto& b = m_current_stroke->m_brush;
|
||||
|
||||
@@ -775,7 +902,14 @@ void Canvas::stroke_commit()
|
||||
// save image before commit
|
||||
glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
|
||||
action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
|
||||
glReadPixels(m_dirty_box[i].x, m_dirty_box[i].y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
|
||||
glReadPixels(
|
||||
m_dirty_box[i].x,
|
||||
m_dirty_box[i].y,
|
||||
box_sz.x,
|
||||
box_sz.y,
|
||||
rgba_pixel_format(),
|
||||
unsigned_byte_component_type(),
|
||||
action->m_image[i].get());
|
||||
|
||||
action->m_box[i] = m_dirty_box[i];
|
||||
action->m_old_box[i] = m_layers[m_current_layer_idx]->box(i);
|
||||
@@ -792,13 +926,13 @@ void Canvas::stroke_commit()
|
||||
m_layers[m_current_layer_idx]->face(i) = true;
|
||||
|
||||
// copy to tmp2 for layer blending
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex2[i].bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
|
||||
m_tex2[i].unbind();
|
||||
|
||||
m_tmp[i].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tex2[i].bind();
|
||||
m_sampler.bind(0);
|
||||
m_sampler_nearest.bind(1);
|
||||
@@ -815,17 +949,17 @@ void Canvas::stroke_commit()
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, 1);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex2[i].bind();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[i].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_smask.rtt(i).bindTexture();
|
||||
m_plane.draw_fill();
|
||||
m_smask.rtt(i).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[i].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex2[i].unbind();
|
||||
}
|
||||
else
|
||||
@@ -859,28 +993,28 @@ void Canvas::stroke_commit()
|
||||
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
|
||||
ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_pattern_offset);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex2[i].bind();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[i].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_smask.rtt(i).bindTexture();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
m_tmp_dual[i].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_plane.draw_fill();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
m_tmp_dual[i].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_smask.rtt(i).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[i].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex2[i].unbind();
|
||||
}
|
||||
// else
|
||||
@@ -903,19 +1037,19 @@ void Canvas::stroke_commit()
|
||||
ShaderManager::use(kShader::StrokeDilate);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
|
||||
ShaderManager::u_int(kShaderUniform::TexBG, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex2[i].bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
|
||||
m_plane.draw_fill();
|
||||
|
||||
m_layers[m_current_layer_idx]->rtt(i).unbindFramebuffer();
|
||||
}
|
||||
|
||||
// restore viewport and clear color states
|
||||
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
||||
blend ? glEnable(blend_state()) : glDisable(blend_state());
|
||||
glViewport(vp[0], vp[1], vp[2], vp[3]);
|
||||
glClearColor(cc[0], cc[1], cc[2], cc[3]);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
|
||||
// save history
|
||||
action->m_layer_idx = m_current_layer_idx;
|
||||
@@ -962,7 +1096,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
|
||||
use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0;
|
||||
|
||||
// if not using shader blend, use gl rasterizer blend
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(depth_test_state());
|
||||
|
||||
for (int plane_index = 0; plane_index < 6; plane_index++)
|
||||
{
|
||||
@@ -974,7 +1108,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
|
||||
|
||||
if (use_blend)
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(blend_state());
|
||||
m_layers_merge.rtt(plane_index).clear();
|
||||
}
|
||||
else
|
||||
@@ -986,7 +1120,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
|
||||
m_plane.draw_fill();
|
||||
}
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(blend_state());
|
||||
}
|
||||
|
||||
for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
|
||||
@@ -1018,17 +1152,17 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
|
||||
//ShaderManager::u_int(kShaderUniform::Lock, m_layers[layer_index]->m_alpha_locked);
|
||||
ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[plane_index].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_smask.rtt(plane_index).bindTexture();
|
||||
m_plane.draw_fill();
|
||||
m_smask.rtt(plane_index).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[plane_index].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||
}
|
||||
else if (m_current_stroke && m_show_tmp && m_current_layer_idx == layer_index)
|
||||
@@ -1068,28 +1202,28 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
|
||||
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
|
||||
ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[plane_index].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_smask.rtt(plane_index).bindTexture();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
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_plane.draw_fill();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
m_tmp_dual[plane_index].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_smask.rtt(plane_index).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_tmp[plane_index].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||
}
|
||||
else
|
||||
@@ -1101,7 +1235,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
|
||||
ShaderManager::u_int(kShaderUniform::Highlight, m_layers[layer_index]->m_hightlight);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
m_plane.draw_fill();
|
||||
m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||
@@ -1129,35 +1263,35 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
|
||||
ShaderManager::u_int(kShaderUniform::TexBG, 2);
|
||||
}
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_merge_rtt.bindTexture();
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_merge_tex.bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
|
||||
}
|
||||
|
||||
m_plane.draw_fill();
|
||||
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_merge_tex.unbind();
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_merge_rtt.unbindTexture();
|
||||
}
|
||||
}
|
||||
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_merge_tex.bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
|
||||
|
||||
// draw the grid behind the layers using a temporary copy
|
||||
if (use_blend)
|
||||
{
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(blend_state());
|
||||
|
||||
//draw the grid
|
||||
if (draw_checkerboard)
|
||||
@@ -1170,7 +1304,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
|
||||
|
||||
// draw the layers
|
||||
m_sampler.bind(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_merge_tex.bind();
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
@@ -1319,7 +1453,7 @@ void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index
|
||||
{
|
||||
// prepare common states
|
||||
glViewport(0, 0, m_width, m_height);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(blend_state());
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
@@ -1336,9 +1470,9 @@ void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index
|
||||
m_layers[dest_idx]->face(i) = true;
|
||||
|
||||
// copy to tmp2 for layer blending
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex2[i].bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
|
||||
m_tex2[i].unbind();
|
||||
|
||||
m_sampler.bind(0);
|
||||
@@ -1356,13 +1490,13 @@ void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index
|
||||
ShaderManager::u_int(kShaderUniform::UseDual, false);
|
||||
ShaderManager::u_int(kShaderUniform::UsePattern, false);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex2[i].bind();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_layers[source_idx]->rtt(i).bindTexture();
|
||||
m_plane.draw_fill();
|
||||
m_layers[source_idx]->rtt(i).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_tex2[i].unbind();
|
||||
}
|
||||
|
||||
@@ -1571,8 +1705,8 @@ void Canvas::FloodData::apply()
|
||||
App::I->render_task([&]
|
||||
{
|
||||
rtt.bindTexture();
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, rtt.getWidth(), rtt.getHeight(),
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, rgb[plane].get());
|
||||
glTexSubImage2D(texture_2d_target(), 0, 0, 0, rtt.getWidth(), rtt.getHeight(),
|
||||
rgba_pixel_format(), unsigned_byte_component_type(), rgb[plane].get());
|
||||
rtt.unbindTexture();
|
||||
});
|
||||
layer->face(plane) = true;
|
||||
@@ -1588,23 +1722,11 @@ void Canvas::resize(int width, int height)
|
||||
m_size = { width, height };
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (ShaderManager::ext_float32_linear)
|
||||
{
|
||||
m_tmp[i].create(width, height, -1, GL_RGBA32F);
|
||||
m_tmp_dual[i].create(width, height, -1, GL_RGBA32F);
|
||||
}
|
||||
else if (ShaderManager::ext_float16)
|
||||
{
|
||||
m_tmp[i].create(width, height, -1, GL_RGBA16F);
|
||||
m_tmp_dual[i].create(width, height, -1, GL_RGBA16F);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_tmp[i].create(width, height, -1, GL_RGBA8);
|
||||
m_tmp_dual[i].create(width, height, -1, GL_RGBA8);
|
||||
}
|
||||
m_tex[i].create(width, height, GL_RGBA8);
|
||||
m_tex2[i].create(width, height, GL_RGBA8);
|
||||
const auto stroke_format = current_canvas_stroke_internal_format();
|
||||
m_tmp[i].create(width, height, -1, stroke_format);
|
||||
m_tmp_dual[i].create(width, height, -1, stroke_format);
|
||||
m_tex[i].create(width, height, rgba8_internal_format());
|
||||
m_tex2[i].create(width, height, rgba8_internal_format());
|
||||
}
|
||||
for (auto& l : m_layers)
|
||||
l->resize(width, height);
|
||||
@@ -1640,35 +1762,23 @@ bool Canvas::create(int width, int height)
|
||||
m_size = { width, height };
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (ShaderManager::ext_float32_linear)
|
||||
{
|
||||
m_tmp[i].create(width, height, -1, GL_RGBA32F);
|
||||
m_tmp_dual[i].create(width, height, -1, GL_RGBA32F);
|
||||
}
|
||||
else if (ShaderManager::ext_float16)
|
||||
{
|
||||
m_tmp[i].create(width, height, -1, GL_RGBA16F);
|
||||
m_tmp_dual[i].create(width, height, -1, GL_RGBA16F);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_tmp[i].create(width, height, -1, GL_RGBA8);
|
||||
m_tmp_dual[i].create(width, height, -1, GL_RGBA8);
|
||||
}
|
||||
m_tex[i].create(width, height, GL_RGBA8);
|
||||
m_tex2[i].create(width, height, GL_RGBA8);
|
||||
const auto stroke_format = current_canvas_stroke_internal_format();
|
||||
m_tmp[i].create(width, height, -1, stroke_format);
|
||||
m_tmp_dual[i].create(width, height, -1, stroke_format);
|
||||
m_tex[i].create(width, height, rgba8_internal_format());
|
||||
m_tex2[i].create(width, height, rgba8_internal_format());
|
||||
}
|
||||
#if defined(__GLES__)
|
||||
m_sampler_brush.create();
|
||||
#else
|
||||
m_sampler_brush.create(GL_LINEAR, GL_CLAMP_TO_BORDER);
|
||||
m_sampler_brush.create(texture_filter_linear(), texture_wrap_clamp_to_border());
|
||||
#endif
|
||||
m_sampler.create(GL_LINEAR);
|
||||
m_sampler_nearest.create(GL_NEAREST);
|
||||
m_sampler_brush.set_filter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
|
||||
m_sampler.create(texture_filter_linear());
|
||||
m_sampler_nearest.create(texture_filter_nearest());
|
||||
m_sampler_brush.set_filter(texture_filter_linear_mipmap_linear(), texture_filter_linear());
|
||||
m_sampler_brush.set_border({ 1, 1, 1, 1 });
|
||||
m_sampler_stencil.create(GL_LINEAR, GL_REPEAT);
|
||||
m_sampler_mix.create(GL_NEAREST, GL_REPEAT);
|
||||
m_sampler_stencil.create(texture_filter_linear(), texture_wrap_repeat());
|
||||
m_sampler_mix.create(texture_filter_nearest(), texture_wrap_repeat());
|
||||
m_sampler_linear.create();
|
||||
m_plane.create<1>(1, 1);
|
||||
m_plane_brush.create<1>(1, 1);
|
||||
@@ -1754,17 +1864,20 @@ void Canvas::import_equirectangular_thread(std::string file_path, std::shared_pt
|
||||
{
|
||||
Texture2D tex;
|
||||
static const GLint indices[] = { 5, 0, 4, 1, 2, 3 };
|
||||
static const GLint formats[] = { GL_RED, GL_RG, GL_RGB, GL_RGBA };
|
||||
static const GLint iformats[] = { GL_R8, GL_RG8, GL_RGB8, GL_RGBA8 };
|
||||
tex.create(img.width, img.width, iformats[img.comp - 1], formats[img.comp - 1]);
|
||||
const auto texture_format = texture_format_for_image_channels(img.comp);
|
||||
tex.create(
|
||||
img.width,
|
||||
img.width,
|
||||
static_cast<GLint>(texture_format.internal_format),
|
||||
static_cast<GLint>(texture_format.pixel_format));
|
||||
int stride = img.width * img.width * img.comp;
|
||||
Plane plane;
|
||||
plane.create<1>(2, 2);
|
||||
draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(depth_test_state());
|
||||
tex.update(img.m_data.get() + indices[i] * stride);
|
||||
m_sampler.bind(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
tex.bind();
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
@@ -1782,9 +1895,9 @@ void Canvas::import_equirectangular_thread(std::string file_path, std::shared_pt
|
||||
Sphere sphere;
|
||||
sphere.create<64, 64>(2.f);
|
||||
draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(depth_test_state());
|
||||
m_sampler.bind(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
tex.bind();
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
@@ -1920,8 +2033,8 @@ void Canvas::export_depth_thread(std::string file_name)
|
||||
|
||||
rtt.bindFramebuffer();
|
||||
rtt.clear({ 0, 0, 0, 1 });
|
||||
glEnable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(blend_state());
|
||||
glDisable(depth_test_state());
|
||||
glViewport(0, 0, rtt.getWidth(), rtt.getHeight());
|
||||
for (int plane_index = 0; plane_index < 6; plane_index++)
|
||||
{
|
||||
@@ -1937,7 +2050,7 @@ void Canvas::export_depth_thread(std::string file_name)
|
||||
ShaderManager::u_int(kShaderUniform::Highlight, false);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers_merge.rtt(plane_index).bindTexture();
|
||||
m_plane.draw_fill();
|
||||
m_layers_merge.rtt(plane_index).unbindTexture();
|
||||
@@ -1955,8 +2068,8 @@ void Canvas::export_depth_thread(std::string file_name)
|
||||
{
|
||||
rtt.bindFramebuffer();
|
||||
rtt.clear({ 0, 0, 0, 1 });
|
||||
glEnable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(blend_state());
|
||||
glDisable(depth_test_state());
|
||||
glViewport(0, 0, rtt.getWidth(), rtt.getHeight());
|
||||
for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
|
||||
{
|
||||
@@ -1978,7 +2091,7 @@ void Canvas::export_depth_thread(std::string file_name)
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, { glm::vec3((float)(layer_index + 1) / (float)(m_layers.size() + 1)), 1.f });
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
m_plane.draw_fill();
|
||||
m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||
@@ -2646,9 +2759,9 @@ Image Canvas::thumbnail_generate(int w, int h)
|
||||
// save viewport and clear color states
|
||||
GLint vp[4];
|
||||
GLfloat cc[4];
|
||||
glGetIntegerv(GL_VIEWPORT, vp);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
||||
GLboolean blend = glIsEnabled(GL_BLEND);
|
||||
glGetIntegerv(viewport_query(), vp);
|
||||
glGetFloatv(color_clear_value_query(), cc);
|
||||
auto blend = glIsEnabled(blend_state());
|
||||
|
||||
// prepare common states
|
||||
glViewport(0, 0, w, h);
|
||||
@@ -2667,7 +2780,7 @@ Image Canvas::thumbnail_generate(int w, int h)
|
||||
fb.clear({ 1, 1, 1, 0 });
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(blend_state());
|
||||
auto plane_mvp = proj * m_mv * m_plane_transform[i] * glm::translate(glm::vec3(0, 0, -1));
|
||||
|
||||
ShaderManager::use(kShader::TextureBlend);
|
||||
@@ -2676,7 +2789,7 @@ Image Canvas::thumbnail_generate(int w, int h)
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
ShaderManager::u_int(kShaderUniform::TexBG, 2);
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
blendtex.bind();
|
||||
m_sampler_nearest.bind(2);
|
||||
}
|
||||
@@ -2689,12 +2802,12 @@ Image Canvas::thumbnail_generate(int w, int h)
|
||||
continue;
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h);
|
||||
set_active_texture_unit(2);
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, w, h);
|
||||
}
|
||||
ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_layers[layer_index]->rtt(i).bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
m_layers[layer_index]->rtt(i).unbindTexture();
|
||||
@@ -2702,14 +2815,14 @@ Image Canvas::thumbnail_generate(int w, int h)
|
||||
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
blendtex.unbind();
|
||||
}
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
blendtex.bind();
|
||||
// copy the content of the fb before drawing the grid
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h);
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, w, h);
|
||||
|
||||
// draw the grid
|
||||
ShaderManager::use(kShader::Checkerboard);
|
||||
@@ -2717,7 +2830,7 @@ Image Canvas::thumbnail_generate(int w, int h)
|
||||
m_face_plane.draw_fill();
|
||||
|
||||
// now blend with the background
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(blend_state());
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
|
||||
@@ -2736,10 +2849,10 @@ Image Canvas::thumbnail_generate(int w, int h)
|
||||
blendtex.destroy();
|
||||
|
||||
// restore viewport and clear color states
|
||||
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
||||
blend ? glEnable(blend_state()) : glDisable(blend_state());
|
||||
glViewport(vp[0], vp[1], vp[2], vp[3]);
|
||||
glClearColor(cc[0], cc[1], cc[2], cc[3]);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
});
|
||||
|
||||
return image;
|
||||
@@ -2779,30 +2892,30 @@ void Canvas::draw_objects_direct(std::function<void(const glm::mat4& camera, con
|
||||
// save viewport and clear color states
|
||||
GLint vp[4];
|
||||
GLfloat cc[4];
|
||||
glGetIntegerv(GL_VIEWPORT, vp);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
||||
GLboolean blend = glIsEnabled(GL_BLEND);
|
||||
glGetIntegerv(viewport_query(), vp);
|
||||
glGetFloatv(color_clear_value_query(), cc);
|
||||
auto blend = glIsEnabled(blend_state());
|
||||
|
||||
// prepare common states
|
||||
glViewport(0, 0, layer.w, layer.h);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(blend_state());
|
||||
|
||||
GLuint rboID;
|
||||
glGenRenderbuffers(1, &rboID);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, rboID);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, layer.w, layer.h);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
glBindRenderbuffer(renderbuffer_target(), rboID);
|
||||
glRenderbufferStorage(renderbuffer_target(), depth_component24_format(), layer.w, layer.h);
|
||||
glBindRenderbuffer(renderbuffer_target(), 0);
|
||||
|
||||
glm::mat4 proj = glm::perspective(glm::radians(90.f), 1.f, .01f, 1000.f);
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
glm::mat4 plane_camera = glm::lookAt(glm::vec3(0), m_plane_origin[i], m_plane_tangent[i]);
|
||||
layer.rtt(i, frame).bindFramebuffer();
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboID);
|
||||
glFramebufferRenderbuffer(framebuffer_target(), framebuffer_depth_attachment(), renderbuffer_target(), rboID);
|
||||
|
||||
observer(plane_camera, proj, i);
|
||||
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, 0);
|
||||
glFramebufferRenderbuffer(framebuffer_target(), framebuffer_depth_attachment(), renderbuffer_target(), 0);
|
||||
layer.rtt(i, frame).unbindFramebuffer();
|
||||
|
||||
layer.face(i, frame) = true;
|
||||
@@ -2812,10 +2925,10 @@ void Canvas::draw_objects_direct(std::function<void(const glm::mat4& camera, con
|
||||
glDeleteRenderbuffers(1, &rboID);
|
||||
|
||||
// restore viewport and clear color states
|
||||
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
||||
blend ? glEnable(blend_state()) : glDisable(blend_state());
|
||||
glViewport(vp[0], vp[1], vp[2], vp[3]);
|
||||
glClearColor(cc[0], cc[1], cc[2], cc[3]);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2826,24 +2939,24 @@ void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm:
|
||||
// save viewport and clear color states
|
||||
GLint vp[4];
|
||||
GLfloat cc[4];
|
||||
glGetIntegerv(GL_VIEWPORT, vp);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
||||
GLboolean blend = glIsEnabled(GL_BLEND);
|
||||
glGetIntegerv(viewport_query(), vp);
|
||||
glGetFloatv(color_clear_value_query(), cc);
|
||||
auto blend = glIsEnabled(blend_state());
|
||||
|
||||
// prepare common states
|
||||
glViewport(0, 0, layer.w, layer.h);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(blend_state());
|
||||
|
||||
GLuint rboID;
|
||||
glGenRenderbuffers(1, &rboID);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, rboID);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, layer.w, layer.h);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
glBindRenderbuffer(renderbuffer_target(), rboID);
|
||||
glRenderbufferStorage(renderbuffer_target(), depth_component24_format(), layer.w, layer.h);
|
||||
glBindRenderbuffer(renderbuffer_target(), 0);
|
||||
|
||||
RTT rtt;
|
||||
rtt.create(layer.w, layer.h);
|
||||
rtt.bindFramebuffer();
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboID);
|
||||
glFramebufferRenderbuffer(framebuffer_target(), framebuffer_depth_attachment(), renderbuffer_target(), rboID);
|
||||
rtt.unbindFramebuffer();
|
||||
|
||||
// allocate action to add to history
|
||||
@@ -2877,7 +2990,7 @@ void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm:
|
||||
if (has_data)
|
||||
{
|
||||
action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
|
||||
glReadPixels(bounds.x, bounds.y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
|
||||
glReadPixels(bounds.x, bounds.y, box_sz.x, box_sz.y, rgba_pixel_format(), unsigned_byte_component_type(), action->m_image[i].get());
|
||||
action->m_box[i] = bounds;
|
||||
}
|
||||
action->m_old_box[i] = layer.box(i, frame);
|
||||
@@ -2890,7 +3003,7 @@ void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm:
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-0.5f, 0.5f, -0.5f, 0.5f));
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_sampler_nearest.bind(0);
|
||||
rtt.bindTexture();
|
||||
m_plane.draw_fill();
|
||||
@@ -2917,10 +3030,10 @@ void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm:
|
||||
rtt.destroy();
|
||||
|
||||
// restore viewport and clear color states
|
||||
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
||||
blend ? glEnable(blend_state()) : glDisable(blend_state());
|
||||
glViewport(vp[0], vp[1], vp[2], vp[3]);
|
||||
glClearColor(cc[0], cc[1], cc[2], cc[3]);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "document/document.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::document {
|
||||
@@ -107,37 +109,70 @@ namespace {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> byte_size(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height) noexcept
|
||||
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("face pixel dimensions overflow"));
|
||||
pp::foundation::Status::out_of_range(dimensions_overflow_message));
|
||||
}
|
||||
|
||||
const auto pixels = width64 * height64;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / rgba8_components) {
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("face pixel byte size overflows"));
|
||||
pp::foundation::Status::out_of_range(byte_size_overflow_message));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * rgba8_components;
|
||||
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("face pixel payload exceeds the configured limit"));
|
||||
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("face pixel payload exceeds addressable memory"));
|
||||
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,
|
||||
@@ -168,6 +203,60 @@ namespace {
|
||||
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)
|
||||
@@ -244,6 +333,11 @@ pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(Docu
|
||||
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()) {
|
||||
@@ -269,9 +363,33 @@ pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(Docu
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -325,6 +443,11 @@ std::size_t CanvasDocument::face_pixel_payload_count() const noexcept
|
||||
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_;
|
||||
@@ -335,6 +458,11 @@ 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) {
|
||||
@@ -656,6 +784,47 @@ pp::foundation::Status CanvasDocument::set_layer_frame_face_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)
|
||||
|
||||
@@ -20,6 +20,7 @@ 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 {
|
||||
@@ -37,6 +38,15 @@ struct LayerFacePixels {
|
||||
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;
|
||||
@@ -65,6 +75,7 @@ struct DocumentSnapshotConfig {
|
||||
std::uint32_t height = 0;
|
||||
std::span<const DocumentLayerConfig> layers;
|
||||
std::span<const AnimationFrame> frames;
|
||||
std::span<const SelectionMask> selection_masks;
|
||||
};
|
||||
|
||||
class CanvasDocument {
|
||||
@@ -79,8 +90,10 @@ public:
|
||||
[[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);
|
||||
@@ -102,6 +115,8 @@ public:
|
||||
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;
|
||||
@@ -110,6 +125,7 @@ private:
|
||||
std::size_t active_frame_index_ = 0;
|
||||
std::vector<Layer> layers_;
|
||||
std::vector<AnimationFrame> frames_;
|
||||
std::vector<SelectionMask> selection_masks_;
|
||||
};
|
||||
|
||||
class DocumentHistory {
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -79,6 +79,7 @@ namespace {
|
||||
.height = project.body.summary.height,
|
||||
.layers = layers,
|
||||
.frames = frames,
|
||||
.selection_masks = {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
#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)
|
||||
{
|
||||
@@ -135,6 +156,12 @@ Status ByteWriter::write_bytes(std::span<const std::byte> bytes)
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
enum class StatusCode {
|
||||
@@ -38,7 +40,7 @@ class Result {
|
||||
public:
|
||||
[[nodiscard]] static constexpr Result success(T value) noexcept
|
||||
{
|
||||
return Result(value, Status::success());
|
||||
return Result(std::move(value), Status::success());
|
||||
}
|
||||
|
||||
[[nodiscard]] static constexpr Result failure(Status status) noexcept
|
||||
@@ -61,6 +63,11 @@ public:
|
||||
return value_;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr T& value() noexcept
|
||||
{
|
||||
return value_;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr Status status() const noexcept
|
||||
{
|
||||
return status_;
|
||||
@@ -68,7 +75,7 @@ public:
|
||||
|
||||
private:
|
||||
constexpr Result(T value, Status status) noexcept
|
||||
: value_(value)
|
||||
: value_(std::move(value))
|
||||
, status_(status)
|
||||
{
|
||||
}
|
||||
|
||||
150
src/main.cpp
150
src/main.cpp
@@ -8,6 +8,8 @@
|
||||
#include "canvas.h"
|
||||
#include "keymap.h"
|
||||
#include "hmd.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "platform_windows/windows_platform_services.h"
|
||||
#include "../resource.h"
|
||||
|
||||
#include <shellscalingapi.h>
|
||||
@@ -242,114 +244,6 @@ void win32_update_fps(int frames)
|
||||
PostMessage(hWnd, WM_USER_WAKEUP, 0, 0);
|
||||
}
|
||||
|
||||
void win32_show_cursor(bool visible)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(main_task_mutex);
|
||||
main_tasklist.emplace_back([=] {
|
||||
if (visible)
|
||||
while (ShowCursor(true) < 0);
|
||||
else
|
||||
while (ShowCursor(false) >= 0);
|
||||
});
|
||||
}
|
||||
|
||||
std::string win32_clipboard_get_text()
|
||||
{
|
||||
std::string ret;
|
||||
if (OpenClipboard(hWnd))
|
||||
{
|
||||
if (HANDLE h = GetClipboardData(CF_TEXT))
|
||||
{
|
||||
if (char* s = (char*)GlobalLock(h))
|
||||
{
|
||||
ret = s;
|
||||
GlobalUnlock(h);
|
||||
}
|
||||
}
|
||||
CloseClipboard();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool win32_clipboard_set_text(const std::string& s)
|
||||
{
|
||||
bool success = false;
|
||||
if (OpenClipboard(hWnd))
|
||||
{
|
||||
// owned by SetClipboardData
|
||||
if (HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, s.size() + 1))
|
||||
{
|
||||
if (char* p = (char*)GlobalLock(h))
|
||||
{
|
||||
std::copy(s.begin(), s.end(), p);
|
||||
p[s.size()] = 0; // string null-termination
|
||||
GlobalUnlock(h);
|
||||
success = true;
|
||||
}
|
||||
EmptyClipboard();
|
||||
SetClipboardData(CF_TEXT, h);
|
||||
}
|
||||
CloseClipboard();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
std::string win32_open_file(const char* filter)
|
||||
{
|
||||
OPENFILENAMEA ofn;
|
||||
char fileName[MAX_PATH] = "";
|
||||
ZeroMemory(&ofn, sizeof(ofn));
|
||||
ofn.lStructSize = sizeof(OPENFILENAME);
|
||||
ofn.hwndOwner = hWnd;
|
||||
ofn.lpstrFilter = filter;
|
||||
ofn.lpstrFile = fileName;
|
||||
ofn.nMaxFile = MAX_PATH;
|
||||
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR;
|
||||
ofn.lpstrDefExt = "";
|
||||
ofn.lpstrInitialDir = "";
|
||||
if (GetOpenFileNameA(&ofn) != NULL)
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string win32_save_file(const char* filter)
|
||||
{
|
||||
OPENFILENAMEA ofn;
|
||||
char fileName[MAX_PATH] = "";
|
||||
ZeroMemory(&ofn, sizeof(ofn));
|
||||
ofn.lStructSize = sizeof(OPENFILENAME);
|
||||
ofn.hwndOwner = hWnd;
|
||||
ofn.lpstrFilter = filter;
|
||||
ofn.lpstrFile = fileName;
|
||||
ofn.nMaxFile = MAX_PATH;
|
||||
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR | OFN_OVERWRITEPROMPT;
|
||||
ofn.lpstrDefExt = "";
|
||||
ofn.lpstrInitialDir = "";
|
||||
if (GetSaveFileNameA(&ofn) != NULL)
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string win32_open_dir()
|
||||
{
|
||||
BROWSEINFOA bi;
|
||||
char Buffer[MAX_PATH];
|
||||
ZeroMemory(Buffer, MAX_PATH);
|
||||
ZeroMemory(&bi, sizeof(bi));
|
||||
bi.hwndOwner = hWnd;
|
||||
bi.pszDisplayName = Buffer;
|
||||
bi.lpszTitle = "Title";
|
||||
bi.ulFlags = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNONLYFSDIRS | BIF_SHAREABLE;
|
||||
LPCITEMIDLIST pFolder = SHBrowseForFolderA(&bi);
|
||||
if (pFolder == NULL) return "";
|
||||
if (!SHGetPathFromIDListA(pFolder, Buffer)) return "";
|
||||
return Buffer;
|
||||
}
|
||||
|
||||
int read_WMI_info()
|
||||
{
|
||||
// see: http://win32easy.blogspot.co.uk/2011/03/wmi-in-c-query-everyting-from-your-os.html
|
||||
@@ -828,7 +722,7 @@ void _post_call_callback(const char* name, void* funcptr, int len_args, ...)
|
||||
GLenum error_code;
|
||||
error_code = glad_glGetError();
|
||||
|
||||
if (error_code != GL_NO_ERROR)
|
||||
if (error_code != pp::renderer::gl::no_error_code())
|
||||
{
|
||||
LOG("ERROR %d in %s\n", error_code, name);
|
||||
}
|
||||
@@ -840,6 +734,7 @@ int main(int argc, char** argv)
|
||||
PIXELFORMATDESCRIPTOR pfd;
|
||||
|
||||
App::I = new App();
|
||||
App::I->set_platform_services(&pp::platform::windows::platform_services());
|
||||
App::I->initLog();
|
||||
|
||||
init_shcore_API();
|
||||
@@ -958,9 +853,9 @@ int main(int argc, char** argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG("GL version: %s", glGetString(GL_VERSION));
|
||||
LOG("GL vendor: %s", glGetString(GL_VENDOR));
|
||||
LOG("GL renderer: %s", glGetString(GL_RENDERER));
|
||||
LOG("GL version: %s", glGetString(static_cast<GLenum>(pp::renderer::gl::version_string_name())));
|
||||
LOG("GL vendor: %s", glGetString(static_cast<GLenum>(pp::renderer::gl::vendor_string_name())));
|
||||
LOG("GL renderer: %s", glGetString(static_cast<GLenum>(pp::renderer::gl::renderer_string_name())));
|
||||
|
||||
#ifdef USE_RENDERDOC
|
||||
if (!win32_renderdoc_init())
|
||||
@@ -968,33 +863,12 @@ int main(int argc, char** argv)
|
||||
#endif // USE_RENDERDOC
|
||||
|
||||
swprintf_s(window_title, L"PanoPainter %s (%s)", g_version_number_w,
|
||||
str2wstr((char*)glGetString(GL_RENDERER)).c_str());
|
||||
str2wstr((char*)glGetString(static_cast<GLenum>(pp::renderer::gl::renderer_string_name()))).c_str());
|
||||
|
||||
// If supported create a 3.3 context
|
||||
if (GLAD_WGL_ARB_create_context)
|
||||
{
|
||||
int contex_attribs[] =
|
||||
{
|
||||
WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
|
||||
WGL_CONTEXT_MINOR_VERSION_ARB, 3,
|
||||
WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
|
||||
WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
|
||||
0
|
||||
};
|
||||
int pixel_attribs[] =
|
||||
{
|
||||
WGL_DRAW_TO_WINDOW_ARB, GL_TRUE,
|
||||
WGL_SUPPORT_OPENGL_ARB, GL_TRUE,
|
||||
WGL_DOUBLE_BUFFER_ARB, GL_TRUE,
|
||||
WGL_ACCELERATION_ARB,WGL_FULL_ACCELERATION_ARB,
|
||||
WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
|
||||
WGL_COLOR_BITS_ARB, 24,
|
||||
WGL_DEPTH_BITS_ARB, 16,
|
||||
//WGL_STENCIL_BITS_ARB, 8,
|
||||
//WGL_SAMPLE_BUFFERS_ARB, 1, // Number of buffers (must be 1 at time of writing)
|
||||
//WGL_SAMPLES_ARB, 4, // Number of samples
|
||||
0
|
||||
};
|
||||
const auto wgl_config = pp::renderer::gl::windows_wgl_core_context_3_3_config();
|
||||
UINT numFormat;
|
||||
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
@@ -1006,9 +880,9 @@ int main(int argc, char** argv)
|
||||
(float)(clientRect.bottom - clientRect.top), 0, 0, hInst, 0);
|
||||
|
||||
hDC = GetDC(hWnd);
|
||||
wglChoosePixelFormatARB(hDC, pixel_attribs, nullptr, 1, &pxfmt, &numFormat);
|
||||
wglChoosePixelFormatARB(hDC, wgl_config.pixel_format_attributes.data(), nullptr, 1, &pxfmt, &numFormat);
|
||||
SetPixelFormat(hDC, pxfmt, &pfd);
|
||||
hRC = wglCreateContextAttribsARB(hDC, NULL, contex_attribs);
|
||||
hRC = wglCreateContextAttribsARB(hDC, NULL, wgl_config.context_attributes.data());
|
||||
wglMakeCurrent(hDC, hRC);
|
||||
}
|
||||
else
|
||||
@@ -1188,7 +1062,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
case WM_ACTIVATE:
|
||||
{
|
||||
win32_show_cursor(true);
|
||||
pp::platform::windows::platform_services().set_cursor_visible(true);
|
||||
App::I->ui_task_async([=] {
|
||||
int active = GET_WM_ACTIVATE_STATE(wp, lp);
|
||||
WacomTablet::I.set_focus(active);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "node_border.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "shader.h"
|
||||
|
||||
Plane NodeBorder::m_plane;
|
||||
@@ -62,21 +63,22 @@ void NodeBorder::draw()
|
||||
{
|
||||
ShaderManager::use(kShader::Color);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp);
|
||||
const auto blend_state = pp::renderer::gl::blend_state();
|
||||
|
||||
if (m_color.a > 0.f)
|
||||
{
|
||||
m_color.a < 1.f ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
||||
m_color.a < 1.f ? glEnable(blend_state) : glDisable(blend_state);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, m_color);
|
||||
m_plane.draw_fill();
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(blend_state);
|
||||
}
|
||||
|
||||
if (m_thinkness > 0 && m_border_color.a > 0.f)
|
||||
{
|
||||
//glLineWidth(m_thinkness);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, m_border_color);
|
||||
m_border_color.a < 1.f ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
||||
m_border_color.a < 1.f ? glEnable(blend_state) : glDisable(blend_state);
|
||||
m_plane.draw_stroke();
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(blend_state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,85 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "app_core/canvas_tool_ui.h"
|
||||
#include "app_core/history_ui.h"
|
||||
#include "app.h"
|
||||
#include "log.h"
|
||||
#include "node_canvas.h"
|
||||
#include "node_image_texture.h"
|
||||
#include "settings.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
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 run_history_undo_if_available()
|
||||
{
|
||||
const auto plan = pp::app::plan_history_undo(static_cast<int>(ActionManager::I.m_actions.size()));
|
||||
if (plan && plan.value().invokes_undo)
|
||||
ActionManager::undo();
|
||||
}
|
||||
|
||||
void run_history_redo_if_available()
|
||||
{
|
||||
const auto plan = pp::app::plan_history_redo(static_cast<int>(ActionManager::I.m_redos.size()));
|
||||
if (plan && plan.value().invokes_redo)
|
||||
ActionManager::redo();
|
||||
}
|
||||
|
||||
class LegacyNodeCanvasToolServices final : public pp::app::CanvasToolServices {
|
||||
public:
|
||||
void select_toolbar_button(pp::app::CanvasToolMode) override
|
||||
{
|
||||
}
|
||||
|
||||
void set_transform_action(pp::app::CanvasToolTransformAction) override
|
||||
{
|
||||
}
|
||||
|
||||
void set_canvas_mode(pp::app::CanvasToolMode mode) override
|
||||
{
|
||||
switch (mode) {
|
||||
case pp::app::CanvasToolMode::draw:
|
||||
Canvas::set_mode(kCanvasMode::Draw);
|
||||
return;
|
||||
case pp::app::CanvasToolMode::erase:
|
||||
Canvas::set_mode(kCanvasMode::Erase);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void toggle_picking() override
|
||||
{
|
||||
}
|
||||
|
||||
void toggle_touch_lock() override
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
void run_canvas_tool_mode(pp::app::CanvasToolMode mode)
|
||||
{
|
||||
const auto plan = pp::app::plan_canvas_tool_select(mode);
|
||||
LegacyNodeCanvasToolServices services;
|
||||
const auto status = pp::app::execute_canvas_tool_plan(plan, services);
|
||||
if (!status.ok())
|
||||
LOG("Canvas input tool action failed: %s", status.message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Node* NodeCanvas::clone_instantiate() const
|
||||
{
|
||||
@@ -22,12 +98,14 @@ void NodeCanvas::init()
|
||||
m_canvas->m_node = this;
|
||||
|
||||
m_sampler.create();
|
||||
//m_sampler.set_filter(GL_LINEAR, GL_NEAREST);
|
||||
//m_sampler.set_filter(pp::renderer::gl::linear_texture_filter(), pp::renderer::gl::nearest_texture_filter());
|
||||
|
||||
m_sampler_nearest.create(GL_NEAREST);
|
||||
m_sampler_nearest.create(pp::renderer::gl::nearest_texture_filter());
|
||||
|
||||
m_sampler_linear.create(GL_LINEAR);
|
||||
m_sampler_stencil.create(GL_LINEAR, GL_REPEAT);
|
||||
m_sampler_linear.create(pp::renderer::gl::linear_texture_filter());
|
||||
m_sampler_stencil.create(
|
||||
pp::renderer::gl::linear_texture_filter(),
|
||||
pp::renderer::gl::repeat_texture_wrap());
|
||||
m_face_plane.create<1>(2, 2);
|
||||
m_line.create();
|
||||
CanvasMode::node = this;
|
||||
@@ -45,7 +123,9 @@ void NodeCanvas::restore_context()
|
||||
|
||||
m_sampler.create();
|
||||
|
||||
m_sampler.set_filter(GL_LINEAR, GL_NEAREST);
|
||||
m_sampler.set_filter(
|
||||
pp::renderer::gl::linear_texture_filter(),
|
||||
pp::renderer::gl::nearest_texture_filter());
|
||||
m_face_plane.create<1>(2, 2);
|
||||
m_canvas->snapshot_restore();
|
||||
CanvasMode::node = this;
|
||||
@@ -74,13 +154,13 @@ void NodeCanvas::draw()
|
||||
|
||||
GLint vp[4];
|
||||
GLfloat cc[4];
|
||||
glGetIntegerv(GL_VIEWPORT, vp);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
||||
auto blend = glIsEnabled(GL_BLEND);
|
||||
auto depth = glIsEnabled(GL_DEPTH_TEST);
|
||||
auto scissor = glIsEnabled(GL_SCISSOR_TEST);
|
||||
glGetIntegerv(pp::renderer::gl::viewport_query(), vp);
|
||||
glGetFloatv(pp::renderer::gl::color_clear_value_query(), cc);
|
||||
auto blend = glIsEnabled(pp::renderer::gl::blend_state());
|
||||
auto depth = glIsEnabled(pp::renderer::gl::depth_test_state());
|
||||
auto scissor = glIsEnabled(pp::renderer::gl::scissor_test_state());
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(pp::renderer::gl::scissor_test_state());
|
||||
|
||||
glm::ivec4 c = (glm::ivec4)glm::vec4(box.x, (int)(vp[3] - box.y - box.w), box.z, box.w);
|
||||
|
||||
@@ -120,13 +200,13 @@ void NodeCanvas::draw()
|
||||
{
|
||||
m_rtt.bindFramebuffer();
|
||||
glClearColor(1, 1, 0, 0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
|
||||
glViewport(0, 0, m_rtt.getWidth(), m_rtt.getHeight());
|
||||
}
|
||||
else
|
||||
{
|
||||
glClearColor(1, 1, 1, 0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
|
||||
glViewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
|
||||
}
|
||||
|
||||
@@ -136,7 +216,7 @@ void NodeCanvas::draw()
|
||||
|
||||
if (draw_merged)
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
// draw the grid
|
||||
for (int plane_index = 0; plane_index < 6; plane_index++)
|
||||
{
|
||||
@@ -164,7 +244,7 @@ void NodeCanvas::draw()
|
||||
ShaderManager::u_int(kShaderUniform::Highlight, false);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_canvas->m_layers_merge.rtt(plane_index).bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
m_canvas->m_layers_merge.rtt(plane_index).unbindTexture();
|
||||
@@ -205,8 +285,8 @@ void NodeCanvas::draw()
|
||||
}
|
||||
|
||||
// if not using shader blend, use gl rasterizer blend
|
||||
use_blend ? glDisable(GL_BLEND) : glEnable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
use_blend ? glDisable(pp::renderer::gl::blend_state()) : glEnable(pp::renderer::gl::blend_state());
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
|
||||
const auto& b = m_canvas->m_current_stroke->m_brush;
|
||||
|
||||
@@ -254,23 +334,23 @@ void NodeCanvas::draw()
|
||||
//ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
|
||||
ShaderManager::u_int(kShaderUniform::Mask, m_canvas->m_smask_active);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_canvas->m_tmp[plane_index].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_canvas->m_smask.rtt(plane_index).bindTexture();
|
||||
for (int frame = frame_start; frame <= frame_end; frame++)
|
||||
{
|
||||
float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity* onion_alpha);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_canvas->m_smask.rtt(plane_index).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_canvas->m_tmp[plane_index].unbindTexture();
|
||||
}
|
||||
else if(m_canvas->m_current_stroke && m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index)
|
||||
@@ -309,33 +389,33 @@ void NodeCanvas::draw()
|
||||
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
|
||||
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
|
||||
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_canvas->m_tmp[plane_index].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_canvas->m_smask.rtt(plane_index).bindTexture();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
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();
|
||||
for (int frame = frame_start; frame <= frame_end; frame++)
|
||||
{
|
||||
float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
m_canvas->m_tmp_dual[plane_index].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_canvas->m_smask.rtt(plane_index).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
m_canvas->m_tmp[plane_index].unbindTexture();
|
||||
}
|
||||
else
|
||||
@@ -350,7 +430,7 @@ void NodeCanvas::draw()
|
||||
{
|
||||
float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
|
||||
@@ -376,13 +456,13 @@ void NodeCanvas::draw()
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-1, 1, -1, 1));
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_blender_rtt.bindTexture();
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_blender_bg.bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0,
|
||||
glCopyTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0, 0, 0, 0, 0,
|
||||
m_blender_bg.size().x, m_blender_bg.size().y);
|
||||
}
|
||||
|
||||
@@ -390,10 +470,10 @@ void NodeCanvas::draw()
|
||||
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
m_blender_bg.unbind();
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_blender_rtt.unbindTexture();
|
||||
}
|
||||
|
||||
@@ -428,7 +508,7 @@ void NodeCanvas::draw()
|
||||
// draw the grid behind the layers using a temporary copy
|
||||
if (use_blend)
|
||||
{
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
|
||||
//draw the grid
|
||||
for (int plane_index = 0; plane_index < 6; plane_index++)
|
||||
@@ -446,7 +526,7 @@ void NodeCanvas::draw()
|
||||
|
||||
// draw the layers
|
||||
m_sampler.bind(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_cache_rtt.bindTexture();
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
@@ -456,7 +536,7 @@ void NodeCanvas::draw()
|
||||
}
|
||||
}
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
|
||||
if (m_canvas->m_smask_active || m_canvas->m_current_mode == kCanvasMode::Copy || m_canvas->m_current_mode == kCanvasMode::Cut)
|
||||
{
|
||||
@@ -471,8 +551,8 @@ void NodeCanvas::draw()
|
||||
ShaderManager::use(kShader::TextureMask);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_outline_pan);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glEnable(GL_BLEND);
|
||||
set_active_texture_unit(0);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
|
||||
//draw the cube faces
|
||||
for (int plane_index = 0; plane_index < 6; plane_index++)
|
||||
@@ -506,12 +586,12 @@ void NodeCanvas::draw()
|
||||
m_rtt.unbindFramebuffer();
|
||||
|
||||
glClearColor(1, 1, 1, 0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
|
||||
glViewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
|
||||
|
||||
// draw the canvas
|
||||
m_sampler_nearest.bind(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
m_rtt.bindTexture();
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
@@ -520,9 +600,9 @@ void NodeCanvas::draw()
|
||||
m_rtt.unbindTexture();
|
||||
}
|
||||
|
||||
scissor ? glEnable(GL_SCISSOR_TEST) : glDisable(GL_SCISSOR_TEST);
|
||||
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
||||
depth ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
|
||||
scissor ? glEnable(pp::renderer::gl::scissor_test_state()) : glDisable(pp::renderer::gl::scissor_test_state());
|
||||
blend ? glEnable(pp::renderer::gl::blend_state()) : glDisable(pp::renderer::gl::blend_state());
|
||||
depth ? glEnable(pp::renderer::gl::depth_test_state()) : glDisable(pp::renderer::gl::depth_test_state());
|
||||
glViewport(vp[0], vp[1], vp[2], vp[3]);
|
||||
glClearColor(cc[0], cc[1], cc[2], cc[3]);
|
||||
}
|
||||
@@ -551,9 +631,9 @@ kEventResult NodeCanvas::handle_event(Event* e)
|
||||
case kEventType::MouseMove:
|
||||
if (stylus_eraser != me->m_eraser)
|
||||
{
|
||||
Canvas::set_mode(me->m_eraser ?
|
||||
kCanvasMode::Erase :
|
||||
kCanvasMode::Draw);
|
||||
run_canvas_tool_mode(me->m_eraser ?
|
||||
pp::app::CanvasToolMode::erase :
|
||||
pp::app::CanvasToolMode::draw);
|
||||
stylus_eraser = me->m_eraser;
|
||||
}
|
||||
case kEventType::MouseScroll:
|
||||
@@ -576,10 +656,9 @@ kEventResult NodeCanvas::handle_event(Event* e)
|
||||
break;
|
||||
case kEventType::KeyDown:
|
||||
if (ke->m_key == kKey::KeyE)
|
||||
Canvas::set_mode(kCanvasMode::Erase);
|
||||
run_canvas_tool_mode(pp::app::CanvasToolMode::erase);
|
||||
if (ke->m_key == kKey::AndroidBack)
|
||||
if (!ActionManager::empty())
|
||||
ActionManager::undo();
|
||||
run_history_undo_if_available();
|
||||
if (ke->m_key == kKey::KeyAlt && m_mouse_focus)
|
||||
App::I->show_cursor();
|
||||
for (auto& mode : *m_canvas->m_mode)
|
||||
@@ -588,32 +667,18 @@ kEventResult NodeCanvas::handle_event(Event* e)
|
||||
case kEventType::KeyUp:
|
||||
update_cursor();
|
||||
if (ke->m_key == kKey::KeyE)
|
||||
Canvas::set_mode(kCanvasMode::Draw);
|
||||
run_canvas_tool_mode(pp::app::CanvasToolMode::draw);
|
||||
if (ke->m_key == kKey::KeyTab)
|
||||
App::I->toggle_ui();
|
||||
if (ke->m_key == kKey::KeyZ && App::I->keys[(int)kKey::KeyCtrl])
|
||||
App::I->keys[(int)kKey::KeyShift] ? ActionManager::redo() : ActionManager::undo();
|
||||
App::I->keys[(int)kKey::KeyShift] ? run_history_redo_if_available() : run_history_undo_if_available();
|
||||
if (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && !App::I->keys[(int)kKey::KeyShift])
|
||||
{
|
||||
if (Canvas::I->m_newdoc)
|
||||
{
|
||||
App::I->dialog_save();
|
||||
}
|
||||
else if (Canvas::I->m_unsaved)
|
||||
{
|
||||
Canvas::I->project_save();
|
||||
}
|
||||
App::I->save_document(pp::app::DocumentSaveIntent::save);
|
||||
}
|
||||
if (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && App::I->keys[(int)kKey::KeyShift])
|
||||
{
|
||||
if (Canvas::I->m_newdoc)
|
||||
{
|
||||
App::I->dialog_save();
|
||||
}
|
||||
else if (Canvas::I->m_unsaved)
|
||||
{
|
||||
App::I->dialog_save_ver();
|
||||
}
|
||||
App::I->save_document(pp::app::DocumentSaveIntent::save_dirty_version);
|
||||
}
|
||||
if (ke->m_key == kKey::KeyBracketLeft)
|
||||
{
|
||||
@@ -646,7 +711,7 @@ kEventResult NodeCanvas::handle_event(Event* e)
|
||||
break;
|
||||
case kEventType::TouchTap:
|
||||
if (te->m_finger_count == 2)
|
||||
ActionManager::undo();
|
||||
run_history_undo_if_available();
|
||||
break;
|
||||
default:
|
||||
return kEventResult::Available;
|
||||
@@ -668,11 +733,11 @@ void NodeCanvas::create_buffers()
|
||||
auto new_size = GetSize() * m_density;
|
||||
LOG("NodeCanvas::create_buffers size: %d x %d density %f", (int)new_size.x, (int)new_size.y, m_density);
|
||||
m_canvas->m_mixer.create((int)new_size.x * m_canvas->m_mixer_scale,
|
||||
(int)new_size.y * m_canvas->m_mixer_scale, -1, GL_RGBA8);
|
||||
m_blender_rtt.create((int)new_size.x, (int)new_size.y, -1, GL_RGBA8);
|
||||
m_cache_rtt.create((int)new_size.x, (int)new_size.y, -1, GL_RGBA8);
|
||||
m_rtt.create((int)new_size.x, (int)new_size.y, -1, GL_RGBA8, true);
|
||||
m_blender_bg.create((int)new_size.x, (int)new_size.y, GL_RGBA8);
|
||||
(int)new_size.y * m_canvas->m_mixer_scale, -1, pp::renderer::gl::rgba8_internal_format());
|
||||
m_blender_rtt.create((int)new_size.x, (int)new_size.y, -1, pp::renderer::gl::rgba8_internal_format());
|
||||
m_cache_rtt.create((int)new_size.x, (int)new_size.y, -1, pp::renderer::gl::rgba8_internal_format());
|
||||
m_rtt.create((int)new_size.x, (int)new_size.y, -1, pp::renderer::gl::rgba8_internal_format(), true);
|
||||
m_blender_bg.create((int)new_size.x, (int)new_size.y, pp::renderer::gl::rgba8_internal_format());
|
||||
}
|
||||
|
||||
void NodeCanvas::set_density(float d)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "pch.h"
|
||||
#include "node_colorwheel.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "shader.h"
|
||||
#include "log.h"
|
||||
#include "app.h"
|
||||
@@ -29,43 +30,67 @@ void NodeColorWheel::init_controls()
|
||||
|
||||
void NodeColorWheel::loaded()
|
||||
{
|
||||
m_circle.create<64>(.5, .4, Circle::kUVMapping::Tube);
|
||||
m_cur_hue.create<16>(.05, 0.04);
|
||||
m_cur_quad.create<16>(.04, 0.03, Circle::kUVMapping::Tube);
|
||||
m_circle.create<64>(.5f, .4f, Circle::kUVMapping::Tube);
|
||||
m_cur_hue.create<16>(.05f, 0.04f);
|
||||
m_cur_quad.create<16>(.04f, 0.03f, Circle::kUVMapping::Tube);
|
||||
|
||||
float quad_scale = glm::sin(glm::radians(45.f)) * 0.8f;
|
||||
m_quad.create<1>(quad_scale, quad_scale);
|
||||
|
||||
struct vertex_t { glm::vec4 pos; glm::vec2 uvs; glm::vec4 col; };
|
||||
std::vector<vertex_t> vertices;
|
||||
float l = 0.4;
|
||||
float l = 0.4f;
|
||||
vertices.push_back({{glm::cos(4.f/3.f*glm::pi<float>())*l,glm::sin(4.f/3.f*glm::pi<float>())*l,0,1},{1,-1},{1,1,1,1}});
|
||||
vertices.push_back({{glm::cos(2.f/3.f*glm::pi<float>())*l,glm::sin(2.f/3.f*glm::pi<float>())*l,0,1},{0,0},{0,0,0,1}});
|
||||
vertices.push_back({{l,0,0,1},{1,1},{1,0,0,1}});
|
||||
|
||||
App::I->render_task([&]
|
||||
{
|
||||
const auto buffer_target = pp::renderer::gl::array_buffer_target();
|
||||
const auto upload_usage = pp::renderer::gl::static_draw_buffer_usage();
|
||||
const auto attribute_type = pp::renderer::gl::vertex_attribute_float_component_type();
|
||||
const auto attribute_normalized =
|
||||
static_cast<GLboolean>(pp::renderer::gl::vertex_attribute_not_normalized());
|
||||
|
||||
glGenBuffers(1, &buffers);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, buffers);
|
||||
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(vertex_t), vertices.data(), GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(buffer_target, buffers);
|
||||
glBufferData(buffer_target, vertices.size() * sizeof(vertex_t), vertices.data(), upload_usage);
|
||||
glBindBuffer(buffer_target, 0);
|
||||
|
||||
glGenVertexArrays(1, &arrays);
|
||||
glBindVertexArray(arrays);
|
||||
glEnableVertexAttribArray(0);
|
||||
glEnableVertexAttribArray(1);
|
||||
glEnableVertexAttribArray(2);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, buffers);
|
||||
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (GLvoid*)0);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (GLvoid*)offsetof(vertex_t, uvs));
|
||||
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (GLvoid*)offsetof(vertex_t, col));
|
||||
glBindBuffer(buffer_target, buffers);
|
||||
glVertexAttribPointer(
|
||||
0,
|
||||
4,
|
||||
attribute_type,
|
||||
attribute_normalized,
|
||||
sizeof(vertex_t),
|
||||
(GLvoid*)0);
|
||||
glVertexAttribPointer(
|
||||
1,
|
||||
2,
|
||||
attribute_type,
|
||||
attribute_normalized,
|
||||
sizeof(vertex_t),
|
||||
(GLvoid*)offsetof(vertex_t, uvs));
|
||||
glVertexAttribPointer(
|
||||
2,
|
||||
4,
|
||||
attribute_type,
|
||||
attribute_normalized,
|
||||
sizeof(vertex_t),
|
||||
(GLvoid*)offsetof(vertex_t, col));
|
||||
glBindVertexArray(0);
|
||||
});
|
||||
}
|
||||
|
||||
void NodeColorWheel::draw()
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
ShaderManager::use(kShader::ColorHue);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp * glm::eulerAngleZ(glm::radians(-90.f)));
|
||||
ShaderManager::u_int(kShaderUniform::Direction, 0); // set horizontal
|
||||
@@ -74,9 +99,8 @@ void NodeColorWheel::draw()
|
||||
// ShaderManager::use(kShader::ColorTri);
|
||||
// ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp);
|
||||
// ShaderManager::u_vec4(kShaderUniform::Col, glm::vec4(m_hsv, 0.f));
|
||||
// GLenum type = GL_TRIANGLES;
|
||||
// glBindVertexArray(arrays);
|
||||
// glDrawArrays(type, 0, 3);
|
||||
// glDrawArrays(pp::renderer::gl::primitive_mode_for_fill_count(3U), 0, 3);
|
||||
// glBindVertexArray(0);
|
||||
|
||||
ShaderManager::use(kShader::Color);
|
||||
@@ -144,8 +168,8 @@ kEventResult NodeColorWheel::handle_event(Event* e)
|
||||
else if (l >= 0.4f && l <= 0.5f)
|
||||
{
|
||||
mode = 1;
|
||||
auto pos = glm::normalize(me->m_pos - m_pos - GetSize() * 0.5f);
|
||||
m_hsv.x = (glm::atan(pos.y, -pos.x) + glm::pi<float>()) / glm::two_pi<float>();
|
||||
auto normalized_pos = glm::normalize(me->m_pos - m_pos - GetSize() * 0.5f);
|
||||
m_hsv.x = (glm::atan(normalized_pos.y, -normalized_pos.x) + glm::pi<float>()) / glm::two_pi<float>();
|
||||
handle_color_change();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "node_dialog_resize.h"
|
||||
#include "app_core/document_resize.h"
|
||||
#include "canvas.h"
|
||||
#include "node_image_texture.h"
|
||||
#include "app.h"
|
||||
#include <array>
|
||||
|
||||
Node* NodeDialogResize::clone_instantiate() const
|
||||
@@ -30,9 +30,12 @@ void NodeDialogResize::init_controls()
|
||||
combo = find<NodeComboBox>("resolution");
|
||||
text = find<NodeText>("current-res");
|
||||
resolution = Canvas::I->m_width;
|
||||
static char txt[128];
|
||||
sprintf(txt, "Current: %s", App::I->res_to_string(resolution).c_str());
|
||||
text->set_text(txt);
|
||||
const auto state = pp::app::make_document_resize_dialog_state(resolution);
|
||||
text->set_text(state.current_resolution_text.c_str());
|
||||
if (combo && state.current_resolution_index >= 0
|
||||
&& state.current_resolution_index < static_cast<int>(combo->m_items.size())) {
|
||||
combo->m_current_index = state.current_resolution_index;
|
||||
}
|
||||
btn_cancel->on_click = [this](Node*) {
|
||||
destroy();
|
||||
};
|
||||
@@ -47,5 +50,6 @@ void NodeDialogResize::loaded()
|
||||
|
||||
int NodeDialogResize::get_resolution()
|
||||
{
|
||||
return combo ? App::I->res_from_index(combo->m_current_index) : 512;
|
||||
const auto plan = pp::app::plan_document_resize(combo ? combo->m_current_index : 0);
|
||||
return plan ? plan.value().resolution : pp::app::document_resolution_values.front();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "node_image.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "shader.h"
|
||||
#include "app.h"
|
||||
|
||||
@@ -13,7 +14,9 @@ void NodeImage::static_init()
|
||||
m_plane.create<1>(1, 1);
|
||||
m_sampler.create();
|
||||
m_sampler_mips.create();
|
||||
m_sampler_mips.set_filter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
|
||||
m_sampler_mips.set_filter(
|
||||
pp::renderer::gl::linear_mipmap_linear_texture_filter(),
|
||||
pp::renderer::gl::linear_texture_filter());
|
||||
}
|
||||
|
||||
Node* NodeImage::clone_instantiate() const
|
||||
@@ -99,7 +102,7 @@ void NodeImage::draw()
|
||||
auto& sampler = m_use_mipmaps ? m_sampler_mips : m_sampler;
|
||||
sampler.bind(0);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
if (m_use_atlas)
|
||||
{
|
||||
ShaderManager::use(kShader::Atlas);
|
||||
@@ -114,7 +117,7 @@ void NodeImage::draw()
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp * glm::scale(glm::vec3(m_scale, 1.f)));
|
||||
m_plane.draw_fill();
|
||||
sampler.unbind();
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
}
|
||||
|
||||
bool NodeImage::set_image(const std::string& path)
|
||||
@@ -155,7 +158,12 @@ void NodeImage::load_url(const std::string& url)
|
||||
int w, h, c;
|
||||
uint8_t* rgba = stbi_load_from_memory(m_remote_asset->m_data, m_remote_asset->m_len, &w, &h, &c, 4);
|
||||
m_remote_texture = std::make_shared<Texture2D>();
|
||||
m_remote_texture->create(w, h, GL_RGBA8, GL_RGBA, rgba);
|
||||
m_remote_texture->create(
|
||||
w,
|
||||
h,
|
||||
pp::renderer::gl::rgba8_internal_format(),
|
||||
pp::renderer::gl::rgba_pixel_format(),
|
||||
rgba);
|
||||
if (m_use_mipmaps)
|
||||
m_remote_texture->create_mipmaps();
|
||||
delete rgba;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "node_image_texture.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "shader.h"
|
||||
#include "node_image.h"
|
||||
|
||||
@@ -18,14 +19,14 @@ void NodeImageTexture::clone_copy(Node* dest) const
|
||||
|
||||
void NodeImageTexture::draw()
|
||||
{
|
||||
tex ? tex->bind() : glBindTexture(GL_TEXTURE_2D, 0);
|
||||
tex ? tex->bind() : glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
|
||||
auto& sampler = tex && tex->has_mips ? NodeImage::m_sampler_mips : NodeImage::m_sampler;
|
||||
sampler.bind(0);
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp);
|
||||
NodeImage::m_plane.draw_fill();
|
||||
sampler.unbind();
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "pch.h"
|
||||
#include "node_panel_animation.h"
|
||||
#include "app_core/document_animation.h"
|
||||
#include "node_button.h"
|
||||
#include "node_button_custom.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "canvas.h"
|
||||
#include "app.h"
|
||||
|
||||
@@ -24,6 +26,152 @@ void NodePanelAnimation::init()
|
||||
init_controls();
|
||||
}
|
||||
|
||||
void NodePanelAnimation::execute_animation_plan(const pp::app::DocumentAnimationOperationPlan& plan, Layer* layer)
|
||||
{
|
||||
class LegacyAnimationServices final : public pp::app::DocumentAnimationServices {
|
||||
public:
|
||||
LegacyAnimationServices(NodePanelAnimation& panel, Layer* layer) noexcept
|
||||
: panel_(panel)
|
||||
, layer_(layer)
|
||||
{
|
||||
}
|
||||
|
||||
void add_frame() override
|
||||
{
|
||||
Canvas::I->layer().add_frame();
|
||||
}
|
||||
|
||||
void duplicate_frame(int selected_frame) override
|
||||
{
|
||||
if (layer_)
|
||||
layer_->duplicate_frame(selected_frame);
|
||||
}
|
||||
|
||||
void remove_frame(int selected_frame, int target_frame) override
|
||||
{
|
||||
if (!layer_)
|
||||
return;
|
||||
layer_->remove_frame(selected_frame);
|
||||
panel_.m_selected_frame_index = target_frame;
|
||||
}
|
||||
|
||||
void set_frame_duration(int selected_frame, int duration) override
|
||||
{
|
||||
if (layer_)
|
||||
layer_->set_frame_duration(selected_frame, duration);
|
||||
}
|
||||
|
||||
int move_frame(int selected_frame, int move_offset) override
|
||||
{
|
||||
if (!layer_)
|
||||
return selected_frame;
|
||||
panel_.m_selected_frame_index = layer_->move_frame_offset(selected_frame, move_offset);
|
||||
return panel_.m_selected_frame_index;
|
||||
}
|
||||
|
||||
void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) override
|
||||
{
|
||||
panel_.m_selected_frame_layer_id = layer_id;
|
||||
panel_.m_selected_frame_index = selected_frame;
|
||||
panel_.m_timeline->m_frame = selected_frame;
|
||||
}
|
||||
|
||||
void select_layer(int layer_index) override
|
||||
{
|
||||
App::I->layers->handle_layer_selected(App::I->layers->get_layer_at(layer_index));
|
||||
}
|
||||
|
||||
void goto_frame(int target_frame) override
|
||||
{
|
||||
Canvas::I->anim_goto_frame(target_frame);
|
||||
}
|
||||
|
||||
void set_timeline_frame(int target_frame) override
|
||||
{
|
||||
panel_.m_timeline->m_frame = target_frame;
|
||||
}
|
||||
|
||||
void set_onion_size(int onion_size) override
|
||||
{
|
||||
panel_.m_timeline->m_onion_size = onion_size;
|
||||
}
|
||||
|
||||
void capture_playback_restore_mode() override
|
||||
{
|
||||
playback_restore_mode() = Canvas::I->m_current_mode;
|
||||
}
|
||||
|
||||
void enter_playback_camera_mode() override
|
||||
{
|
||||
Canvas::set_mode(kCanvasMode::Camera);
|
||||
}
|
||||
|
||||
void restore_playback_canvas_mode() override
|
||||
{
|
||||
Canvas::set_mode(playback_restore_mode());
|
||||
}
|
||||
|
||||
void set_playback_active(bool active) override
|
||||
{
|
||||
panel_.btn_play->set_active(active);
|
||||
}
|
||||
|
||||
void reset_playback_timer() override
|
||||
{
|
||||
panel_.m_playback_timer = 0;
|
||||
}
|
||||
|
||||
void set_playback_idle_ms(int idle_ms) override
|
||||
{
|
||||
App::I->idle_ms = idle_ms;
|
||||
}
|
||||
|
||||
void update_canvas_animation() override
|
||||
{
|
||||
Canvas::I->anim_update();
|
||||
}
|
||||
|
||||
void update_frame_status() override
|
||||
{
|
||||
panel_.update_frames();
|
||||
}
|
||||
|
||||
void reload_animation_layers() override
|
||||
{
|
||||
panel_.load_layers();
|
||||
}
|
||||
|
||||
void mark_unsaved() override
|
||||
{
|
||||
Canvas::I->m_unsaved = true;
|
||||
}
|
||||
|
||||
private:
|
||||
static kCanvasMode& playback_restore_mode()
|
||||
{
|
||||
static auto mode = Canvas::I->m_current_mode;
|
||||
return mode;
|
||||
}
|
||||
|
||||
NodePanelAnimation& panel_;
|
||||
Layer* layer_ = nullptr;
|
||||
};
|
||||
|
||||
LegacyAnimationServices services(*this, layer);
|
||||
const auto status = pp::app::execute_animation_operation_plan(plan, services);
|
||||
if (!status.ok())
|
||||
LOG("Animation panel action failed: %s", status.message);
|
||||
}
|
||||
|
||||
pp::app::DocumentAnimationPanelState NodePanelAnimation::animation_panel_state() const
|
||||
{
|
||||
return pp::app::DocumentAnimationPanelState {
|
||||
.total_duration = Canvas::I->anim_duration(),
|
||||
.current_frame = Canvas::I->m_anim_frame,
|
||||
.playback_active = btn_play->is_active(),
|
||||
};
|
||||
}
|
||||
|
||||
void NodePanelAnimation::init_controls()
|
||||
{
|
||||
m_layers_container = find<NodeScroll>("layers");
|
||||
@@ -44,80 +192,130 @@ void NodePanelAnimation::init_controls()
|
||||
m_frame_label = find<NodeText>("frame-index");
|
||||
|
||||
btn_add->on_click = [this](Node*) {
|
||||
Canvas::I->layer().add_frame();
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_add_frame(
|
||||
Canvas::I->layer().frames_count(),
|
||||
Canvas::I->m_anim_frame);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value());
|
||||
};
|
||||
btn_duplicate->on_click = [this](Node*) {
|
||||
Canvas::I->layer().duplicate_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_duplicate_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value(), layer.get());
|
||||
}
|
||||
};
|
||||
btn_remove->on_click = [this](Node*) {
|
||||
Canvas::I->layer_with_id(m_selected_frame_layer_id)->remove_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_remove_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value(), layer.get());
|
||||
}
|
||||
};
|
||||
btn_up->on_click = [this](Node*) {
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
layer->set_frame_duration(m_selected_frame_index, glm::max(layer->frame_duration(m_selected_frame_index) + 1, 1));
|
||||
load_layers();
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_adjust_duration(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
layer->frame_duration(m_selected_frame_index),
|
||||
1);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value(), layer.get());
|
||||
}
|
||||
};
|
||||
btn_down->on_click = [this](Node*) {
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
layer->set_frame_duration(m_selected_frame_index, glm::max(layer->frame_duration(m_selected_frame_index) - 1, 1));
|
||||
load_layers();
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_adjust_duration(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
layer->frame_duration(m_selected_frame_index),
|
||||
-1);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value(), layer.get());
|
||||
}
|
||||
};
|
||||
btn_left->on_click = [this](Node*) {
|
||||
if (!m_selected_frame)
|
||||
return;
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
m_selected_frame_index = layer->move_frame_offset(m_selected_frame_index, -1);
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_move_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
-1);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value(), layer.get());
|
||||
}
|
||||
};
|
||||
btn_right->on_click = [this](Node*) {
|
||||
if (!m_selected_frame)
|
||||
return;
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
m_selected_frame_index = layer->move_frame_offset(m_selected_frame_index, +1);
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_move_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
1);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value(), layer.get());
|
||||
}
|
||||
};
|
||||
|
||||
m_onion->on_select = [this] (Node* target, int index) {
|
||||
m_timeline->m_onion_size = m_onion->get_int();
|
||||
Canvas::I->anim_update();
|
||||
const auto plan = pp::app::plan_animation_onion_size(m_onion->get_int());
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value());
|
||||
};
|
||||
|
||||
m_timeline->on_frame_changed = [this] (NodeAnimationTimeline* target, int frame) {
|
||||
LOG("goto frame %d", frame);
|
||||
Canvas::I->anim_goto_frame(frame);
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_panel_action(
|
||||
pp::app::DocumentAnimationPanelAction::goto_frame,
|
||||
animation_panel_state(),
|
||||
frame);
|
||||
if (!plan)
|
||||
return;
|
||||
LOG("goto frame %d", plan.value().target_frame);
|
||||
execute_animation_plan(plan.value());
|
||||
};
|
||||
|
||||
btn_next->on_click = [this] (Node* target) {
|
||||
Canvas::I->anim_goto_next();
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_panel_action(
|
||||
pp::app::DocumentAnimationPanelAction::next_frame,
|
||||
animation_panel_state());
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value());
|
||||
};
|
||||
btn_prev->on_click = [this](Node* target) {
|
||||
Canvas::I->anim_goto_prev();
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_panel_action(
|
||||
pp::app::DocumentAnimationPanelAction::previous_frame,
|
||||
animation_panel_state());
|
||||
if (!plan)
|
||||
return;
|
||||
execute_animation_plan(plan.value());
|
||||
};
|
||||
btn_play->on_click = [this] (Node* target) {
|
||||
static auto mode = Canvas::I->m_current_mode;
|
||||
auto b = static_cast<NodeButtonCustom*>(target);
|
||||
if (b->is_active())
|
||||
{
|
||||
Canvas::set_mode(mode);
|
||||
b->set_active(false);
|
||||
App::I->idle_ms = 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
mode = Canvas::I->m_current_mode;
|
||||
Canvas::set_mode(kCanvasMode::Camera);
|
||||
m_playback_timer = 0;
|
||||
b->set_active(true);
|
||||
App::I->idle_ms = 10;
|
||||
}
|
||||
btn_play->on_click = [this] (Node*) {
|
||||
const auto plan = pp::app::plan_animation_panel_action(
|
||||
pp::app::DocumentAnimationPanelAction::toggle_playback,
|
||||
animation_panel_state());
|
||||
if (plan)
|
||||
execute_animation_plan(plan.value());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -153,11 +351,13 @@ void NodePanelAnimation::load_layers()
|
||||
m_selected_frame->set_active(false);
|
||||
frame->set_active(true);
|
||||
m_selected_frame = frame;
|
||||
m_selected_frame_layer_id = lid;
|
||||
m_selected_frame_index = fi;
|
||||
m_timeline->m_frame = fi;
|
||||
Canvas::I->anim_goto_frame(fi);
|
||||
App::I->layers->handle_layer_selected(App::I->layers->get_layer_at(i));
|
||||
const auto plan = pp::app::plan_animation_select_frame(
|
||||
Canvas::I->m_layers[i]->frames_count(),
|
||||
i,
|
||||
lid,
|
||||
fi);
|
||||
if (plan)
|
||||
execute_animation_plan(plan.value(), Canvas::I->m_layers[i].get());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -182,9 +382,11 @@ void NodePanelAnimation::on_tick(float dt)
|
||||
if (m_playback_timer > (1.f / m_fps->get_float()))
|
||||
{
|
||||
m_playback_timer = 0;
|
||||
Canvas::I->anim_goto_next();
|
||||
m_timeline->m_frame = Canvas::I->m_anim_frame;
|
||||
update_frames();
|
||||
const auto plan = pp::app::plan_animation_panel_action(
|
||||
pp::app::DocumentAnimationPanelAction::playback_step,
|
||||
animation_panel_state());
|
||||
if (plan)
|
||||
execute_animation_plan(plan.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,7 +438,7 @@ void NodeAnimationTimeline::draw()
|
||||
{
|
||||
parent::draw();
|
||||
ShaderManager::use(kShader::Color);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
|
||||
float step = 35.f;
|
||||
glm::vec2 cur_pos = {
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
#include "node_button_custom.h"
|
||||
#include "node_combobox.h"
|
||||
|
||||
class Layer;
|
||||
|
||||
namespace pp::app {
|
||||
struct DocumentAnimationPanelState;
|
||||
struct DocumentAnimationOperationPlan;
|
||||
}
|
||||
|
||||
class NodeAnimationFrame : public NodeButtonCustom
|
||||
{
|
||||
public:
|
||||
@@ -65,6 +72,9 @@ class NodePanelAnimation : public Node
|
||||
int m_selected_frame_index = -1;
|
||||
uint32_t m_selected_frame_layer_id = 0;
|
||||
float m_playback_timer = 0;
|
||||
|
||||
void execute_animation_plan(const pp::app::DocumentAnimationOperationPlan& plan, Layer* layer = nullptr);
|
||||
[[nodiscard]] pp::app::DocumentAnimationPanelState animation_panel_state() const;
|
||||
public:
|
||||
using this_class = NodePanelAnimation;
|
||||
using parent = Node;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "pch.h"
|
||||
#include "app_core/grid_ui.h"
|
||||
#include "log.h"
|
||||
#include "node_panel_grid.h"
|
||||
#include "canvas.h"
|
||||
#include "app.h"
|
||||
#include "image.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "util.h"
|
||||
|
||||
Node* NodePanelGrid::clone_instantiate() const
|
||||
@@ -76,28 +78,19 @@ void NodePanelGrid::init_controls()
|
||||
};
|
||||
|
||||
m_hm_load->on_click = [this](Node*) {
|
||||
const auto plan = pp::app::plan_grid_heightmap_pick();
|
||||
if (!plan.opens_picker)
|
||||
return;
|
||||
App::I->pick_image([this](std::string path) {
|
||||
Image img;
|
||||
if (img.load_file(path))
|
||||
{
|
||||
m_file_path = path;
|
||||
m_hm_image = img.resize(128, 128);
|
||||
m_hm_preview->tex = std::make_shared<Texture2D>();
|
||||
m_hm_preview->tex->create(m_hm_image);
|
||||
m_hm_preview->tex->create_mipmaps();
|
||||
auto sz = m_hm_preview->tex->size();
|
||||
m_hm_preview->SetAspectRatio(sz.x / sz.y);
|
||||
m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height());
|
||||
m_hm_preview->SetHeight(100);
|
||||
if (m_groud_opacity->get_value() == 0.f)
|
||||
m_groud_opacity->set_value(1.f);
|
||||
m_rt_dirty = true;
|
||||
}
|
||||
load_heightmap_file(path, true);
|
||||
});
|
||||
};
|
||||
|
||||
m_hm_clear->on_click = [this](Node*)
|
||||
{
|
||||
const auto plan = pp::app::plan_grid_heightmap_clear(static_cast<bool>(m_hm_image.data()));
|
||||
if (!plan.clears_heightmap)
|
||||
return;
|
||||
m_hm_plane.create(1, 1, 100 * get_resolution());
|
||||
m_hm_image.destroy();
|
||||
m_hm_preview->tex.reset();
|
||||
@@ -106,24 +99,26 @@ void NodePanelGrid::init_controls()
|
||||
|
||||
m_hm_reload->on_click = [this](Node*)
|
||||
{
|
||||
Image img;
|
||||
if (img.load_file(m_file_path))
|
||||
{
|
||||
m_hm_image = img.resize(128, 128);
|
||||
m_hm_preview->tex = std::make_shared<Texture2D>();
|
||||
m_hm_preview->tex->create(m_hm_image);
|
||||
m_hm_preview->tex->create_mipmaps();
|
||||
auto sz = m_hm_preview->tex->size();
|
||||
m_hm_preview->SetAspectRatio(sz.x / sz.y);
|
||||
m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height());
|
||||
m_hm_preview->SetHeight(100);
|
||||
m_rt_dirty = true;
|
||||
}
|
||||
load_heightmap_file(m_file_path, false);
|
||||
};
|
||||
|
||||
m_render->on_click = [this](Node*)
|
||||
{
|
||||
if (ShaderManager::ext_float32 || ShaderManager::ext_float16)
|
||||
const auto plan = pp::app::plan_grid_lightmap_render(
|
||||
static_cast<bool>(m_hm_image.data()),
|
||||
ShaderManager::ext_float32,
|
||||
ShaderManager::ext_float16,
|
||||
get_texres(),
|
||||
get_samples());
|
||||
if (!plan)
|
||||
return;
|
||||
if (plan.value().shows_unsupported_message)
|
||||
{
|
||||
App::I->message_box("Rendering failed",
|
||||
"Your hardware does not support lightmap rendering.");
|
||||
return;
|
||||
}
|
||||
if (plan.value().renders_lightmap)
|
||||
{
|
||||
std::thread([this] {
|
||||
BT_SetTerminate();
|
||||
@@ -132,17 +127,16 @@ void NodePanelGrid::init_controls()
|
||||
m_shade_mode = ShadeMode::Textured;
|
||||
}).detach();
|
||||
}
|
||||
else
|
||||
{
|
||||
App::I->message_box("Rendering failed",
|
||||
"Your hardware does not support lightmap rendering.");
|
||||
}
|
||||
};
|
||||
m_commit->on_click = [this](Node*)
|
||||
{
|
||||
const auto plan = pp::app::plan_grid_heightmap_commit(Canvas::I != nullptr);
|
||||
if (!plan.commits_heightmap)
|
||||
return;
|
||||
Canvas::I->draw_objects([this](const glm::mat4& camera, const glm::mat4& proj, int i) {
|
||||
draw_heightmap(proj, camera, true);
|
||||
}, Canvas::I->layer().m_frame_index, true);
|
||||
if (plan.updates_ground_opacity)
|
||||
m_groud_opacity->set_value(0);
|
||||
};
|
||||
m_hm_texres->on_select = [this](Node*, int index) {
|
||||
@@ -160,7 +154,12 @@ void NodePanelGrid::init_controls()
|
||||
App::I->render_task([&]
|
||||
{
|
||||
m_texture.bind();
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.m_data.get());
|
||||
glGetTexImage(
|
||||
pp::renderer::gl::texture_2d_target(),
|
||||
0,
|
||||
pp::renderer::gl::rgba_pixel_format(),
|
||||
pp::renderer::gl::unsigned_byte_component_type(),
|
||||
img.m_data.get());
|
||||
m_texture.unbind();
|
||||
});
|
||||
Image resized = img.resize(texres, texres);
|
||||
@@ -171,7 +170,9 @@ void NodePanelGrid::init_controls()
|
||||
int rexres = get_texres();
|
||||
m_texture.create(rexres, rexres);
|
||||
m_sampler_mipmap.create();
|
||||
m_sampler_mipmap.set_filter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
|
||||
m_sampler_mipmap.set_filter(
|
||||
pp::renderer::gl::linear_mipmap_linear_texture_filter(),
|
||||
pp::renderer::gl::linear_texture_filter());
|
||||
m_sampler_linear.create();
|
||||
m_plane.create<1>(1, 1);
|
||||
|
||||
@@ -218,14 +219,41 @@ float NodePanelGrid::get_offset() const
|
||||
return glm::pow(m_groud_offset->get_value() - 0.5f, 3);
|
||||
}
|
||||
|
||||
bool NodePanelGrid::load_heightmap_file(const std::string& path, bool raise_ground_opacity)
|
||||
{
|
||||
const auto plan = raise_ground_opacity
|
||||
? pp::app::plan_grid_heightmap_load(path)
|
||||
: pp::app::plan_grid_heightmap_reload(path);
|
||||
if (!plan)
|
||||
return false;
|
||||
|
||||
Image img;
|
||||
if (!img.load_file(plan.value().path))
|
||||
return false;
|
||||
|
||||
m_file_path = plan.value().path;
|
||||
m_hm_image = img.resize(128, 128);
|
||||
m_hm_preview->tex = std::make_shared<Texture2D>();
|
||||
m_hm_preview->tex->create(m_hm_image);
|
||||
m_hm_preview->tex->create_mipmaps();
|
||||
auto sz = m_hm_preview->tex->size();
|
||||
m_hm_preview->SetAspectRatio(sz.x / sz.y);
|
||||
m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height());
|
||||
m_hm_preview->SetHeight(100);
|
||||
if (plan.value().updates_ground_opacity && m_groud_opacity->get_value() == 0.f)
|
||||
m_groud_opacity->set_value(1.f);
|
||||
m_rt_dirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camera, bool commit) const
|
||||
{
|
||||
assert(App::I->is_render_thread());
|
||||
if (m_groud_opacity->get_value() > 0.f)
|
||||
{
|
||||
bool depth = glIsEnabled(GL_DEPTH_TEST);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
|
||||
glEnable(pp::renderer::gl::depth_test_state());
|
||||
glClear(pp::renderer::gl::framebuffer_depth_buffer_mask());
|
||||
|
||||
auto nav = m_hm_image.m_data ? -(m_hm_preview_nav->m_value - 0.5f) : glm::vec2(0);
|
||||
auto mvp = proj * camera
|
||||
@@ -239,14 +267,14 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
|
||||
auto light_pos = glm::vec3(sinf(light_yaw) + nav.x, light_pitch + get_offset(), cosf(light_yaw) + nav.y);
|
||||
auto light_dir = glm::normalize(light_pos);
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
// DRAW SOLID
|
||||
if (m_hm_image.m_data)
|
||||
{
|
||||
|
||||
if (m_shade_mode == ShadeMode::Solid)
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
ShaderManager::use(kShader::Lambert);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
||||
ShaderManager::u_vec3(kShaderUniform::LightDir, light_dir);
|
||||
@@ -271,7 +299,7 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
|
||||
ShaderManager::u_float(kShaderUniform::Ambient, get_ambient());
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
m_sampler_mipmap.bind(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||
m_texture.bind();
|
||||
m_hm_plane.draw_fill();
|
||||
m_texture.unbind();
|
||||
@@ -282,7 +310,7 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
|
||||
auto wire_alpha = m_hm_image.m_data ? m_hm_wireframe->get_value() : 1.f;
|
||||
if (wire_alpha > 0.f)
|
||||
{
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
ShaderManager::use(kShader::Color);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, glm::vec4(
|
||||
glm::vec3(m_groud_value->get_value()),
|
||||
@@ -292,9 +320,17 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
||||
if (m_hm_image.m_data && m_shade_mode == ShadeMode::Transparent)
|
||||
{
|
||||
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
|
||||
glColorMask(
|
||||
pp::renderer::gl::color_write_disabled(),
|
||||
pp::renderer::gl::color_write_disabled(),
|
||||
pp::renderer::gl::color_write_disabled(),
|
||||
pp::renderer::gl::color_write_disabled());
|
||||
m_hm_plane.draw_fill();
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glColorMask(
|
||||
pp::renderer::gl::color_write_enabled(),
|
||||
pp::renderer::gl::color_write_enabled(),
|
||||
pp::renderer::gl::color_write_enabled(),
|
||||
pp::renderer::gl::color_write_enabled());
|
||||
}
|
||||
m_hm_plane.draw_stroke();
|
||||
}
|
||||
@@ -307,23 +343,23 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
|
||||
{
|
||||
auto p2d = xy(c) / c.z;
|
||||
GLint vp[4];
|
||||
glGetIntegerv(GL_VIEWPORT, vp);
|
||||
glGetIntegerv(pp::renderer::gl::viewport_query(), vp);
|
||||
auto aspect_ratio = (float)vp[3] / (float)vp[2];
|
||||
glEnable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-1.f, 1.f, -1.f, 1.f) *
|
||||
//glm::scale(glm::vec3(100)) *
|
||||
glm::translate(glm::vec3(p2d, 0)) * glm::scale(glm::vec3(.1f * aspect_ratio, .1f, 1.f)));
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||
m_sampler_linear.bind(0);
|
||||
constexpr auto sun_tex = const_hash("data/sun.png");
|
||||
TextureManager::get(sun_tex).bind();
|
||||
m_plane.draw_fill();
|
||||
}
|
||||
}
|
||||
depth ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
|
||||
depth ? glEnable(pp::renderer::gl::depth_test_state()) : glDisable(pp::renderer::gl::depth_test_state());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,11 +391,11 @@ void NodePanelGrid::bake_uvs()
|
||||
RTT fb;
|
||||
if (ShaderManager::ext_float32)
|
||||
{
|
||||
fb.create(m_texture.size().x, m_texture.size().y, -1, GL_RGBA32F);
|
||||
fb.create(m_texture.size().x, m_texture.size().y, -1, pp::renderer::gl::rgba32f_internal_format());
|
||||
}
|
||||
else if (ShaderManager::ext_float16)
|
||||
{
|
||||
fb.create(m_texture.size().x, m_texture.size().y, -1, GL_RGBA16F);
|
||||
fb.create(m_texture.size().x, m_texture.size().y, -1, pp::renderer::gl::rgba16f_internal_format());
|
||||
}
|
||||
|
||||
std::unique_ptr<float[]> data_nor;
|
||||
@@ -367,8 +403,8 @@ void NodePanelGrid::bake_uvs()
|
||||
App::I->render_task([&]{
|
||||
fb.bindFramebuffer();
|
||||
fb.clear({ 1, 0, 0, 1 });
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
glViewport(0, 0, fb.getWidth(), fb.getHeight());
|
||||
ShaderManager::use(kShader::BakeUV);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::mat4(1));
|
||||
|
||||
@@ -86,6 +86,7 @@ public:
|
||||
float get_resolution() const;
|
||||
float get_height() const;
|
||||
float get_offset() const;
|
||||
bool load_heightmap_file(const std::string& path, bool raise_ground_opacity);
|
||||
void draw_heightmap(const glm::mat4& proj, const glm::mat4& camera, bool commit) const;
|
||||
void bake_uvs();
|
||||
};
|
||||
|
||||
@@ -1,9 +1,198 @@
|
||||
#include "pch.h"
|
||||
#include "app_core/quick_ui.h"
|
||||
#include "node_panel_quick.h"
|
||||
#include "node_stroke_preview.h"
|
||||
#include "node_image.h"
|
||||
#include "app.h"
|
||||
|
||||
namespace {
|
||||
|
||||
class LegacyQuickUiServices final : public pp::app::QuickUiServices {
|
||||
public:
|
||||
LegacyQuickUiServices(NodePanelQuick& panel, const NodePanelQuick::MiniState* restore_state = nullptr) noexcept
|
||||
: panel_(panel)
|
||||
, restore_state_(restore_state)
|
||||
{
|
||||
}
|
||||
|
||||
void select_slot(pp::app::QuickUiSlotKind slot_kind, int slot_index, bool fire_event) override
|
||||
{
|
||||
if (slot_kind == pp::app::QuickUiSlotKind::brush) {
|
||||
panel_.set_selected_brush_index(slot_index, fire_event);
|
||||
return;
|
||||
}
|
||||
|
||||
panel_.set_selected_color_index(slot_index, fire_event);
|
||||
}
|
||||
|
||||
void open_slot_popup(pp::app::QuickUiSlotKind slot_kind, int slot_index) override
|
||||
{
|
||||
if (slot_kind == pp::app::QuickUiSlotKind::brush) {
|
||||
open_brush_popup(slot_index);
|
||||
return;
|
||||
}
|
||||
|
||||
open_color_picker(slot_index);
|
||||
}
|
||||
|
||||
void restore_state(int brush_index, int color_index, bool fire_event) override
|
||||
{
|
||||
if (!restore_state_)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < static_cast<int>(panel_.m_button_brushes.size()); i++)
|
||||
{
|
||||
auto b = static_cast<NodeStrokePreview*>(panel_.m_button_brushes[i]->m_children[0].get());
|
||||
b->m_brush = restore_state_->brushes[i];
|
||||
b->draw_stroke();
|
||||
auto c = static_cast<NodeBorder*>(panel_.m_button_colors[i]->m_children[0].get());
|
||||
c->m_color = restore_state_->colors[i];
|
||||
}
|
||||
panel_.set_selected_color_index(color_index, fire_event);
|
||||
panel_.set_selected_brush_index(brush_index, fire_event);
|
||||
}
|
||||
|
||||
void reset_state(bool fire_event) override
|
||||
{
|
||||
for (int i = 0; i < static_cast<int>(panel_.m_button_brushes.size()); i++)
|
||||
{
|
||||
auto b = static_cast<NodeStrokePreview*>(panel_.m_button_brushes[i]->m_children[0].get());
|
||||
b->m_brush = std::make_shared<Brush>();
|
||||
b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png");
|
||||
b->draw_stroke();
|
||||
}
|
||||
static_cast<NodeBorder*>(panel_.m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1);
|
||||
static_cast<NodeBorder*>(panel_.m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1);
|
||||
static_cast<NodeBorder*>(panel_.m_button_colors[2]->m_children[0].get())->m_color = glm::vec4(1, 1, 1, 1);
|
||||
panel_.set_selected_brush_index(0, fire_event);
|
||||
panel_.set_selected_color_index(0, fire_event);
|
||||
}
|
||||
|
||||
private:
|
||||
void open_brush_popup(int slot_index)
|
||||
{
|
||||
auto button = panel_.m_button_brushes[slot_index];
|
||||
if (!button)
|
||||
return;
|
||||
|
||||
auto popup = App::I->presets;
|
||||
auto screen = panel_.root()->m_size;
|
||||
glm::vec2 tick_sz = { 16, 32 };
|
||||
glm::vec2 tick_pos = button->m_pos + glm::vec2(button->m_size.x, 0);
|
||||
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y };
|
||||
|
||||
auto tick = panel_.root()->add_child<NodeImage>();
|
||||
tick->SetPositioning(YGPositionTypeAbsolute);
|
||||
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
|
||||
tick->SetSize(tick_sz);
|
||||
tick->set_image("data/ui/popup-tick.png");
|
||||
tick->m_scale = { 1, 1 };
|
||||
|
||||
float hh = popup->m_container->m_children.size() > 10 ? (screen.y - 90.f) : 400.f;
|
||||
popup->SetWidth(350);
|
||||
popup->SetHeight(glm::max(hh, 400.f));
|
||||
popup->SetPositioning(YGPositionTypeAbsolute);
|
||||
popup->SetPosition(popup_pos);
|
||||
panel_.root()->add_child(popup);
|
||||
|
||||
panel_.root()->update();
|
||||
popup->tick(0);
|
||||
popup->update();
|
||||
|
||||
if (tick_pos.x + popup->m_size.x > screen.x)
|
||||
{
|
||||
tick_pos = button->m_pos - glm::vec2(tick_sz.x, 0);
|
||||
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y };
|
||||
tick->m_scale.x = -1.f;
|
||||
}
|
||||
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
|
||||
popup->SetPosition(popup_pos);
|
||||
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
|
||||
popup->update();
|
||||
|
||||
popup->m_mouse_ignore = false;
|
||||
popup->m_flood_events = true;
|
||||
popup->m_capture_children = false;
|
||||
popup->mouse_capture();
|
||||
|
||||
popup->on_popup_close = [tick](Node*) {
|
||||
tick->destroy();
|
||||
};
|
||||
|
||||
auto* panel = &panel_;
|
||||
popup->on_brush_changed = [panel, button](Node*, std::shared_ptr<Brush>& b) {
|
||||
auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get());
|
||||
*pr->m_brush = *b;
|
||||
pr->m_brush->load();
|
||||
pr->draw_stroke();
|
||||
if (panel->on_brush_change)
|
||||
panel->on_brush_change(button, pr->m_brush);
|
||||
};
|
||||
}
|
||||
|
||||
void open_color_picker(int slot_index)
|
||||
{
|
||||
auto target = panel_.m_button_colors[slot_index];
|
||||
if (!target)
|
||||
return;
|
||||
|
||||
auto popup = panel_.m_picker;
|
||||
auto screen = panel_.root()->m_size;
|
||||
glm::vec2 tick_sz = { 16, 32 };
|
||||
glm::vec2 tick_pos = target->m_pos + glm::vec2(target->m_size.x, 0);
|
||||
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y - 140.f };
|
||||
|
||||
auto tick = panel_.root()->add_child<NodeImage>();
|
||||
tick->SetPositioning(YGPositionTypeAbsolute);
|
||||
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
|
||||
tick->SetSize(tick_sz);
|
||||
tick->set_image("data/ui/popup-tick.png");
|
||||
tick->m_scale = { 1, 1 };
|
||||
|
||||
popup->SetPositioning(YGPositionTypeAbsolute);
|
||||
popup->SetPosition(popup_pos);
|
||||
panel_.root()->add_child(popup);
|
||||
|
||||
panel_.root()->update();
|
||||
popup->tick(0);
|
||||
popup->update();
|
||||
|
||||
if (tick_pos.x + popup->m_size.x > screen.x)
|
||||
{
|
||||
tick_pos = target->m_pos - glm::vec2(tick_sz.x, 0);
|
||||
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y - 140.f };
|
||||
tick->m_scale.x = -1.f;
|
||||
}
|
||||
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
|
||||
popup->SetPosition(popup_pos);
|
||||
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
|
||||
popup->update();
|
||||
|
||||
popup->m_mouse_ignore = false;
|
||||
popup->m_flood_events = true;
|
||||
popup->m_capture_children = false;
|
||||
popup->mouse_capture();
|
||||
|
||||
auto c = static_cast<NodeBorder*>(target->m_children[0].get());
|
||||
panel_.m_picker->set_color(c->m_color);
|
||||
panel_.m_picker->on_popup_close = [tick](Node*) {
|
||||
tick->destroy();
|
||||
};
|
||||
|
||||
auto* panel = &panel_;
|
||||
panel_.m_picker->on_color_change = [panel, c](Node*, glm::vec3 rgb) {
|
||||
c->m_color = glm::vec4(rgb, 1.f);
|
||||
if (panel->on_color_change)
|
||||
panel->on_color_change(panel, rgb);
|
||||
};
|
||||
}
|
||||
|
||||
NodePanelQuick& panel_;
|
||||
const NodePanelQuick::MiniState* restore_state_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Node* NodePanelQuick::clone_instantiate() const
|
||||
{
|
||||
return new this_class;
|
||||
@@ -31,11 +220,13 @@ void NodePanelQuick::set_color(glm::vec3 color)
|
||||
int NodePanelQuick::get_selected_brush_index() const
|
||||
{
|
||||
auto it = std::find(m_button_brushes.begin(), m_button_brushes.end(), m_button_brush_current);
|
||||
return std::distance(m_button_brushes.begin(), it);
|
||||
return static_cast<int>(std::distance(m_button_brushes.begin(), it));
|
||||
}
|
||||
|
||||
void NodePanelQuick::set_selected_brush_index(int idx, bool fire_event /*= false*/)
|
||||
{
|
||||
if (!pp::app::validate_quick_slot_index(idx, static_cast<int>(m_button_brushes.size())).ok())
|
||||
return;
|
||||
if (m_button_brush_current)
|
||||
m_button_brush_current->set_active(false);
|
||||
m_button_brush_current = m_button_brushes[idx];
|
||||
@@ -48,11 +239,13 @@ void NodePanelQuick::set_selected_brush_index(int idx, bool fire_event /*= false
|
||||
int NodePanelQuick::get_selected_color_index() const
|
||||
{
|
||||
auto it = std::find(m_button_colors.begin(), m_button_colors.end(), m_button_color_current);
|
||||
return std::distance(m_button_colors.begin(), it);
|
||||
return static_cast<int>(std::distance(m_button_colors.begin(), it));
|
||||
}
|
||||
|
||||
void NodePanelQuick::set_selected_color_index(int idx, bool fire_event /*= false*/)
|
||||
{
|
||||
if (!pp::app::validate_quick_slot_index(idx, static_cast<int>(m_button_colors.size())).ok())
|
||||
return;
|
||||
if (m_button_color_current)
|
||||
m_button_color_current->set_active(false);
|
||||
m_button_color_current = m_button_colors[idx];
|
||||
@@ -77,32 +270,30 @@ NodePanelQuick::MiniState NodePanelQuick::get_state() const
|
||||
|
||||
void NodePanelQuick::set_state(const MiniState& state, bool fire_event /*= false*/)
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
auto b = static_cast<NodeStrokePreview*>(m_button_brushes[i]->m_children[0].get());
|
||||
b->m_brush = state.brushes[i];
|
||||
b->draw_stroke();
|
||||
auto c = static_cast<NodeBorder*>(m_button_colors[i]->m_children[0].get());
|
||||
c->m_color = state.colors[i];
|
||||
}
|
||||
set_selected_color_index(state.color_index, fire_event);
|
||||
set_selected_brush_index(state.brush_index, fire_event);
|
||||
const auto plan = pp::app::plan_quick_state_restore(
|
||||
state.brush_index,
|
||||
state.color_index,
|
||||
static_cast<int>(m_button_brushes.size()),
|
||||
fire_event);
|
||||
if (!plan)
|
||||
return;
|
||||
|
||||
LegacyQuickUiServices services(*this, &state);
|
||||
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
|
||||
if (!status.ok())
|
||||
LOG("Quick restore action failed: %s", status.message);
|
||||
}
|
||||
|
||||
void NodePanelQuick::reset_state(bool fire_event /*= false*/)
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
auto b = static_cast<NodeStrokePreview*>(m_button_brushes[i]->m_children[0].get());
|
||||
b->m_brush = std::make_shared<Brush>();
|
||||
b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png");
|
||||
b->draw_stroke();
|
||||
}
|
||||
static_cast<NodeBorder*>(m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1);
|
||||
static_cast<NodeBorder*>(m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1);
|
||||
static_cast<NodeBorder*>(m_button_colors[2]->m_children[0].get())->m_color = glm::vec4(1, 1, 1, 1);
|
||||
set_selected_brush_index(0, fire_event);
|
||||
set_selected_color_index(0, fire_event);
|
||||
const auto plan = pp::app::plan_quick_state_reset(static_cast<int>(m_button_brushes.size()), fire_event);
|
||||
if (!plan)
|
||||
return;
|
||||
|
||||
LegacyQuickUiServices services(*this);
|
||||
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
|
||||
if (!status.ok())
|
||||
LOG("Quick reset action failed: %s", status.message);
|
||||
}
|
||||
|
||||
void NodePanelQuick::init_controls()
|
||||
@@ -179,10 +370,12 @@ NodeButtonCustom* NodePanelQuick::init_button_brush(const std::string& name, boo
|
||||
{
|
||||
LOG("init_button_brush %s", name.c_str());
|
||||
auto button = find<NodeButtonCustom>(name.c_str());
|
||||
if (!button)
|
||||
if (!button) {
|
||||
LOG("couldn't find button %s", name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
button->on_click = std::bind(&this_class::handle_button_brush_click, this, std::placeholders::_1);
|
||||
LOG("button has %d children", button->m_children.size());
|
||||
LOG("button has %d children", static_cast<int>(button->m_children.size()));
|
||||
auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get());
|
||||
pr->m_brush = std::make_shared<Brush>();
|
||||
pr->m_brush->m_tip_size_pressure = szp;
|
||||
@@ -197,138 +390,36 @@ NodeButtonCustom* NodePanelQuick::init_button_brush(const std::string& name, boo
|
||||
|
||||
void NodePanelQuick::handle_button_brush_click(Node* button)
|
||||
{
|
||||
// the first time select the box
|
||||
if (m_button_brush_current != button)
|
||||
{
|
||||
auto b = static_cast<NodeButtonCustom*>(button);
|
||||
b->set_active(true);
|
||||
m_button_brush_current->set_active(false);
|
||||
m_button_brush_current = b;
|
||||
m_button_brush_current_preview = static_cast<NodeStrokePreview*>(button->m_children[0].get());
|
||||
if (on_brush_change)
|
||||
on_brush_change(this, m_button_brush_current_preview->m_brush);
|
||||
const auto clicked = std::find(m_button_brushes.begin(), m_button_brushes.end(), button);
|
||||
const int clicked_index = static_cast<int>(std::distance(m_button_brushes.begin(), clicked));
|
||||
const auto plan = pp::app::plan_quick_slot_click(
|
||||
pp::app::QuickUiSlotKind::brush,
|
||||
get_selected_brush_index(),
|
||||
clicked_index,
|
||||
static_cast<int>(m_button_brushes.size()));
|
||||
if (!plan)
|
||||
return;
|
||||
}
|
||||
|
||||
// if the box is already selected show the popup
|
||||
|
||||
auto popup = App::I->presets;
|
||||
auto screen = root()->m_size;
|
||||
glm::vec2 tick_sz = { 16, 32 };
|
||||
glm::vec2 tick_pos = button->m_pos + glm::vec2(button->m_size.x, 0);
|
||||
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y };
|
||||
|
||||
auto tick = root()->add_child<NodeImage>();
|
||||
tick->SetPositioning(YGPositionTypeAbsolute);
|
||||
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
|
||||
tick->SetSize(tick_sz);
|
||||
tick->set_image("data/ui/popup-tick.png");
|
||||
tick->m_scale = { 1, 1 };
|
||||
|
||||
float hh = popup->m_container->m_children.size() > 10 ? (screen.y - 90.f) : 400.f;
|
||||
popup->SetWidth(350);
|
||||
popup->SetHeight(glm::max(hh, 400.f));
|
||||
popup->SetPositioning(YGPositionTypeAbsolute);
|
||||
popup->SetPosition(popup_pos);
|
||||
root()->add_child(popup);
|
||||
|
||||
root()->update();
|
||||
popup->tick(0);
|
||||
popup->update();
|
||||
|
||||
if (tick_pos.x + popup->m_size.x > screen.x)
|
||||
{
|
||||
tick_pos = button->m_pos - glm::vec2(tick_sz.x, 0);
|
||||
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y };
|
||||
tick->m_scale.x = -1.f;
|
||||
}
|
||||
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
|
||||
popup->SetPosition(popup_pos);
|
||||
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
|
||||
popup->update();
|
||||
|
||||
popup->m_mouse_ignore = false;
|
||||
popup->m_flood_events = true;
|
||||
popup->m_capture_children = false;
|
||||
popup->mouse_capture();
|
||||
|
||||
popup->on_popup_close = [this, tick](Node*) {
|
||||
tick->destroy();
|
||||
};
|
||||
|
||||
popup->on_brush_changed = [this, button](Node* target, std::shared_ptr<Brush>& b) {
|
||||
auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get());
|
||||
*pr->m_brush = *b;
|
||||
pr->m_brush->load();
|
||||
pr->draw_stroke();
|
||||
if (on_brush_change)
|
||||
on_brush_change(button, pr->m_brush);
|
||||
};
|
||||
LegacyQuickUiServices services(*this);
|
||||
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
|
||||
if (!status.ok())
|
||||
LOG("Quick brush action failed: %s", status.message);
|
||||
}
|
||||
|
||||
void NodePanelQuick::handle_button_color_click(Node* target)
|
||||
{
|
||||
// the first time select the box
|
||||
if (m_button_color_current != target)
|
||||
{
|
||||
auto button = static_cast<NodeButtonCustom*>(target);
|
||||
button->set_active(true);
|
||||
m_button_color_current->set_active(false);
|
||||
m_button_color_current = button;
|
||||
m_button_color_current_inner = static_cast<NodeBorder*>(m_button_color_current->m_children[0].get());
|
||||
if (on_color_change)
|
||||
on_color_change(this, m_button_color_current_inner->m_color);
|
||||
const auto clicked = std::find(m_button_colors.begin(), m_button_colors.end(), target);
|
||||
const int clicked_index = static_cast<int>(std::distance(m_button_colors.begin(), clicked));
|
||||
const auto plan = pp::app::plan_quick_slot_click(
|
||||
pp::app::QuickUiSlotKind::color,
|
||||
get_selected_color_index(),
|
||||
clicked_index,
|
||||
static_cast<int>(m_button_colors.size()));
|
||||
if (!plan)
|
||||
return;
|
||||
}
|
||||
|
||||
// if the box is already selected show the popup
|
||||
auto popup = m_picker;
|
||||
auto screen = root()->m_size;
|
||||
glm::vec2 tick_sz = { 16, 32 };
|
||||
glm::vec2 tick_pos = target->m_pos + glm::vec2(target->m_size.x, 0);
|
||||
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y - 140.f };
|
||||
|
||||
auto tick = root()->add_child<NodeImage>();
|
||||
tick->SetPositioning(YGPositionTypeAbsolute);
|
||||
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
|
||||
tick->SetSize(tick_sz);
|
||||
tick->set_image("data/ui/popup-tick.png");
|
||||
tick->m_scale = { 1, 1 };
|
||||
|
||||
//float hh = popup->m_container->m_children.size() > 10 ? (screen.y / App::I->zoom - 90.f) : 400.f;
|
||||
//popup->SetHeight(glm::max(hh, 400.f));
|
||||
popup->SetPositioning(YGPositionTypeAbsolute);
|
||||
popup->SetPosition(popup_pos);
|
||||
root()->add_child(popup);
|
||||
|
||||
root()->update();
|
||||
popup->tick(0);
|
||||
popup->update();
|
||||
|
||||
if (tick_pos.x + popup->m_size.x > screen.x)
|
||||
{
|
||||
tick_pos = target->m_pos - glm::vec2(tick_sz.x, 0);
|
||||
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y - 140.f };
|
||||
tick->m_scale.x = -1.f;
|
||||
}
|
||||
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
|
||||
popup->SetPosition(popup_pos);
|
||||
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
|
||||
popup->update();
|
||||
|
||||
popup->m_mouse_ignore = false;
|
||||
popup->m_flood_events = true;
|
||||
popup->m_capture_children = false;
|
||||
popup->mouse_capture();
|
||||
|
||||
auto c = static_cast<NodeBorder*>(target->m_children[0].get());
|
||||
m_picker->set_color(c->m_color);
|
||||
m_picker->on_popup_close = [this, tick](Node*) {
|
||||
tick->destroy();
|
||||
};
|
||||
m_picker->on_color_change = [this, c](Node*, glm::vec3 rgb) {
|
||||
c->m_color = glm::vec4(rgb, 1.f);
|
||||
if (on_color_change)
|
||||
on_color_change(this, rgb);
|
||||
};
|
||||
|
||||
LegacyQuickUiServices services(*this);
|
||||
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
|
||||
if (!status.ok())
|
||||
LOG("Quick color action failed: %s", status.message);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "node_scroll.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "event.h"
|
||||
#include "shader.h"
|
||||
|
||||
@@ -110,7 +111,7 @@ void NodeScroll::draw()
|
||||
|
||||
if (rect.w > 0 && rect.z > 0)
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
ShaderManager::use(kShader::Color);
|
||||
|
||||
if (m_direction == kScrollDirection::Vertical)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "bezier.h"
|
||||
#include "canvas.h"
|
||||
#include "app.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
std::atomic_int NodeStrokePreview::s_instances{ 0 };
|
||||
std::atomic_bool NodeStrokePreview::s_running{ false };
|
||||
@@ -71,7 +72,7 @@ void NodeStrokePreview::restore_context()
|
||||
NodeBorder::restore_context();
|
||||
init_controls();
|
||||
if (m_size.x > 0 && m_size.y > 0)
|
||||
m_tex_preview.create(m_size.x, m_size.y);
|
||||
m_tex_preview.create(static_cast<int>(m_size.x), static_cast<int>(m_size.y));
|
||||
draw_stroke();
|
||||
}
|
||||
|
||||
@@ -89,11 +90,15 @@ void NodeStrokePreview::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2
|
||||
m_rtt_mixer.bindFramebuffer();
|
||||
|
||||
glViewport(0, 0, m_rtt_mixer.getWidth(), m_rtt_mixer.getHeight());
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
glEnable(pp::renderer::gl::scissor_test_state());
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
|
||||
glScissor(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
|
||||
glScissor(
|
||||
static_cast<int>(bb_min.x),
|
||||
static_cast<int>(bb_min.y),
|
||||
static_cast<int>(bb_sz.x),
|
||||
static_cast<int>(bb_sz.y));
|
||||
|
||||
const auto& b = m_brush;
|
||||
glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
|
||||
@@ -126,16 +131,16 @@ void NodeStrokePreview::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2
|
||||
ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity);
|
||||
|
||||
m_sampler_linear.bind(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||
m_tex_background.bind();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
|
||||
m_rtt.bindTexture();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(3U));
|
||||
m_tex_dual.bind();
|
||||
glActiveTexture(GL_TEXTURE4);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(4U));
|
||||
b->m_pattern_texture ?
|
||||
b->m_pattern_texture->bind() :
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
|
||||
m_plane.draw_fill();
|
||||
|
||||
m_rtt_mixer.unbindFramebuffer();
|
||||
@@ -146,7 +151,7 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array<vertex_t, 4>& P, Tex
|
||||
{
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
|
||||
blend_tex.bind(); // bg, copy of framebuffer (copied before drawing)
|
||||
}
|
||||
|
||||
@@ -167,7 +172,7 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array<vertex_t, 4>& P, Tex
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
// this is also used by the mixer
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, tex_pos.x, tex_pos.y,
|
||||
glCopyTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0, tex_pos.x, tex_pos.y,
|
||||
tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y);
|
||||
}
|
||||
|
||||
@@ -186,7 +191,7 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array<vertex_t, 4>& P, Tex
|
||||
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
|
||||
blend_tex.unbind();
|
||||
}
|
||||
|
||||
@@ -259,8 +264,8 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
|
||||
GLint vp[4];
|
||||
GLfloat cc[4];
|
||||
glGetIntegerv(GL_VIEWPORT, vp);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
||||
glGetIntegerv(pp::renderer::gl::viewport_query(), vp);
|
||||
glGetFloatv(pp::renderer::gl::color_clear_value_query(), cc);
|
||||
|
||||
float zoom = root()->m_zoom;
|
||||
|
||||
@@ -345,7 +350,7 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
if (b->m_pattern_flipx) patt_scale.x *= -1.f;
|
||||
if (b->m_pattern_flipy) patt_scale.y *= -1.f;
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
ShaderManager::use(kShader::Stroke);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0); // brush
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
@@ -373,10 +378,10 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
ShaderManager::u_float(kShaderUniform::MixAlpha, 0);
|
||||
ShaderManager::u_float(kShaderUniform::Wet, 0);
|
||||
ShaderManager::u_float(kShaderUniform::Noise, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||
dual_brush->m_tip_texture ?
|
||||
dual_brush->m_tip_texture->bind() :
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
|
||||
auto frames_dual = stroke_draw_compute(m_dual_stroke, zoom);
|
||||
for (auto& f : frames_dual)
|
||||
{
|
||||
@@ -387,9 +392,17 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
}
|
||||
|
||||
// copy raw stroke to tex
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
|
||||
m_tex_dual.bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.x, size.y);
|
||||
glCopyTexSubImage2D(
|
||||
pp::renderer::gl::texture_2d_target(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
static_cast<int>(size.x),
|
||||
static_cast<int>(size.y));
|
||||
}
|
||||
|
||||
// CHEKCERBOARD
|
||||
@@ -402,7 +415,15 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
m_plane.draw_fill();
|
||||
//m_rtt.clear({ .3f, .3f, .3f, 1.f });
|
||||
m_tex_background.bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.x, size.y);
|
||||
glCopyTexSubImage2D(
|
||||
pp::renderer::gl::texture_2d_target(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
static_cast<int>(size.x),
|
||||
static_cast<int>(size.y));
|
||||
|
||||
// DRAW MAIN BRUSH
|
||||
|
||||
@@ -412,19 +433,19 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
ShaderManager::u_float(kShaderUniform::Wet, b->m_tip_wet);
|
||||
ShaderManager::u_float(kShaderUniform::Noise, b->m_tip_noise);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||
b->m_tip_texture->bind();
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
|
||||
m_tex.bind(); // tmp swap for blending
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(2U));
|
||||
b->m_pattern_texture ?
|
||||
b->m_pattern_texture->bind() :
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
b->m_tip_mix > 0.f ? m_rtt_mixer.bindTexture() : glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(3U));
|
||||
b->m_tip_mix > 0.f ? m_rtt_mixer.bindTexture() : glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
|
||||
auto frames = stroke_draw_compute(m_stroke, zoom);
|
||||
m_rtt.clear();
|
||||
for (auto& f : frames)
|
||||
@@ -443,13 +464,21 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
ShaderManager::u_float(kShaderUniform::Opacity, f.opacity);
|
||||
/*auto rect =*/ stroke_draw_samples(f.shapes, m_tex);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(3U));
|
||||
m_rtt_mixer.unbindTexture();
|
||||
|
||||
// copy raw stroke to tex
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
|
||||
m_tex.bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.x, size.y);
|
||||
glCopyTexSubImage2D(
|
||||
pp::renderer::gl::texture_2d_target(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
static_cast<int>(size.x),
|
||||
static_cast<int>(size.y));
|
||||
|
||||
// COMPOSITE
|
||||
|
||||
@@ -483,21 +512,29 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
m_sampler_linear.bind(3);
|
||||
m_sampler_linear_repeat.bind(4);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
|
||||
m_tex_background.bind();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
|
||||
m_tex.bind();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(3U));
|
||||
m_tex_dual.bind();
|
||||
glActiveTexture(GL_TEXTURE4);
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(4U));
|
||||
b->m_pattern_texture ?
|
||||
b->m_pattern_texture->bind() :
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
|
||||
m_plane.draw_fill();
|
||||
|
||||
// copy the result to the actual preview
|
||||
m_tex_preview.bind();
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.x, size.y);
|
||||
glCopyTexSubImage2D(
|
||||
pp::renderer::gl::texture_2d_target(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
static_cast<int>(size.x),
|
||||
static_cast<int>(size.y));
|
||||
|
||||
m_rtt.unbindFramebuffer();
|
||||
|
||||
@@ -538,9 +575,13 @@ void NodeStrokePreview::draw_stroke()
|
||||
BT_SetTerminate();
|
||||
|
||||
m_sampler_linear.create();
|
||||
m_sampler_linear_repeat.create(GL_LINEAR, GL_REPEAT);
|
||||
m_sampler_linear_repeat.create(
|
||||
pp::renderer::gl::linear_texture_filter(),
|
||||
pp::renderer::gl::repeat_texture_wrap());
|
||||
m_sampler_mipmap.create();
|
||||
m_sampler_mipmap.set_filter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
|
||||
m_sampler_mipmap.set_filter(
|
||||
pp::renderer::gl::linear_mipmap_linear_texture_filter(),
|
||||
pp::renderer::gl::linear_texture_filter());
|
||||
m_brush_shape.create();
|
||||
while (s_running)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "node_text.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "shader.h"
|
||||
|
||||
Node* NodeText::clone_instantiate() const
|
||||
@@ -173,9 +174,9 @@ void NodeText::draw()
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, m_proj * pos);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, m_color);
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
m_text_mesh.draw();
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
}
|
||||
|
||||
void NodeText::handle_resize(glm::vec2 old_size, glm::vec2 new_size, float zoom)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "app.h"
|
||||
#include "log.h"
|
||||
#include "node_text_input.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "node_border.h"
|
||||
|
||||
Node* NodeTextInput::clone_instantiate() const
|
||||
@@ -219,13 +220,13 @@ void NodeTextInput::draw()
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, m_proj * pos);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, m_color);
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
m_text_mesh.draw();
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
|
||||
if (m_cursor_visible)
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
glm::mat4 cur_pos = glm::translate(glm::vec3(m_pos + m_off + xy(m_text_mesh.cur_box), 0));
|
||||
glm::mat4 cur_scale = glm::scale(glm::vec3(zw(m_text_mesh.cur_box), 1));
|
||||
glm::mat4 cur_pivot = glm::translate(glm::vec3(0.5, 0.5, 0));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "node_viewport.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "shader.h"
|
||||
#include "app.h"
|
||||
|
||||
@@ -11,24 +12,24 @@ void NodeViewport::draw()
|
||||
|
||||
GLint vp[4];
|
||||
GLfloat cc[4];
|
||||
glGetIntegerv(GL_VIEWPORT, vp);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
||||
glGetIntegerv(pp::renderer::gl::viewport_query(), vp);
|
||||
glGetFloatv(pp::renderer::gl::color_clear_value_query(), cc);
|
||||
|
||||
glClearColor(1, 0, 0, 1);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
|
||||
auto box = m_clip * root()->m_zoom;
|
||||
glm::ivec4 c = (glm::ivec4)glm::vec4(box.x, (int)(vp[3] - box.y - box.w), box.z, box.w);
|
||||
glViewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
|
||||
TextureManager::get(m_tex_id).bind();
|
||||
m_sampler->bind(0);
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(pp::renderer::gl::blend_state());
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, proj * cam);
|
||||
m_faces->draw_fill();
|
||||
m_sampler->unbind();
|
||||
TextureManager::get(m_tex_id).unbind();
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(pp::renderer::gl::blend_state());
|
||||
|
||||
glViewport(vp[0], vp[1], vp[2], vp[3]);
|
||||
glClearColor(cc[0], cc[1], cc[2], cc[3]);
|
||||
|
||||
@@ -44,6 +44,67 @@ namespace {
|
||||
return stroke;
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_screen(float base, float stroke) noexcept
|
||||
{
|
||||
return base + stroke - (base * stroke);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_hard_light(float base, float stroke) noexcept
|
||||
{
|
||||
return stroke < 0.5F
|
||||
? base * (stroke * 2.0F)
|
||||
: blend_stroke_screen(base, 2.0F * stroke - 1.0F);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_hard_mix(float base, float stroke) noexcept
|
||||
{
|
||||
if (base == 0.0F) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
return base + stroke < 0.5F ? 0.0F : saturate(base + stroke);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_color_dodge(float base, float stroke) noexcept
|
||||
{
|
||||
if (base == 0.0F) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
if (stroke == 1.0F) {
|
||||
return 1.0F;
|
||||
}
|
||||
|
||||
return base / (1.0F - stroke);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_color_burn(float base, float stroke) noexcept
|
||||
{
|
||||
if (base == 1.0F) {
|
||||
return 1.0F;
|
||||
}
|
||||
|
||||
if (stroke == 0.0F) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
return 1.0F - std::min(1.0F, (1.0F - base) / stroke);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_linear_height(float base, float stroke, float depth) noexcept
|
||||
{
|
||||
const auto partial = (1.0F - stroke) * std::pow(depth, 0.25F) + (base * depth * 10.0F);
|
||||
return base * partial;
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_height(float base, float stroke, float depth) noexcept
|
||||
{
|
||||
const auto a = std::pow(1.0F - stroke, std::max(1.0F, (1.0F - depth) * 10.0F))
|
||||
* std::pow(depth, 0.25F);
|
||||
const auto b = base * depth * 5.0F;
|
||||
return base * (a + b);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_rgb(float base, float stroke, float base_alpha, float stroke_alpha, float alpha_total, BlendMode mode) noexcept
|
||||
{
|
||||
if (alpha_total <= 0.0F) {
|
||||
@@ -88,6 +149,44 @@ Rgba blend_pixels(Rgba base, Rgba stroke, BlendMode mode) noexcept
|
||||
};
|
||||
}
|
||||
|
||||
float blend_stroke_alpha(
|
||||
float base,
|
||||
float stroke,
|
||||
float depth,
|
||||
StrokeBlendMode mode) noexcept
|
||||
{
|
||||
base = saturate(base);
|
||||
stroke = saturate(stroke);
|
||||
depth = saturate(depth);
|
||||
|
||||
switch (mode) {
|
||||
case StrokeBlendMode::normal:
|
||||
return saturate(mix(base, stroke, depth));
|
||||
case StrokeBlendMode::multiply:
|
||||
return saturate(mix(base, base * stroke, depth));
|
||||
case StrokeBlendMode::subtract:
|
||||
return saturate(mix(base, std::max(0.0F, base - stroke), depth));
|
||||
case StrokeBlendMode::darken:
|
||||
return saturate(mix(base, std::min(base, stroke), depth));
|
||||
case StrokeBlendMode::overlay:
|
||||
return saturate(mix(base, blend_stroke_hard_light(stroke, base), depth));
|
||||
case StrokeBlendMode::color_dodge:
|
||||
return saturate(mix(base, blend_stroke_color_dodge(base, stroke), depth));
|
||||
case StrokeBlendMode::color_burn:
|
||||
return saturate(mix(base, blend_stroke_color_burn(base, stroke), depth));
|
||||
case StrokeBlendMode::linear_burn:
|
||||
return saturate(mix(base, saturate(base + stroke - 1.0F), depth));
|
||||
case StrokeBlendMode::hard_mix:
|
||||
return saturate(mix(base, blend_stroke_hard_mix(base, stroke), depth));
|
||||
case StrokeBlendMode::linear_height:
|
||||
return saturate(blend_stroke_linear_height(base, stroke, depth));
|
||||
case StrokeBlendMode::height:
|
||||
return saturate(blend_stroke_height(base, stroke, depth));
|
||||
}
|
||||
|
||||
return 1.0F;
|
||||
}
|
||||
|
||||
const char* blend_mode_name(BlendMode mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
@@ -106,4 +205,34 @@ const char* blend_mode_name(BlendMode mode) noexcept
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* stroke_blend_mode_name(StrokeBlendMode mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
case StrokeBlendMode::normal:
|
||||
return "normal";
|
||||
case StrokeBlendMode::multiply:
|
||||
return "multiply";
|
||||
case StrokeBlendMode::subtract:
|
||||
return "subtract";
|
||||
case StrokeBlendMode::darken:
|
||||
return "darken";
|
||||
case StrokeBlendMode::overlay:
|
||||
return "overlay";
|
||||
case StrokeBlendMode::color_dodge:
|
||||
return "color_dodge";
|
||||
case StrokeBlendMode::color_burn:
|
||||
return "color_burn";
|
||||
case StrokeBlendMode::linear_burn:
|
||||
return "linear_burn";
|
||||
case StrokeBlendMode::hard_mix:
|
||||
return "hard_mix";
|
||||
case StrokeBlendMode::linear_height:
|
||||
return "linear_height";
|
||||
case StrokeBlendMode::height:
|
||||
return "height";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,20 @@ enum class BlendMode : std::uint8_t {
|
||||
overlay,
|
||||
};
|
||||
|
||||
enum class StrokeBlendMode : std::uint8_t {
|
||||
normal,
|
||||
multiply,
|
||||
subtract,
|
||||
darken,
|
||||
overlay,
|
||||
color_dodge,
|
||||
color_burn,
|
||||
linear_burn,
|
||||
hard_mix,
|
||||
linear_height,
|
||||
height,
|
||||
};
|
||||
|
||||
struct Rgba {
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
@@ -20,6 +34,12 @@ struct Rgba {
|
||||
};
|
||||
|
||||
[[nodiscard]] Rgba blend_pixels(Rgba base, Rgba stroke, BlendMode mode) noexcept;
|
||||
[[nodiscard]] float blend_stroke_alpha(
|
||||
float base,
|
||||
float stroke,
|
||||
float depth,
|
||||
StrokeBlendMode mode) noexcept;
|
||||
[[nodiscard]] const char* blend_mode_name(BlendMode mode) noexcept;
|
||||
[[nodiscard]] const char* stroke_blend_mode_name(StrokeBlendMode mode) noexcept;
|
||||
|
||||
}
|
||||
|
||||
7
src/platform_api/platform_services.cpp
Normal file
7
src/platform_api/platform_services.cpp
Normal file
@@ -0,0 +1,7 @@
|
||||
#include "platform_api/platform_services.h"
|
||||
|
||||
namespace pp::platform {
|
||||
namespace {
|
||||
static_assert(sizeof(PlatformServices*) == sizeof(void*));
|
||||
}
|
||||
}
|
||||
60
src/platform_api/platform_services.h
Normal file
60
src/platform_api/platform_services.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::platform {
|
||||
|
||||
using PickedPathCallback = std::function<void(std::string path)>;
|
||||
using PreparedFileCallback = std::function<void(std::string path, bool saved)>;
|
||||
|
||||
struct PlatformStoragePaths {
|
||||
std::string data_path;
|
||||
std::string work_path;
|
||||
std::string recording_path;
|
||||
std::string temporary_path;
|
||||
};
|
||||
|
||||
class PlatformServices {
|
||||
public:
|
||||
virtual ~PlatformServices() = default;
|
||||
|
||||
[[nodiscard]] virtual PlatformStoragePaths prepare_storage_paths() = 0;
|
||||
virtual void log_stacktrace() = 0;
|
||||
virtual void trigger_crash_test() = 0;
|
||||
[[nodiscard]] virtual std::string clipboard_text() = 0;
|
||||
[[nodiscard]] virtual bool set_clipboard_text(std::string_view text) = 0;
|
||||
virtual void set_cursor_visible(bool visible) = 0;
|
||||
virtual void set_virtual_keyboard_visible(bool visible) = 0;
|
||||
virtual void attach_ui_thread() = 0;
|
||||
virtual void detach_ui_thread() = 0;
|
||||
virtual void acquire_render_context() = 0;
|
||||
virtual void release_render_context() = 0;
|
||||
virtual void present_render_context() = 0;
|
||||
virtual void bind_default_render_target() = 0;
|
||||
virtual void bind_main_render_target() = 0;
|
||||
virtual void apply_render_platform_hints() = 0;
|
||||
virtual void install_render_debug_callback() = 0;
|
||||
virtual void begin_render_capture_frame() = 0;
|
||||
virtual void end_render_capture_frame() = 0;
|
||||
[[nodiscard]] virtual bool deletes_recorded_files_on_clear() = 0;
|
||||
virtual void clear_recorded_files(std::string_view recording_path) = 0;
|
||||
[[nodiscard]] virtual bool enables_live_asset_reloading() = 0;
|
||||
virtual void update_platform_frame(float delta_time_seconds) = 0;
|
||||
virtual void report_rendered_frames(int frames) = 0;
|
||||
virtual void display_file(std::string_view path) = 0;
|
||||
virtual void share_file(std::string_view path) = 0;
|
||||
virtual void request_app_close() = 0;
|
||||
virtual void pick_image(PickedPathCallback callback) = 0;
|
||||
virtual void pick_file(std::vector<std::string> file_types, PickedPathCallback callback) = 0;
|
||||
virtual void pick_save_file(std::vector<std::string> file_types, PickedPathCallback callback) = 0;
|
||||
virtual void pick_directory(PickedPathCallback callback) = 0;
|
||||
virtual void save_prepared_file(
|
||||
std::string_view path,
|
||||
std::string_view suggested_name,
|
||||
PreparedFileCallback callback) = 0;
|
||||
};
|
||||
|
||||
}
|
||||
468
src/platform_legacy/legacy_platform_services.cpp
Normal file
468
src/platform_legacy/legacy_platform_services.cpp
Normal file
@@ -0,0 +1,468 @@
|
||||
#include "pch.h"
|
||||
#include "platform_legacy/legacy_platform_services.h"
|
||||
|
||||
#include "app.h"
|
||||
#include "app_core/document_platform_io.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
#ifdef __ANDROID__
|
||||
void displayKeyboard(bool pShow);
|
||||
void android_async_lock();
|
||||
void android_async_swap();
|
||||
void android_async_unlock();
|
||||
void android_attach_jni();
|
||||
void android_detach_jni();
|
||||
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 __APPLE__
|
||||
void delete_all_files_in_path(const std::string& source_path);
|
||||
#elif __LINUX__
|
||||
#include <tinyfiledialogs.h>
|
||||
std::string linux_home_path();
|
||||
int mkpath(const std::string& dir, mode_t mode = DEFFILEMODE);
|
||||
void linux_update_fps(int frames);
|
||||
#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 {
|
||||
|
||||
void invoke_picked_path_if_selected(
|
||||
const std::string& path,
|
||||
const std::function<void(std::string path)>& callback)
|
||||
{
|
||||
if (pp::app::plan_picked_path(path) == pp::app::PickedPathAction::invoke_callback)
|
||||
callback(path);
|
||||
}
|
||||
|
||||
// DEBT-0017: fallback for platforms that do not inject PlatformServices yet.
|
||||
class LegacyPlatformServices final : public pp::platform::PlatformServices {
|
||||
public:
|
||||
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths() override
|
||||
{
|
||||
#if defined(__IOS__)
|
||||
[App::I->ios_view init_dirs];
|
||||
return {
|
||||
App::I->data_path,
|
||||
App::I->work_path,
|
||||
App::I->rec_path,
|
||||
App::I->tmp_path,
|
||||
};
|
||||
#elif defined(__OSX__)
|
||||
[App::I->osx_app init_dirs];
|
||||
return {
|
||||
App::I->data_path,
|
||||
App::I->work_path,
|
||||
App::I->rec_path,
|
||||
App::I->tmp_path,
|
||||
};
|
||||
#elif __LINUX__
|
||||
const std::string 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");
|
||||
return {
|
||||
data_path,
|
||||
data_path,
|
||||
data_path + "/frames",
|
||||
{},
|
||||
};
|
||||
#elif __WEB__
|
||||
const std::string 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);
|
||||
return {
|
||||
data_path,
|
||||
data_path,
|
||||
data_path + "/frames",
|
||||
{},
|
||||
};
|
||||
#else
|
||||
return {
|
||||
App::I->data_path,
|
||||
App::I->work_path,
|
||||
App::I->rec_path,
|
||||
App::I->tmp_path,
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
void log_stacktrace() override
|
||||
{
|
||||
#if defined(__OSX__)
|
||||
NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
|
||||
LOG("callstack:\n%s", [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
#endif
|
||||
}
|
||||
|
||||
void trigger_crash_test() override
|
||||
{
|
||||
#ifdef __IOS__
|
||||
[App::I->ios_view crash];
|
||||
#elif __OSX__
|
||||
[App::I->osx_view hockeyapp_crash];
|
||||
#elif defined(__ANDROID__)
|
||||
int *x = nullptr; *x = 42;
|
||||
LOG("%d", *x);
|
||||
#endif
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string clipboard_text() override
|
||||
{
|
||||
#if __IOS__
|
||||
return [App::I->ios_view clipboard_get_string];
|
||||
#elif __OSX__
|
||||
return [App::I->osx_view clipboard_get_string];
|
||||
#elif __ANDROID__
|
||||
return android_get_clipboard();
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
[[nodiscard]] bool set_clipboard_text(std::string_view text) override
|
||||
{
|
||||
const std::string value(text);
|
||||
#if __IOS__
|
||||
return [App::I->ios_view clipboard_set_string:value];
|
||||
#elif __OSX__
|
||||
return [App::I->osx_view clipboard_set_string:value];
|
||||
#elif __ANDROID__
|
||||
return android_set_clipboard(value);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void set_cursor_visible(bool visible) override
|
||||
{
|
||||
#ifdef __OSX__
|
||||
[App::I->osx_view show_cursor:visible];
|
||||
#else
|
||||
(void)visible;
|
||||
#endif
|
||||
}
|
||||
|
||||
void set_virtual_keyboard_visible(bool visible) override
|
||||
{
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (visible)
|
||||
[App::I->ios_view show_keyboard];
|
||||
else
|
||||
[App::I->ios_view hide_keyboard];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
displayKeyboard(visible);
|
||||
#else
|
||||
(void)visible;
|
||||
#endif
|
||||
}
|
||||
|
||||
void attach_ui_thread() override
|
||||
{
|
||||
#ifdef __ANDROID__
|
||||
android_attach_jni();
|
||||
#endif
|
||||
}
|
||||
|
||||
void detach_ui_thread() override
|
||||
{
|
||||
#ifdef __ANDROID__
|
||||
android_detach_jni();
|
||||
#endif
|
||||
}
|
||||
|
||||
void acquire_render_context() override
|
||||
{
|
||||
#if __OSX__
|
||||
[App::I->osx_view async_lock];
|
||||
#elif __IOS__
|
||||
[App::I->ios_view async_lock];
|
||||
#elif __ANDROID__
|
||||
android_async_lock();
|
||||
#elif __LINUX__ || __WEB__
|
||||
glfwMakeContextCurrent(App::I->glfw_window);
|
||||
#endif
|
||||
}
|
||||
|
||||
void release_render_context() override
|
||||
{
|
||||
#if __OSX__
|
||||
[App::I->osx_view async_unlock];
|
||||
#elif __IOS__
|
||||
[App::I->ios_view async_unlock];
|
||||
#elif __ANDROID__
|
||||
android_async_unlock();
|
||||
#endif
|
||||
}
|
||||
|
||||
void present_render_context() override
|
||||
{
|
||||
#if __OSX__
|
||||
[App::I->osx_view async_swap];
|
||||
#elif __IOS__
|
||||
[App::I->ios_view async_swap];
|
||||
#elif __ANDROID__
|
||||
android_async_swap();
|
||||
#elif __LINUX__ || __WEB__
|
||||
glfwSwapBuffers(App::I->glfw_window);
|
||||
#endif
|
||||
}
|
||||
|
||||
void bind_default_render_target() override
|
||||
{
|
||||
glBindFramebuffer(
|
||||
static_cast<GLenum>(pp::renderer::gl::framebuffer_target()),
|
||||
pp::renderer::gl::default_framebuffer_id());
|
||||
}
|
||||
|
||||
void bind_main_render_target() override
|
||||
{
|
||||
#if __IOS__
|
||||
[App::I->ios_view->glview bindDrawable];
|
||||
#else
|
||||
bind_default_render_target();
|
||||
#endif
|
||||
}
|
||||
|
||||
void apply_render_platform_hints() override
|
||||
{
|
||||
#if defined(__OSX__)
|
||||
glEnable(static_cast<GLenum>(pp::renderer::gl::program_point_size_state()));
|
||||
glEnable(static_cast<GLenum>(pp::renderer::gl::line_smooth_state()));
|
||||
#endif
|
||||
}
|
||||
|
||||
void install_render_debug_callback() override
|
||||
{
|
||||
}
|
||||
|
||||
void begin_render_capture_frame() override
|
||||
{
|
||||
}
|
||||
|
||||
void end_render_capture_frame() override
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] bool deletes_recorded_files_on_clear() override
|
||||
{
|
||||
#if defined(__IOS__) || defined(__OSX__)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void clear_recorded_files(std::string_view recording_path) override
|
||||
{
|
||||
#if defined(__IOS__) || defined(__OSX__)
|
||||
delete_all_files_in_path(std::string(recording_path));
|
||||
#else
|
||||
(void)recording_path;
|
||||
#endif
|
||||
}
|
||||
|
||||
[[nodiscard]] bool enables_live_asset_reloading() override
|
||||
{
|
||||
#if defined(__OSX__)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void update_platform_frame(float delta_time_seconds) override
|
||||
{
|
||||
(void)delta_time_seconds;
|
||||
}
|
||||
|
||||
void report_rendered_frames(int frames) override
|
||||
{
|
||||
#ifdef __LINUX__
|
||||
linux_update_fps(frames);
|
||||
#else
|
||||
(void)frames;
|
||||
#endif
|
||||
}
|
||||
|
||||
void pick_image(pp::platform::PickedPathCallback callback) override
|
||||
{
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[App::I->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 = [App::I->osx_view pick_file:fileTypes];
|
||||
invoke_picked_path_if_selected(path, callback);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
android_pick_file(callback);
|
||||
#elif __LINUX__
|
||||
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
|
||||
invoke_picked_path_if_selected(p, callback);
|
||||
#elif __WEB__
|
||||
webgl_pick_file(callback);
|
||||
#else
|
||||
(void)callback;
|
||||
#endif
|
||||
}
|
||||
|
||||
void pick_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
|
||||
{
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()];
|
||||
for (const auto& t : file_types)
|
||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
||||
[App::I->ios_view pick_file:fileTypes then:callback];
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()];
|
||||
for (const auto& t : file_types)
|
||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
||||
std::string path = [App::I->osx_view pick_file:fileTypes];
|
||||
invoke_picked_path_if_selected(path, callback);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
android_pick_file(callback);
|
||||
#elif __LINUX__
|
||||
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
|
||||
invoke_picked_path_if_selected(p, callback);
|
||||
#elif __WEB__
|
||||
webgl_pick_file(callback);
|
||||
#else
|
||||
(void)file_types;
|
||||
(void)callback;
|
||||
#endif
|
||||
}
|
||||
|
||||
void pick_save_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
|
||||
{
|
||||
#if __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()];
|
||||
for (const auto& t : file_types)
|
||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
||||
std::string path = [App::I->osx_view pick_file_save:fileTypes];
|
||||
invoke_picked_path_if_selected(path, callback);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
android_pick_file_save(callback);
|
||||
#else
|
||||
(void)file_types;
|
||||
(void)callback;
|
||||
#endif
|
||||
}
|
||||
|
||||
void pick_directory(pp::platform::PickedPathCallback callback) override
|
||||
{
|
||||
#ifdef __IOS__
|
||||
(void)callback;
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
std::string path = [App::I->osx_view pick_dir];
|
||||
invoke_picked_path_if_selected(path, callback);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
(void)callback;
|
||||
#else
|
||||
(void)callback;
|
||||
#endif
|
||||
}
|
||||
|
||||
void display_file(std::string_view path) override
|
||||
{
|
||||
const std::string value(path);
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[App::I->ios_view display_file:value];
|
||||
});
|
||||
#elif __OSX__
|
||||
[[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:value.c_str()]];
|
||||
#else
|
||||
(void)value;
|
||||
#endif
|
||||
}
|
||||
|
||||
void share_file(std::string_view path) override
|
||||
{
|
||||
const std::string value(path);
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[App::I->ios_view share_file:[NSString stringWithUTF8String:value.c_str()]];
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[App::I->osx_view share_file:[NSString stringWithUTF8String:value.c_str()]];
|
||||
});
|
||||
#else
|
||||
(void)value;
|
||||
#endif
|
||||
}
|
||||
|
||||
void request_app_close() override
|
||||
{
|
||||
#ifdef __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[App::I->osx_view close];
|
||||
});
|
||||
#elif __LINUX__
|
||||
glfwSetWindowShouldClose(App::I->glfw_window, GLFW_TRUE);
|
||||
#endif
|
||||
}
|
||||
|
||||
void save_prepared_file(
|
||||
std::string_view path,
|
||||
std::string_view suggested_name,
|
||||
pp::platform::PreparedFileCallback callback) override
|
||||
{
|
||||
const std::string value(path);
|
||||
const std::string name(suggested_name);
|
||||
#ifdef __IOS__
|
||||
(void)name;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[App::I->ios_view pick_file_save:value];
|
||||
});
|
||||
callback(value, true);
|
||||
#elif __WEB__
|
||||
webgl_pick_file_save(value, name, [callback = std::move(callback), value](bool success) {
|
||||
callback(value, success);
|
||||
});
|
||||
#else
|
||||
(void)name;
|
||||
callback(value, false);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace pp::platform::legacy {
|
||||
|
||||
PlatformServices& platform_services()
|
||||
{
|
||||
static LegacyPlatformServices services;
|
||||
return services;
|
||||
}
|
||||
|
||||
}
|
||||
9
src/platform_legacy/legacy_platform_services.h
Normal file
9
src/platform_legacy/legacy_platform_services.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "platform_api/platform_services.h"
|
||||
|
||||
namespace pp::platform::legacy {
|
||||
|
||||
[[nodiscard]] PlatformServices& platform_services();
|
||||
|
||||
}
|
||||
448
src/platform_windows/windows_platform_services.cpp
Normal file
448
src/platform_windows/windows_platform_services.cpp
Normal file
@@ -0,0 +1,448 @@
|
||||
#include "pch.h"
|
||||
#include "platform_windows/windows_platform_services.h"
|
||||
|
||||
#include "log.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
#include <deque>
|
||||
#include <map>
|
||||
|
||||
extern HWND hWnd;
|
||||
extern std::deque<std::packaged_task<void()>> main_tasklist;
|
||||
extern std::mutex main_task_mutex;
|
||||
|
||||
void destroy_window();
|
||||
void async_lock();
|
||||
void async_unlock();
|
||||
void win32_async_swap();
|
||||
void win32_renderdoc_frame_start();
|
||||
void win32_renderdoc_frame_end();
|
||||
void win32_update_fps(int frames);
|
||||
void win32_update_stylus(float dt);
|
||||
|
||||
namespace {
|
||||
|
||||
static CONSOLE_SCREEN_BUFFER_INFO render_debug_console_info;
|
||||
|
||||
[[nodiscard]] GLenum debug_severity_notification() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::debug_severity_notification());
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum debug_severity_low() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::debug_severity_low());
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum debug_severity_medium() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::debug_severity_medium());
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum debug_severity_high() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::debug_severity_high());
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum debug_output_state() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::debug_output_state());
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum debug_output_synchronous_state() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::debug_output_synchronous_state());
|
||||
}
|
||||
|
||||
void handle_gl_callback(
|
||||
GLenum source,
|
||||
GLenum type,
|
||||
GLuint id,
|
||||
GLenum severity,
|
||||
GLsizei length,
|
||||
const GLchar* message,
|
||||
const void* userParam)
|
||||
{
|
||||
(void)source;
|
||||
(void)type;
|
||||
(void)id;
|
||||
(void)userParam;
|
||||
|
||||
static std::map<GLenum, WORD> colors = {
|
||||
{ debug_severity_notification(), static_cast<WORD>(8) },
|
||||
{ debug_severity_low(), static_cast<WORD>(8) },
|
||||
{ debug_severity_medium(), static_cast<WORD>(FOREGROUND_GREEN | FOREGROUND_INTENSITY) },
|
||||
{ debug_severity_high(), static_cast<WORD>(FOREGROUND_RED | FOREGROUND_INTENSITY) },
|
||||
};
|
||||
if (severity == debug_severity_high()
|
||||
|| severity == debug_severity_medium()
|
||||
|| severity == 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), render_debug_console_info.wAttributes);
|
||||
#ifdef _DEBUG
|
||||
if (severity == debug_severity_high())
|
||||
__debugbreak();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void show_cursor(bool visible)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(main_task_mutex);
|
||||
main_tasklist.emplace_back([=] {
|
||||
if (visible)
|
||||
while (ShowCursor(true) < 0);
|
||||
else
|
||||
while (ShowCursor(false) >= 0);
|
||||
});
|
||||
}
|
||||
|
||||
std::string clipboard_text()
|
||||
{
|
||||
std::string ret;
|
||||
if (OpenClipboard(hWnd))
|
||||
{
|
||||
if (HANDLE h = GetClipboardData(CF_TEXT))
|
||||
{
|
||||
if (char* s = static_cast<char*>(GlobalLock(h)))
|
||||
{
|
||||
ret = s;
|
||||
GlobalUnlock(h);
|
||||
}
|
||||
}
|
||||
CloseClipboard();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool set_clipboard_text(const std::string& s)
|
||||
{
|
||||
bool success = false;
|
||||
if (OpenClipboard(hWnd))
|
||||
{
|
||||
// owned by SetClipboardData
|
||||
if (HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, s.size() + 1))
|
||||
{
|
||||
if (char* p = static_cast<char*>(GlobalLock(h)))
|
||||
{
|
||||
std::copy(s.begin(), s.end(), p);
|
||||
p[s.size()] = 0;
|
||||
GlobalUnlock(h);
|
||||
success = true;
|
||||
}
|
||||
EmptyClipboard();
|
||||
SetClipboardData(CF_TEXT, h);
|
||||
}
|
||||
CloseClipboard();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
std::string open_file(const char* filter)
|
||||
{
|
||||
OPENFILENAMEA ofn;
|
||||
char fileName[MAX_PATH] = "";
|
||||
ZeroMemory(&ofn, sizeof(ofn));
|
||||
ofn.lStructSize = sizeof(OPENFILENAME);
|
||||
ofn.hwndOwner = hWnd;
|
||||
ofn.lpstrFilter = filter;
|
||||
ofn.lpstrFile = fileName;
|
||||
ofn.nMaxFile = MAX_PATH;
|
||||
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR;
|
||||
ofn.lpstrDefExt = "";
|
||||
ofn.lpstrInitialDir = "";
|
||||
if (GetOpenFileNameA(&ofn) != NULL)
|
||||
return fileName;
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string save_file(const char* filter)
|
||||
{
|
||||
OPENFILENAMEA ofn;
|
||||
char fileName[MAX_PATH] = "";
|
||||
ZeroMemory(&ofn, sizeof(ofn));
|
||||
ofn.lStructSize = sizeof(OPENFILENAME);
|
||||
ofn.hwndOwner = hWnd;
|
||||
ofn.lpstrFilter = filter;
|
||||
ofn.lpstrFile = fileName;
|
||||
ofn.nMaxFile = MAX_PATH;
|
||||
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR | OFN_OVERWRITEPROMPT;
|
||||
ofn.lpstrDefExt = "";
|
||||
ofn.lpstrInitialDir = "";
|
||||
if (GetSaveFileNameA(&ofn) != NULL)
|
||||
return fileName;
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string open_directory()
|
||||
{
|
||||
BROWSEINFOA bi;
|
||||
char Buffer[MAX_PATH];
|
||||
ZeroMemory(Buffer, MAX_PATH);
|
||||
ZeroMemory(&bi, sizeof(bi));
|
||||
bi.hwndOwner = hWnd;
|
||||
bi.pszDisplayName = Buffer;
|
||||
bi.lpszTitle = "Title";
|
||||
bi.ulFlags = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNONLYFSDIRS | BIF_SHAREABLE;
|
||||
LPCITEMIDLIST pFolder = SHBrowseForFolderA(&bi);
|
||||
if (pFolder == NULL)
|
||||
return "";
|
||||
if (!SHGetPathFromIDListA(pFolder, Buffer))
|
||||
return "";
|
||||
return Buffer;
|
||||
}
|
||||
|
||||
void invoke_selected_path(
|
||||
const std::string& path,
|
||||
const pp::platform::PickedPathCallback& callback)
|
||||
{
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
}
|
||||
|
||||
void ensure_directory(const std::string& path)
|
||||
{
|
||||
if (!PathFileExistsA(path.c_str()))
|
||||
CreateDirectoryA(path.c_str(), NULL);
|
||||
}
|
||||
|
||||
std::string build_supported_files_filter(const std::vector<std::string>& types)
|
||||
{
|
||||
std::string filter = "Supported Files (";
|
||||
bool first_type = true;
|
||||
for (const auto& t : types)
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
|
||||
first_type = false;
|
||||
}
|
||||
filter.append(")");
|
||||
filter.push_back(0);
|
||||
first_type = true;
|
||||
for (const auto& t : types)
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : ";") + "*." + t);
|
||||
first_type = false;
|
||||
}
|
||||
filter.push_back(0);
|
||||
return filter;
|
||||
}
|
||||
|
||||
class WindowsPlatformServices final : public pp::platform::PlatformServices {
|
||||
public:
|
||||
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths() override
|
||||
{
|
||||
std::string data_path;
|
||||
CHAR my_documents[MAX_PATH];
|
||||
HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
|
||||
if (SUCCEEDED(result))
|
||||
{
|
||||
data_path = std::string(my_documents) + "\\PanoPainter";
|
||||
ensure_directory(data_path);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHAR path[MAX_PATH];
|
||||
GetCurrentDirectoryA(sizeof(path), path);
|
||||
data_path = path;
|
||||
}
|
||||
|
||||
ensure_directory(data_path + "\\frames");
|
||||
ensure_directory(data_path + "\\brushes");
|
||||
ensure_directory(data_path + "\\brushes\\thumbs");
|
||||
ensure_directory(data_path + "\\patterns");
|
||||
ensure_directory(data_path + "\\patterns\\thumbs");
|
||||
ensure_directory(data_path + "\\settings");
|
||||
|
||||
return {
|
||||
data_path,
|
||||
data_path,
|
||||
data_path + "\\frames",
|
||||
{},
|
||||
};
|
||||
}
|
||||
|
||||
void log_stacktrace() override
|
||||
{
|
||||
}
|
||||
|
||||
void trigger_crash_test() override
|
||||
{
|
||||
__debugbreak();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string clipboard_text() override
|
||||
{
|
||||
return ::clipboard_text();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool set_clipboard_text(std::string_view text) override
|
||||
{
|
||||
return ::set_clipboard_text(std::string(text));
|
||||
}
|
||||
|
||||
void set_cursor_visible(bool visible) override
|
||||
{
|
||||
show_cursor(visible);
|
||||
}
|
||||
|
||||
void set_virtual_keyboard_visible(bool visible) override
|
||||
{
|
||||
(void)visible;
|
||||
}
|
||||
|
||||
void attach_ui_thread() override
|
||||
{
|
||||
}
|
||||
|
||||
void detach_ui_thread() override
|
||||
{
|
||||
}
|
||||
|
||||
void acquire_render_context() override
|
||||
{
|
||||
async_lock();
|
||||
glBindFramebuffer(
|
||||
static_cast<GLenum>(pp::renderer::gl::framebuffer_target()),
|
||||
static_cast<GLuint>(pp::renderer::gl::default_framebuffer_id()));
|
||||
}
|
||||
|
||||
void release_render_context() override
|
||||
{
|
||||
async_unlock();
|
||||
}
|
||||
|
||||
void present_render_context() override
|
||||
{
|
||||
win32_async_swap();
|
||||
}
|
||||
|
||||
void bind_default_render_target() override
|
||||
{
|
||||
glBindFramebuffer(
|
||||
static_cast<GLenum>(pp::renderer::gl::framebuffer_target()),
|
||||
pp::renderer::gl::default_framebuffer_id());
|
||||
}
|
||||
|
||||
void bind_main_render_target() override
|
||||
{
|
||||
bind_default_render_target();
|
||||
}
|
||||
|
||||
void apply_render_platform_hints() override
|
||||
{
|
||||
glEnable(static_cast<GLenum>(pp::renderer::gl::program_point_size_state()));
|
||||
glEnable(static_cast<GLenum>(pp::renderer::gl::line_smooth_state()));
|
||||
}
|
||||
|
||||
void install_render_debug_callback() override
|
||||
{
|
||||
if (!glDebugMessageCallback)
|
||||
return;
|
||||
|
||||
// colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c
|
||||
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &render_debug_console_info);
|
||||
glDebugMessageCallback(handle_gl_callback, nullptr);
|
||||
glEnable(debug_output_state());
|
||||
glEnable(debug_output_synchronous_state());
|
||||
}
|
||||
|
||||
void begin_render_capture_frame() override
|
||||
{
|
||||
win32_renderdoc_frame_start();
|
||||
}
|
||||
|
||||
void end_render_capture_frame() override
|
||||
{
|
||||
win32_renderdoc_frame_end();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool deletes_recorded_files_on_clear() override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear_recorded_files(std::string_view recording_path) override
|
||||
{
|
||||
(void)recording_path;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool enables_live_asset_reloading() override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void update_platform_frame(float delta_time_seconds) override
|
||||
{
|
||||
win32_update_stylus(delta_time_seconds);
|
||||
}
|
||||
|
||||
void report_rendered_frames(int frames) override
|
||||
{
|
||||
win32_update_fps(frames);
|
||||
}
|
||||
|
||||
void display_file(std::string_view path) override
|
||||
{
|
||||
(void)path;
|
||||
}
|
||||
|
||||
void share_file(std::string_view path) override
|
||||
{
|
||||
(void)path;
|
||||
}
|
||||
|
||||
void request_app_close() override
|
||||
{
|
||||
destroy_window();
|
||||
}
|
||||
|
||||
void pick_image(pp::platform::PickedPathCallback callback) override
|
||||
{
|
||||
const std::string path = open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png");
|
||||
invoke_selected_path(path, callback);
|
||||
}
|
||||
|
||||
void pick_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
|
||||
{
|
||||
const std::string filter = build_supported_files_filter(file_types);
|
||||
const std::string path = open_file(filter.c_str());
|
||||
invoke_selected_path(path, callback);
|
||||
}
|
||||
|
||||
void pick_save_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
|
||||
{
|
||||
const std::string filter = build_supported_files_filter(file_types);
|
||||
const std::string path = save_file(filter.c_str());
|
||||
invoke_selected_path(path, callback);
|
||||
}
|
||||
|
||||
void pick_directory(pp::platform::PickedPathCallback callback) override
|
||||
{
|
||||
const std::string path = open_directory();
|
||||
invoke_selected_path(path, callback);
|
||||
}
|
||||
|
||||
void save_prepared_file(
|
||||
std::string_view path,
|
||||
std::string_view suggested_name,
|
||||
pp::platform::PreparedFileCallback callback) override
|
||||
{
|
||||
(void)suggested_name;
|
||||
callback(std::string(path), false);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace pp::platform::windows {
|
||||
|
||||
PlatformServices& platform_services()
|
||||
{
|
||||
static WindowsPlatformServices services;
|
||||
return services;
|
||||
}
|
||||
|
||||
}
|
||||
9
src/platform_windows/windows_platform_services.h
Normal file
9
src/platform_windows/windows_platform_services.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "platform_api/platform_services.h"
|
||||
|
||||
namespace pp::platform::windows {
|
||||
|
||||
[[nodiscard]] PlatformServices& platform_services();
|
||||
|
||||
}
|
||||
830
src/renderer_api/recording_renderer.cpp
Normal file
830
src/renderer_api/recording_renderer.cpp
Normal file
@@ -0,0 +1,830 @@
|
||||
#include "renderer_api/recording_renderer.h"
|
||||
|
||||
#include <new>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::renderer {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] const char* non_null_name(const char* name) noexcept
|
||||
{
|
||||
return name == nullptr ? "" : name;
|
||||
}
|
||||
|
||||
void push_command(
|
||||
std::vector<RecordedRenderCommand>* commands,
|
||||
RecordedRenderCommand command)
|
||||
{
|
||||
if (commands != nullptr) {
|
||||
commands->push_back(command);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Resource, typename Interface, typename... Args>
|
||||
[[nodiscard]] pp::foundation::Result<std::unique_ptr<Interface>> make_recording_resource(
|
||||
Args&&... args) noexcept
|
||||
{
|
||||
auto resource = std::unique_ptr<Resource>(new (std::nothrow) Resource(std::forward<Args>(args)...));
|
||||
if (!resource) {
|
||||
return pp::foundation::Result<std::unique_ptr<Interface>>::failure(
|
||||
pp::foundation::Status::out_of_range("renderer resource allocation failed"));
|
||||
}
|
||||
|
||||
std::unique_ptr<Interface> erased = std::move(resource);
|
||||
return pp::foundation::Result<std::unique_ptr<Interface>>::success(std::move(erased));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RecordingTexture2D::RecordingTexture2D(TextureDesc desc) noexcept
|
||||
: desc_(desc)
|
||||
{
|
||||
}
|
||||
|
||||
TextureDesc RecordingTexture2D::desc() const noexcept
|
||||
{
|
||||
return desc_;
|
||||
}
|
||||
|
||||
RecordingRenderTarget::RecordingRenderTarget(TextureDesc color_desc) noexcept
|
||||
: color_desc_(color_desc)
|
||||
{
|
||||
}
|
||||
|
||||
TextureDesc RecordingRenderTarget::color_desc() const noexcept
|
||||
{
|
||||
return color_desc_;
|
||||
}
|
||||
|
||||
RecordingShaderProgram::RecordingShaderProgram(const char* debug_name) noexcept
|
||||
: debug_name_(non_null_name(debug_name))
|
||||
{
|
||||
}
|
||||
|
||||
const char* RecordingShaderProgram::debug_name() const noexcept
|
||||
{
|
||||
return debug_name_;
|
||||
}
|
||||
|
||||
RecordingMesh::RecordingMesh(MeshDesc desc) noexcept
|
||||
: desc_(desc)
|
||||
{
|
||||
}
|
||||
|
||||
MeshDesc RecordingMesh::desc() const noexcept
|
||||
{
|
||||
return desc_;
|
||||
}
|
||||
|
||||
RecordingReadbackBuffer::RecordingReadbackBuffer(std::uint64_t size_bytes) noexcept
|
||||
: size_bytes_(size_bytes)
|
||||
{
|
||||
}
|
||||
|
||||
std::uint64_t RecordingReadbackBuffer::size_bytes() const noexcept
|
||||
{
|
||||
return size_bytes_;
|
||||
}
|
||||
|
||||
RecordingCommandContext::RecordingCommandContext(std::vector<RecordedRenderCommand>& commands) noexcept
|
||||
: commands_(&commands)
|
||||
{
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::begin_render_pass(
|
||||
IRenderTarget& target,
|
||||
RenderPassDesc desc) noexcept
|
||||
{
|
||||
if (in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass is already active");
|
||||
}
|
||||
|
||||
active_target_ = target.color_desc();
|
||||
if (!has_texture_usage(active_target_.usage, TextureUsage::render_target)) {
|
||||
return pp::foundation::Status::invalid_argument("render target texture must allow render_target usage");
|
||||
}
|
||||
|
||||
const auto size_status = texture_byte_size(active_target_);
|
||||
if (!size_status.ok()) {
|
||||
return size_status.status();
|
||||
}
|
||||
|
||||
const auto render_pass_status = validate_render_pass_desc(desc);
|
||||
if (!render_pass_status.ok()) {
|
||||
return render_pass_status;
|
||||
}
|
||||
|
||||
in_render_pass_ = true;
|
||||
shader_bound_ = false;
|
||||
mesh_bound_ = false;
|
||||
active_mesh_ = MeshDesc {};
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::begin_render_pass,
|
||||
.target_desc = active_target_,
|
||||
.clear_color_enabled = desc.clear_color_enabled,
|
||||
.clear_color = desc.clear_color,
|
||||
.clear_depth_enabled = desc.clear_depth_enabled,
|
||||
.clear_depth = desc.clear_depth,
|
||||
.clear_stencil_enabled = desc.clear_stencil_enabled,
|
||||
.clear_stencil = desc.clear_stencil,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::set_viewport(Viewport viewport) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
|
||||
const auto status = validate_viewport(viewport, active_target_.extent);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::set_viewport,
|
||||
.viewport = viewport,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::set_scissor(ScissorRect scissor) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
|
||||
const auto status = validate_scissor(scissor, active_target_.extent);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::set_scissor,
|
||||
.scissor = scissor,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::set_blend_state(BlendState state) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
|
||||
const auto status = validate_blend_state(state);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::set_blend_state,
|
||||
.blend_state = state,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::set_depth_state(DepthState state) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
|
||||
const auto status = validate_depth_state(state);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::set_depth_state,
|
||||
.depth_state = state,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::bind_shader(IShaderProgram& shader) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
|
||||
shader_bound_ = true;
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::bind_shader,
|
||||
.name = shader.debug_name(),
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::set_shader_uniform(
|
||||
const char* name,
|
||||
std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
if (!shader_bound_) {
|
||||
return pp::foundation::Status::invalid_argument("shader must be bound before setting uniforms");
|
||||
}
|
||||
|
||||
const auto status = validate_shader_uniform_write(name, bytes);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::set_shader_uniform,
|
||||
.uniform_bytes = static_cast<std::uint64_t>(bytes.size()),
|
||||
.name = name,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::bind_texture(
|
||||
std::uint32_t slot,
|
||||
ITexture2D& texture) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
|
||||
const auto slot_status = validate_texture_slot(slot);
|
||||
if (!slot_status.ok()) {
|
||||
return slot_status;
|
||||
}
|
||||
|
||||
const auto desc = texture.desc();
|
||||
if (!has_texture_usage(desc.usage, TextureUsage::sampled)) {
|
||||
return pp::foundation::Status::invalid_argument("bound texture must allow sampled usage");
|
||||
}
|
||||
|
||||
const auto size_status = texture_byte_size(desc);
|
||||
if (!size_status.ok()) {
|
||||
return size_status.status();
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::bind_texture,
|
||||
.texture_desc = desc,
|
||||
.texture_slot = slot,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::bind_sampler(
|
||||
std::uint32_t slot,
|
||||
SamplerDesc sampler) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
|
||||
const auto slot_status = validate_texture_slot(slot);
|
||||
if (!slot_status.ok()) {
|
||||
return slot_status;
|
||||
}
|
||||
|
||||
const auto sampler_status = validate_sampler_desc(sampler);
|
||||
if (!sampler_status.ok()) {
|
||||
return sampler_status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::bind_sampler,
|
||||
.sampler_desc = sampler,
|
||||
.sampler_slot = slot,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::bind_mesh(IMesh& mesh) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
|
||||
const auto desc = mesh.desc();
|
||||
const auto status = validate_mesh_desc(desc);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
mesh_bound_ = true;
|
||||
active_mesh_ = desc;
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::bind_mesh,
|
||||
.mesh_desc = desc,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::draw(DrawDesc desc) noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render pass has not begun");
|
||||
}
|
||||
if (!shader_bound_) {
|
||||
return pp::foundation::Status::invalid_argument("shader must be bound before draw");
|
||||
}
|
||||
if (!mesh_bound_) {
|
||||
return pp::foundation::Status::invalid_argument("mesh must be bound before draw");
|
||||
}
|
||||
|
||||
const auto draw_status = validate_draw_desc(active_mesh_, desc);
|
||||
if (!draw_status.ok()) {
|
||||
return draw_status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::draw,
|
||||
.mesh_desc = active_mesh_,
|
||||
.draw_desc = desc,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::read_texture(
|
||||
ITexture2D& texture,
|
||||
ReadbackRegion region,
|
||||
IReadbackBuffer& destination) noexcept
|
||||
{
|
||||
if (in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("readback must be outside a render pass");
|
||||
}
|
||||
|
||||
const auto desc = texture.desc();
|
||||
if (!has_texture_usage(desc.usage, TextureUsage::readback_source)) {
|
||||
return pp::foundation::Status::invalid_argument("readback texture must allow readback_source usage");
|
||||
}
|
||||
|
||||
const auto bytes = readback_byte_size(desc, region);
|
||||
if (!bytes) {
|
||||
return bytes.status();
|
||||
}
|
||||
|
||||
if (destination.size_bytes() < bytes.value()) {
|
||||
return pp::foundation::Status::out_of_range("readback buffer is too small");
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::read_texture,
|
||||
.texture_desc = desc,
|
||||
.readback_region = region,
|
||||
.readback_bytes = bytes.value(),
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::upload_texture(
|
||||
ITexture2D& texture,
|
||||
ReadbackRegion region,
|
||||
std::span<const std::byte> rgba_or_channel_bytes) noexcept
|
||||
{
|
||||
if (in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("texture upload must be outside a render pass");
|
||||
}
|
||||
|
||||
const auto desc = texture.desc();
|
||||
if (!has_texture_usage(desc.usage, TextureUsage::upload_destination)) {
|
||||
return pp::foundation::Status::invalid_argument("texture upload destination must allow upload_destination usage");
|
||||
}
|
||||
|
||||
const auto bytes = readback_byte_size(desc, region);
|
||||
if (!bytes) {
|
||||
return bytes.status();
|
||||
}
|
||||
|
||||
if (rgba_or_channel_bytes.size() != bytes.value()) {
|
||||
return pp::foundation::Status::invalid_argument("texture upload byte size does not match the region");
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::upload_texture,
|
||||
.texture_desc = desc,
|
||||
.readback_region = region,
|
||||
.upload_bytes = bytes.value(),
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::generate_mipmaps(ITexture2D& texture) noexcept
|
||||
{
|
||||
if (in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("mipmap generation must be outside a render pass");
|
||||
}
|
||||
|
||||
const auto desc = texture.desc();
|
||||
const auto desc_status = validate_mipmap_generation_desc(desc);
|
||||
if (!desc_status.ok()) {
|
||||
return desc_status;
|
||||
}
|
||||
|
||||
const auto bytes = texture_byte_size(desc);
|
||||
if (!bytes.ok()) {
|
||||
return bytes.status();
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::generate_mipmaps,
|
||||
.texture_desc = desc,
|
||||
.generated_mip_levels = desc.mip_levels,
|
||||
.generated_mip_bytes = bytes.value(),
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::transition_texture(
|
||||
ITexture2D& texture,
|
||||
TextureState before,
|
||||
TextureState after) noexcept
|
||||
{
|
||||
if (in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("texture transition must be outside a render pass");
|
||||
}
|
||||
|
||||
const auto desc = texture.desc();
|
||||
const auto desc_status = validate_texture_transition_desc(desc, before, after);
|
||||
if (!desc_status.ok()) {
|
||||
return desc_status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::transition_texture,
|
||||
.texture_desc = desc,
|
||||
.before_state = before,
|
||||
.after_state = after,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::copy_texture(
|
||||
ITexture2D& source,
|
||||
ReadbackRegion source_region,
|
||||
ITexture2D& destination,
|
||||
ReadbackRegion destination_region) noexcept
|
||||
{
|
||||
if (in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("texture copy must be outside a render pass");
|
||||
}
|
||||
|
||||
const auto source_desc = source.desc();
|
||||
const auto destination_desc = destination.desc();
|
||||
const auto desc_status = validate_texture_copy_descs(
|
||||
source_desc,
|
||||
source_region,
|
||||
destination_desc,
|
||||
destination_region);
|
||||
if (!desc_status.ok()) {
|
||||
return desc_status;
|
||||
}
|
||||
|
||||
const auto source_bytes = readback_byte_size(source_desc, source_region);
|
||||
if (!source_bytes.ok()) {
|
||||
return source_bytes.status();
|
||||
}
|
||||
|
||||
const auto destination_bytes = readback_byte_size(destination_desc, destination_region);
|
||||
if (!destination_bytes.ok()) {
|
||||
return destination_bytes.status();
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::copy_texture,
|
||||
.source_desc = source_desc,
|
||||
.destination_desc = destination_desc,
|
||||
.source_region = source_region,
|
||||
.destination_region = destination_region,
|
||||
.copy_source_bytes = source_bytes.value(),
|
||||
.copy_destination_bytes = destination_bytes.value(),
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::capture_frame(
|
||||
IRenderTarget& target,
|
||||
IReadbackBuffer& destination) noexcept
|
||||
{
|
||||
if (in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("frame capture must be outside a render pass");
|
||||
}
|
||||
|
||||
const auto desc = target.color_desc();
|
||||
const auto bytes = frame_capture_byte_size(desc);
|
||||
if (!bytes) {
|
||||
return bytes.status();
|
||||
}
|
||||
|
||||
if (destination.size_bytes() < bytes.value()) {
|
||||
return pp::foundation::Status::out_of_range("frame capture buffer is too small");
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::capture_frame,
|
||||
.target_desc = desc,
|
||||
.capture_bytes = bytes.value(),
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::blit_render_target(
|
||||
IRenderTarget& source,
|
||||
ReadbackRegion source_region,
|
||||
IRenderTarget& destination,
|
||||
ReadbackRegion destination_region,
|
||||
BlitFilter filter) noexcept
|
||||
{
|
||||
if (in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("render target blit must be outside a render pass");
|
||||
}
|
||||
|
||||
const auto source_desc = source.color_desc();
|
||||
const auto destination_desc = destination.color_desc();
|
||||
const auto desc_status = validate_blit_descs(source_desc, destination_desc);
|
||||
if (!desc_status.ok()) {
|
||||
return desc_status;
|
||||
}
|
||||
|
||||
const auto filter_status = validate_blit_filter(filter);
|
||||
if (!filter_status.ok()) {
|
||||
return filter_status;
|
||||
}
|
||||
|
||||
const auto source_bytes = readback_byte_size(source_desc, source_region);
|
||||
if (!source_bytes) {
|
||||
return source_bytes.status();
|
||||
}
|
||||
|
||||
const auto destination_bytes = readback_byte_size(destination_desc, destination_region);
|
||||
if (!destination_bytes) {
|
||||
return destination_bytes.status();
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::blit_render_target,
|
||||
.source_desc = source_desc,
|
||||
.destination_desc = destination_desc,
|
||||
.source_region = source_region,
|
||||
.destination_region = destination_region,
|
||||
.blit_filter = filter,
|
||||
.blit_source_bytes = source_bytes.value(),
|
||||
.blit_destination_bytes = destination_bytes.value(),
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
void RecordingCommandContext::end_render_pass() noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
return;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::end_render_pass,
|
||||
});
|
||||
in_render_pass_ = false;
|
||||
shader_bound_ = false;
|
||||
mesh_bound_ = false;
|
||||
active_mesh_ = MeshDesc {};
|
||||
}
|
||||
|
||||
bool RecordingCommandContext::in_render_pass() const noexcept
|
||||
{
|
||||
return in_render_pass_;
|
||||
}
|
||||
|
||||
void RecordingCommandContext::reset() noexcept
|
||||
{
|
||||
active_target_ = TextureDesc {};
|
||||
active_mesh_ = MeshDesc {};
|
||||
in_render_pass_ = false;
|
||||
shader_bound_ = false;
|
||||
mesh_bound_ = false;
|
||||
}
|
||||
|
||||
RecordingRenderTrace::RecordingRenderTrace(std::vector<RecordedRenderCommand>& commands) noexcept
|
||||
: commands_(&commands)
|
||||
{
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingRenderTrace::marker(const char* component, const char* name) noexcept
|
||||
{
|
||||
const auto status = validate_trace_label(component, name);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::trace_marker,
|
||||
.component = non_null_name(component),
|
||||
.name = non_null_name(name),
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingRenderTrace::begin_scope(const char* component, const char* name) noexcept
|
||||
{
|
||||
const auto status = validate_trace_label(component, name);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::trace_begin_scope,
|
||||
.component = non_null_name(component),
|
||||
.name = non_null_name(name),
|
||||
});
|
||||
++scope_depth_;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingRenderTrace::end_scope() noexcept
|
||||
{
|
||||
if (scope_depth_ == 0U) {
|
||||
return pp::foundation::Status::invalid_argument("trace scope has not begun");
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::trace_end_scope,
|
||||
});
|
||||
--scope_depth_;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
void RecordingRenderTrace::reset() noexcept
|
||||
{
|
||||
scope_depth_ = 0U;
|
||||
}
|
||||
|
||||
RecordingRenderDevice::RecordingRenderDevice() noexcept
|
||||
: context_(commands_)
|
||||
, trace_(commands_)
|
||||
{
|
||||
}
|
||||
|
||||
const char* RecordingRenderDevice::backend_name() const noexcept
|
||||
{
|
||||
return "recording";
|
||||
}
|
||||
|
||||
RenderDeviceFeatures RecordingRenderDevice::features() const noexcept
|
||||
{
|
||||
return RenderDeviceFeatures {
|
||||
.explicit_texture_transitions = true,
|
||||
.texture_copy = true,
|
||||
.render_target_blit = true,
|
||||
.frame_capture = true,
|
||||
};
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::unique_ptr<ITexture2D>> RecordingRenderDevice::create_texture(
|
||||
TextureDesc desc) noexcept
|
||||
{
|
||||
const auto desc_status = validate_texture_desc(desc);
|
||||
if (!desc_status.ok()) {
|
||||
return pp::foundation::Result<std::unique_ptr<ITexture2D>>::failure(desc_status);
|
||||
}
|
||||
|
||||
const auto bytes = texture_byte_size(desc);
|
||||
if (!bytes.ok()) {
|
||||
return pp::foundation::Result<std::unique_ptr<ITexture2D>>::failure(bytes.status());
|
||||
}
|
||||
|
||||
return make_recording_resource<RecordingTexture2D, ITexture2D>(desc);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::unique_ptr<IRenderTarget>> RecordingRenderDevice::create_render_target(
|
||||
TextureDesc color_desc) noexcept
|
||||
{
|
||||
if (!has_texture_usage(color_desc.usage, TextureUsage::render_target)) {
|
||||
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::failure(
|
||||
pp::foundation::Status::invalid_argument("render target texture must allow render_target usage"));
|
||||
}
|
||||
|
||||
const auto desc_status = validate_texture_desc(color_desc);
|
||||
if (!desc_status.ok()) {
|
||||
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::failure(desc_status);
|
||||
}
|
||||
|
||||
const auto bytes = texture_byte_size(color_desc);
|
||||
if (!bytes.ok()) {
|
||||
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::failure(bytes.status());
|
||||
}
|
||||
|
||||
return make_recording_resource<RecordingRenderTarget, IRenderTarget>(color_desc);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::unique_ptr<IShaderProgram>> RecordingRenderDevice::create_shader_program(
|
||||
ShaderProgramDesc desc) noexcept
|
||||
{
|
||||
const auto status = validate_shader_program_desc(desc);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<std::unique_ptr<IShaderProgram>>::failure(status);
|
||||
}
|
||||
|
||||
return make_recording_resource<RecordingShaderProgram, IShaderProgram>(desc.debug_name);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::unique_ptr<IMesh>> RecordingRenderDevice::create_mesh(
|
||||
MeshDesc desc) noexcept
|
||||
{
|
||||
const auto status = validate_mesh_desc(desc);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<std::unique_ptr<IMesh>>::failure(status);
|
||||
}
|
||||
|
||||
return make_recording_resource<RecordingMesh, IMesh>(desc);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::unique_ptr<IReadbackBuffer>> RecordingRenderDevice::create_readback_buffer(
|
||||
std::uint64_t size_bytes) noexcept
|
||||
{
|
||||
if (size_bytes == 0U) {
|
||||
return pp::foundation::Result<std::unique_ptr<IReadbackBuffer>>::failure(
|
||||
pp::foundation::Status::invalid_argument("readback buffer size must be greater than zero"));
|
||||
}
|
||||
|
||||
if (size_bytes > max_texture_bytes) {
|
||||
return pp::foundation::Result<std::unique_ptr<IReadbackBuffer>>::failure(
|
||||
pp::foundation::Status::out_of_range("readback buffer size exceeds the configured limit"));
|
||||
}
|
||||
|
||||
return make_recording_resource<RecordingReadbackBuffer, IReadbackBuffer>(size_bytes);
|
||||
}
|
||||
|
||||
ICommandContext& RecordingRenderDevice::immediate_context() noexcept
|
||||
{
|
||||
return context_;
|
||||
}
|
||||
|
||||
IRenderTrace* RecordingRenderDevice::trace() noexcept
|
||||
{
|
||||
return &trace_;
|
||||
}
|
||||
|
||||
std::span<const RecordedRenderCommand> RecordingRenderDevice::commands() const noexcept
|
||||
{
|
||||
return commands_;
|
||||
}
|
||||
|
||||
void RecordingRenderDevice::clear() noexcept
|
||||
{
|
||||
commands_.clear();
|
||||
context_.reset();
|
||||
trace_.reset();
|
||||
}
|
||||
|
||||
const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case RecordedRenderCommandKind::begin_render_pass:
|
||||
return "begin_render_pass";
|
||||
case RecordedRenderCommandKind::set_viewport:
|
||||
return "set_viewport";
|
||||
case RecordedRenderCommandKind::set_scissor:
|
||||
return "set_scissor";
|
||||
case RecordedRenderCommandKind::set_blend_state:
|
||||
return "set_blend_state";
|
||||
case RecordedRenderCommandKind::set_depth_state:
|
||||
return "set_depth_state";
|
||||
case RecordedRenderCommandKind::bind_shader:
|
||||
return "bind_shader";
|
||||
case RecordedRenderCommandKind::set_shader_uniform:
|
||||
return "set_shader_uniform";
|
||||
case RecordedRenderCommandKind::bind_texture:
|
||||
return "bind_texture";
|
||||
case RecordedRenderCommandKind::bind_sampler:
|
||||
return "bind_sampler";
|
||||
case RecordedRenderCommandKind::bind_mesh:
|
||||
return "bind_mesh";
|
||||
case RecordedRenderCommandKind::draw:
|
||||
return "draw";
|
||||
case RecordedRenderCommandKind::upload_texture:
|
||||
return "upload_texture";
|
||||
case RecordedRenderCommandKind::generate_mipmaps:
|
||||
return "generate_mipmaps";
|
||||
case RecordedRenderCommandKind::transition_texture:
|
||||
return "transition_texture";
|
||||
case RecordedRenderCommandKind::copy_texture:
|
||||
return "copy_texture";
|
||||
case RecordedRenderCommandKind::read_texture:
|
||||
return "read_texture";
|
||||
case RecordedRenderCommandKind::capture_frame:
|
||||
return "capture_frame";
|
||||
case RecordedRenderCommandKind::blit_render_target:
|
||||
return "blit_render_target";
|
||||
case RecordedRenderCommandKind::end_render_pass:
|
||||
return "end_render_pass";
|
||||
case RecordedRenderCommandKind::trace_marker:
|
||||
return "trace_marker";
|
||||
case RecordedRenderCommandKind::trace_begin_scope:
|
||||
return "trace_begin_scope";
|
||||
case RecordedRenderCommandKind::trace_end_scope:
|
||||
return "trace_end_scope";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
229
src/renderer_api/recording_renderer.h
Normal file
229
src/renderer_api/recording_renderer.h
Normal file
@@ -0,0 +1,229 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer_api/renderer_api.h"
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::renderer {
|
||||
|
||||
enum class RecordedRenderCommandKind : std::uint8_t {
|
||||
begin_render_pass,
|
||||
set_viewport,
|
||||
set_scissor,
|
||||
set_blend_state,
|
||||
set_depth_state,
|
||||
bind_shader,
|
||||
set_shader_uniform,
|
||||
bind_texture,
|
||||
bind_sampler,
|
||||
bind_mesh,
|
||||
draw,
|
||||
upload_texture,
|
||||
generate_mipmaps,
|
||||
transition_texture,
|
||||
copy_texture,
|
||||
read_texture,
|
||||
capture_frame,
|
||||
blit_render_target,
|
||||
end_render_pass,
|
||||
trace_marker,
|
||||
trace_begin_scope,
|
||||
trace_end_scope,
|
||||
};
|
||||
|
||||
struct RecordedRenderCommand {
|
||||
RecordedRenderCommandKind kind = RecordedRenderCommandKind::draw;
|
||||
TextureDesc target_desc {};
|
||||
bool clear_color_enabled = false;
|
||||
ClearColor clear_color {};
|
||||
bool clear_depth_enabled = false;
|
||||
float clear_depth = 1.0F;
|
||||
bool clear_stencil_enabled = false;
|
||||
std::uint8_t clear_stencil = 0;
|
||||
Viewport viewport {};
|
||||
ScissorRect scissor {};
|
||||
BlendState blend_state {};
|
||||
DepthState depth_state {};
|
||||
MeshDesc mesh_desc {};
|
||||
DrawDesc draw_desc {};
|
||||
TextureDesc texture_desc {};
|
||||
std::uint32_t texture_slot = 0;
|
||||
SamplerDesc sampler_desc {};
|
||||
std::uint32_t sampler_slot = 0;
|
||||
TextureDesc source_desc {};
|
||||
TextureDesc destination_desc {};
|
||||
TextureState before_state = TextureState::undefined;
|
||||
TextureState after_state = TextureState::undefined;
|
||||
ReadbackRegion readback_region {};
|
||||
ReadbackRegion source_region {};
|
||||
ReadbackRegion destination_region {};
|
||||
BlitFilter blit_filter = BlitFilter::nearest;
|
||||
std::uint64_t upload_bytes = 0;
|
||||
std::uint32_t generated_mip_levels = 0;
|
||||
std::uint64_t generated_mip_bytes = 0;
|
||||
std::uint64_t copy_source_bytes = 0;
|
||||
std::uint64_t copy_destination_bytes = 0;
|
||||
std::uint64_t readback_bytes = 0;
|
||||
std::uint64_t capture_bytes = 0;
|
||||
std::uint64_t blit_source_bytes = 0;
|
||||
std::uint64_t blit_destination_bytes = 0;
|
||||
std::uint64_t uniform_bytes = 0;
|
||||
const char* component = "";
|
||||
const char* name = "";
|
||||
};
|
||||
|
||||
class RecordingTexture2D final : public ITexture2D {
|
||||
public:
|
||||
explicit RecordingTexture2D(TextureDesc desc) noexcept;
|
||||
[[nodiscard]] TextureDesc desc() const noexcept override;
|
||||
|
||||
private:
|
||||
TextureDesc desc_ {};
|
||||
};
|
||||
|
||||
class RecordingRenderTarget final : public IRenderTarget {
|
||||
public:
|
||||
explicit RecordingRenderTarget(TextureDesc color_desc) noexcept;
|
||||
[[nodiscard]] TextureDesc color_desc() const noexcept override;
|
||||
|
||||
private:
|
||||
TextureDesc color_desc_ {};
|
||||
};
|
||||
|
||||
class RecordingShaderProgram final : public IShaderProgram {
|
||||
public:
|
||||
explicit RecordingShaderProgram(const char* debug_name) noexcept;
|
||||
[[nodiscard]] const char* debug_name() const noexcept override;
|
||||
|
||||
private:
|
||||
const char* debug_name_ = "";
|
||||
};
|
||||
|
||||
class RecordingMesh final : public IMesh {
|
||||
public:
|
||||
explicit RecordingMesh(MeshDesc desc) noexcept;
|
||||
[[nodiscard]] MeshDesc desc() const noexcept override;
|
||||
|
||||
private:
|
||||
MeshDesc desc_ {};
|
||||
};
|
||||
|
||||
class RecordingReadbackBuffer final : public IReadbackBuffer {
|
||||
public:
|
||||
explicit RecordingReadbackBuffer(std::uint64_t size_bytes) noexcept;
|
||||
[[nodiscard]] std::uint64_t size_bytes() const noexcept override;
|
||||
|
||||
private:
|
||||
std::uint64_t size_bytes_ = 0;
|
||||
};
|
||||
|
||||
class RecordingCommandContext final : public ICommandContext {
|
||||
public:
|
||||
explicit RecordingCommandContext(std::vector<RecordedRenderCommand>& commands) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Status begin_render_pass(
|
||||
IRenderTarget& target,
|
||||
RenderPassDesc desc) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status set_viewport(Viewport viewport) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status set_scissor(ScissorRect scissor) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status set_blend_state(BlendState state) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status set_depth_state(DepthState state) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status set_shader_uniform(
|
||||
const char* name,
|
||||
std::span<const std::byte> bytes) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status bind_texture(
|
||||
std::uint32_t slot,
|
||||
ITexture2D& texture) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status bind_sampler(
|
||||
std::uint32_t slot,
|
||||
SamplerDesc sampler) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status bind_mesh(IMesh& mesh) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status draw(DrawDesc desc) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status read_texture(
|
||||
ITexture2D& texture,
|
||||
ReadbackRegion region,
|
||||
IReadbackBuffer& destination) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status upload_texture(
|
||||
ITexture2D& texture,
|
||||
ReadbackRegion region,
|
||||
std::span<const std::byte> rgba_or_channel_bytes) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status generate_mipmaps(
|
||||
ITexture2D& texture) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status transition_texture(
|
||||
ITexture2D& texture,
|
||||
TextureState before,
|
||||
TextureState after) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status copy_texture(
|
||||
ITexture2D& source,
|
||||
ReadbackRegion source_region,
|
||||
ITexture2D& destination,
|
||||
ReadbackRegion destination_region) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status capture_frame(
|
||||
IRenderTarget& target,
|
||||
IReadbackBuffer& destination) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status blit_render_target(
|
||||
IRenderTarget& source,
|
||||
ReadbackRegion source_region,
|
||||
IRenderTarget& destination,
|
||||
ReadbackRegion destination_region,
|
||||
BlitFilter filter) noexcept override;
|
||||
void end_render_pass() noexcept override;
|
||||
|
||||
[[nodiscard]] bool in_render_pass() const noexcept;
|
||||
void reset() noexcept;
|
||||
|
||||
private:
|
||||
std::vector<RecordedRenderCommand>* commands_ = nullptr;
|
||||
TextureDesc active_target_ {};
|
||||
MeshDesc active_mesh_ {};
|
||||
bool in_render_pass_ = false;
|
||||
bool shader_bound_ = false;
|
||||
bool mesh_bound_ = false;
|
||||
};
|
||||
|
||||
class RecordingRenderTrace final : public IRenderTrace {
|
||||
public:
|
||||
explicit RecordingRenderTrace(std::vector<RecordedRenderCommand>& commands) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status marker(const char* component, const char* name) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status begin_scope(const char* component, const char* name) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status end_scope() noexcept override;
|
||||
void reset() noexcept;
|
||||
|
||||
private:
|
||||
std::vector<RecordedRenderCommand>* commands_ = nullptr;
|
||||
std::uint32_t scope_depth_ = 0;
|
||||
};
|
||||
|
||||
class RecordingRenderDevice final : public IRenderDevice {
|
||||
public:
|
||||
RecordingRenderDevice() noexcept;
|
||||
|
||||
[[nodiscard]] const char* backend_name() const noexcept override;
|
||||
[[nodiscard]] RenderDeviceFeatures features() const noexcept override;
|
||||
[[nodiscard]] pp::foundation::Result<std::unique_ptr<ITexture2D>> create_texture(
|
||||
TextureDesc desc) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IRenderTarget>> create_render_target(
|
||||
TextureDesc color_desc) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IShaderProgram>> create_shader_program(
|
||||
ShaderProgramDesc desc) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IMesh>> create_mesh(
|
||||
MeshDesc desc) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IReadbackBuffer>> create_readback_buffer(
|
||||
std::uint64_t size_bytes) noexcept override;
|
||||
[[nodiscard]] ICommandContext& immediate_context() noexcept override;
|
||||
[[nodiscard]] IRenderTrace* trace() noexcept override;
|
||||
|
||||
[[nodiscard]] std::span<const RecordedRenderCommand> commands() const noexcept;
|
||||
void clear() noexcept;
|
||||
|
||||
private:
|
||||
std::vector<RecordedRenderCommand> commands_;
|
||||
RecordingCommandContext context_;
|
||||
RecordingRenderTrace trace_;
|
||||
};
|
||||
|
||||
[[nodiscard]] const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) noexcept;
|
||||
|
||||
}
|
||||
@@ -12,6 +12,20 @@ namespace {
|
||||
return text == nullptr || text[0] == '\0';
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t bounded_c_string_length(const char* text, std::size_t limit) noexcept
|
||||
{
|
||||
if (text == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::size_t length = 0;
|
||||
while (length <= limit && text[length] != '\0') {
|
||||
++length;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_shader_stage_source(
|
||||
ShaderStageSource source,
|
||||
const char* stage_name) noexcept
|
||||
@@ -31,6 +45,18 @@ namespace {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] Extent2D mip_level_extent(Extent2D extent, std::uint32_t level) noexcept
|
||||
{
|
||||
auto width = extent.width;
|
||||
auto height = extent.height;
|
||||
for (std::uint32_t index = 0; index < level; ++index) {
|
||||
width = width > 1U ? width / 2U : 1U;
|
||||
height = height > 1U ? height / 2U : 1U;
|
||||
}
|
||||
|
||||
return Extent2D { .width = width, .height = height };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::uint32_t bytes_per_pixel(TextureFormat format) noexcept
|
||||
@@ -47,6 +73,29 @@ std::uint32_t bytes_per_pixel(TextureFormat format) noexcept
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint32_t max_mip_levels_for_extent(Extent2D extent) noexcept
|
||||
{
|
||||
if (extent.width == 0U || extent.height == 0U) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto dimension = extent.width > extent.height ? extent.width : extent.height;
|
||||
std::uint32_t levels = 1;
|
||||
while (dimension > 1U && levels < max_texture_mip_levels) {
|
||||
dimension /= 2U;
|
||||
++levels;
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
bool has_texture_usage(TextureUsage usage, TextureUsage required) noexcept
|
||||
{
|
||||
const auto usage_bits = static_cast<std::uint32_t>(usage);
|
||||
const auto required_bits = static_cast<std::uint32_t>(required);
|
||||
return required_bits != 0U && (usage_bits & required_bits) == required_bits;
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_extent(Extent2D extent) noexcept
|
||||
{
|
||||
if (extent.width == 0 || extent.height == 0) {
|
||||
@@ -60,21 +109,81 @@ pp::foundation::Status validate_extent(Extent2D extent) noexcept
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::uint64_t> texture_byte_size(TextureDesc desc) noexcept
|
||||
pp::foundation::Status validate_texture_usage(TextureUsage usage) noexcept
|
||||
{
|
||||
constexpr auto allowed_usage = TextureUsage::sampled
|
||||
| TextureUsage::render_target
|
||||
| TextureUsage::upload_destination
|
||||
| TextureUsage::readback_source
|
||||
| TextureUsage::copy_source
|
||||
| TextureUsage::copy_destination;
|
||||
|
||||
const auto usage_bits = static_cast<std::uint32_t>(usage);
|
||||
const auto allowed_bits = static_cast<std::uint32_t>(allowed_usage);
|
||||
if (usage_bits == 0U) {
|
||||
return pp::foundation::Status::invalid_argument("texture usage must not be empty");
|
||||
}
|
||||
|
||||
if ((usage_bits & ~allowed_bits) != 0U) {
|
||||
return pp::foundation::Status::invalid_argument("texture usage contains unsupported flags");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_resource_label(const char* label) noexcept
|
||||
{
|
||||
if (label == nullptr) {
|
||||
return pp::foundation::Status::invalid_argument("resource label must not be null");
|
||||
}
|
||||
|
||||
if (bounded_c_string_length(label, max_resource_label_bytes) > max_resource_label_bytes) {
|
||||
return pp::foundation::Status::out_of_range("resource label exceeds the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_texture_desc(TextureDesc desc) noexcept
|
||||
{
|
||||
const auto extent_status = validate_extent(desc.extent);
|
||||
if (!extent_status.ok()) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(extent_status);
|
||||
return extent_status;
|
||||
}
|
||||
|
||||
if (bytes_per_pixel(desc.format) == 0U) {
|
||||
return pp::foundation::Status::invalid_argument("texture format is not supported");
|
||||
}
|
||||
|
||||
if (desc.mip_levels == 0U) {
|
||||
return pp::foundation::Status::invalid_argument("texture mip level count must be greater than zero");
|
||||
}
|
||||
|
||||
if (desc.mip_levels > max_mip_levels_for_extent(desc.extent)) {
|
||||
return pp::foundation::Status::out_of_range("texture mip level count exceeds the texture extent");
|
||||
}
|
||||
|
||||
const auto usage_status = validate_texture_usage(desc.usage);
|
||||
if (!usage_status.ok()) {
|
||||
return usage_status;
|
||||
}
|
||||
|
||||
return validate_resource_label(desc.debug_name);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::uint64_t> texture_byte_size(TextureDesc desc) noexcept
|
||||
{
|
||||
const auto desc_status = validate_texture_desc(desc);
|
||||
if (!desc_status.ok()) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(desc_status);
|
||||
}
|
||||
|
||||
const auto bpp = static_cast<std::uint64_t>(bytes_per_pixel(desc.format));
|
||||
if (bpp == 0) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("texture format is not supported"));
|
||||
}
|
||||
|
||||
const auto width = static_cast<std::uint64_t>(desc.extent.width);
|
||||
const auto height = static_cast<std::uint64_t>(desc.extent.height);
|
||||
std::uint64_t bytes = 0;
|
||||
for (std::uint32_t level = 0; level < desc.mip_levels; ++level) {
|
||||
const auto level_extent = mip_level_extent(desc.extent, level);
|
||||
const auto width = static_cast<std::uint64_t>(level_extent.width);
|
||||
const auto height = static_cast<std::uint64_t>(level_extent.height);
|
||||
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::out_of_range("texture size overflows uint64"));
|
||||
@@ -86,7 +195,14 @@ pp::foundation::Result<std::uint64_t> texture_byte_size(TextureDesc desc) noexce
|
||||
pp::foundation::Status::out_of_range("texture byte size overflows uint64"));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * bpp;
|
||||
const auto level_bytes = pixels * bpp;
|
||||
if (bytes > std::numeric_limits<std::uint64_t>::max() - level_bytes) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::out_of_range("texture byte size overflows uint64"));
|
||||
}
|
||||
bytes += level_bytes;
|
||||
}
|
||||
|
||||
if (bytes > max_texture_bytes) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::out_of_range("texture byte size exceeds the configured limit"));
|
||||
@@ -131,8 +247,199 @@ pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_exte
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_scissor(ScissorRect scissor, Extent2D target_extent) noexcept
|
||||
{
|
||||
const auto extent_status = validate_extent(target_extent);
|
||||
if (!extent_status.ok()) {
|
||||
return extent_status;
|
||||
}
|
||||
|
||||
if (!scissor.enabled) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
if (scissor.x < 0 || scissor.y < 0) {
|
||||
return pp::foundation::Status::invalid_argument("scissor origin must be non-negative");
|
||||
}
|
||||
|
||||
if (scissor.width == 0 || scissor.height == 0) {
|
||||
return pp::foundation::Status::invalid_argument("scissor size must be greater than zero");
|
||||
}
|
||||
|
||||
const auto x = static_cast<std::uint32_t>(scissor.x);
|
||||
const auto y = static_cast<std::uint32_t>(scissor.y);
|
||||
if (x > target_extent.width || y > target_extent.height) {
|
||||
return pp::foundation::Status::out_of_range("scissor origin is outside the render target");
|
||||
}
|
||||
|
||||
if (scissor.width > target_extent.width - x || scissor.height > target_extent.height - y) {
|
||||
return pp::foundation::Status::out_of_range("scissor exceeds render target bounds");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_render_pass_desc(RenderPassDesc desc) noexcept
|
||||
{
|
||||
if (desc.clear_color_enabled
|
||||
&& (!std::isfinite(desc.clear_color.r)
|
||||
|| !std::isfinite(desc.clear_color.g)
|
||||
|| !std::isfinite(desc.clear_color.b)
|
||||
|| !std::isfinite(desc.clear_color.a))) {
|
||||
return pp::foundation::Status::invalid_argument("render pass clear color must be finite");
|
||||
}
|
||||
|
||||
if (desc.clear_depth_enabled && !std::isfinite(desc.clear_depth)) {
|
||||
return pp::foundation::Status::invalid_argument("render pass clear depth must be finite");
|
||||
}
|
||||
|
||||
if (desc.clear_depth_enabled && (desc.clear_depth < 0.0F || desc.clear_depth > 1.0F)) {
|
||||
return pp::foundation::Status::out_of_range("render pass clear depth must be within 0..1");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_blend_factor(BlendFactor factor) noexcept
|
||||
{
|
||||
switch (factor) {
|
||||
case BlendFactor::zero:
|
||||
case BlendFactor::one:
|
||||
case BlendFactor::source_alpha:
|
||||
case BlendFactor::one_minus_source_alpha:
|
||||
case BlendFactor::destination_alpha:
|
||||
case BlendFactor::one_minus_destination_alpha:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("blend factor is not supported");
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_blend_op(BlendOp op) noexcept
|
||||
{
|
||||
switch (op) {
|
||||
case BlendOp::add:
|
||||
case BlendOp::subtract:
|
||||
case BlendOp::reverse_subtract:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("blend operation is not supported");
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_blend_state(BlendState state) noexcept
|
||||
{
|
||||
const auto source_color = validate_blend_factor(state.source_color);
|
||||
if (!source_color.ok()) {
|
||||
return source_color;
|
||||
}
|
||||
|
||||
const auto destination_color = validate_blend_factor(state.destination_color);
|
||||
if (!destination_color.ok()) {
|
||||
return destination_color;
|
||||
}
|
||||
|
||||
const auto color_op = validate_blend_op(state.color_op);
|
||||
if (!color_op.ok()) {
|
||||
return color_op;
|
||||
}
|
||||
|
||||
const auto source_alpha = validate_blend_factor(state.source_alpha);
|
||||
if (!source_alpha.ok()) {
|
||||
return source_alpha;
|
||||
}
|
||||
|
||||
const auto destination_alpha = validate_blend_factor(state.destination_alpha);
|
||||
if (!destination_alpha.ok()) {
|
||||
return destination_alpha;
|
||||
}
|
||||
|
||||
return validate_blend_op(state.alpha_op);
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_compare_op(CompareOp op) noexcept
|
||||
{
|
||||
switch (op) {
|
||||
case CompareOp::never:
|
||||
case CompareOp::less:
|
||||
case CompareOp::equal:
|
||||
case CompareOp::less_or_equal:
|
||||
case CompareOp::greater:
|
||||
case CompareOp::not_equal:
|
||||
case CompareOp::greater_or_equal:
|
||||
case CompareOp::always:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("depth compare operation is not supported");
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_depth_state(DepthState state) noexcept
|
||||
{
|
||||
return validate_compare_op(state.compare);
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_sampler_filter(SamplerFilter filter) noexcept
|
||||
{
|
||||
switch (filter) {
|
||||
case SamplerFilter::nearest:
|
||||
case SamplerFilter::linear:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("sampler filter is not supported");
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_sampler_address_mode(SamplerAddressMode mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
case SamplerAddressMode::clamp_to_edge:
|
||||
case SamplerAddressMode::repeat:
|
||||
case SamplerAddressMode::mirrored_repeat:
|
||||
case SamplerAddressMode::clamp_to_border:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("sampler address mode is not supported");
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_sampler_desc(SamplerDesc desc) noexcept
|
||||
{
|
||||
const auto min_filter = validate_sampler_filter(desc.min_filter);
|
||||
if (!min_filter.ok()) {
|
||||
return min_filter;
|
||||
}
|
||||
|
||||
const auto mag_filter = validate_sampler_filter(desc.mag_filter);
|
||||
if (!mag_filter.ok()) {
|
||||
return mag_filter;
|
||||
}
|
||||
|
||||
const auto mip_filter = validate_sampler_filter(desc.mip_filter);
|
||||
if (!mip_filter.ok()) {
|
||||
return mip_filter;
|
||||
}
|
||||
|
||||
const auto address_u = validate_sampler_address_mode(desc.address_u);
|
||||
if (!address_u.ok()) {
|
||||
return address_u;
|
||||
}
|
||||
|
||||
const auto address_v = validate_sampler_address_mode(desc.address_v);
|
||||
if (!address_v.ok()) {
|
||||
return address_v;
|
||||
}
|
||||
|
||||
return validate_sampler_address_mode(desc.address_w);
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept
|
||||
{
|
||||
const auto label_status = validate_resource_label(desc.debug_name);
|
||||
if (!label_status.ok()) {
|
||||
return label_status;
|
||||
}
|
||||
|
||||
if (desc.vertex_count == 0) {
|
||||
return pp::foundation::Status::invalid_argument("mesh must contain at least one vertex");
|
||||
}
|
||||
@@ -151,10 +458,54 @@ pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept
|
||||
return pp::foundation::Status::invalid_argument("mesh topology is not supported");
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_draw_desc(MeshDesc mesh, DrawDesc draw) noexcept
|
||||
{
|
||||
const auto mesh_status = validate_mesh_desc(mesh);
|
||||
if (!mesh_status.ok()) {
|
||||
return mesh_status;
|
||||
}
|
||||
|
||||
if (draw.instance_count == 0) {
|
||||
return pp::foundation::Status::invalid_argument("draw instance count must be greater than zero");
|
||||
}
|
||||
|
||||
if (draw.vertex_count == 0 && draw.index_count == 0) {
|
||||
return pp::foundation::Status::invalid_argument("draw must include vertices or indices");
|
||||
}
|
||||
|
||||
if (draw.index_count > 0) {
|
||||
if (mesh.index_count == 0) {
|
||||
return pp::foundation::Status::invalid_argument("indexed draw requires an indexed mesh");
|
||||
}
|
||||
|
||||
if (draw.first_index > mesh.index_count || draw.index_count > mesh.index_count - draw.first_index) {
|
||||
return pp::foundation::Status::out_of_range("draw index range exceeds the bound mesh");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
if (draw.first_vertex > mesh.vertex_count || draw.vertex_count > mesh.vertex_count - draw.first_vertex) {
|
||||
return pp::foundation::Status::out_of_range("draw vertex range exceeds the bound mesh");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_texture_slot(std::uint32_t slot) noexcept
|
||||
{
|
||||
if (slot >= max_texture_slots) {
|
||||
return pp::foundation::Status::out_of_range("texture slot exceeds the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept
|
||||
{
|
||||
if (desc.debug_name == nullptr) {
|
||||
return pp::foundation::Status::invalid_argument("shader debug name must not be null");
|
||||
const auto label_status = validate_resource_label(desc.debug_name);
|
||||
if (!label_status.ok()) {
|
||||
return label_status;
|
||||
}
|
||||
|
||||
const auto vertex_status = validate_shader_stage_source(
|
||||
@@ -174,11 +525,51 @@ pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noex
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_shader_uniform_write(
|
||||
const char* name,
|
||||
std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
if (is_empty_c_string(name)) {
|
||||
return pp::foundation::Status::invalid_argument("shader uniform name must not be empty");
|
||||
}
|
||||
|
||||
if (bytes.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("shader uniform bytes must not be empty");
|
||||
}
|
||||
|
||||
if (bytes.size() > max_shader_uniform_bytes) {
|
||||
return pp::foundation::Status::out_of_range("shader uniform bytes exceed the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_trace_label(const char* component, const char* name) noexcept
|
||||
{
|
||||
if (is_empty_c_string(component)) {
|
||||
return pp::foundation::Status::invalid_argument("trace component must not be empty");
|
||||
}
|
||||
|
||||
if (is_empty_c_string(name)) {
|
||||
return pp::foundation::Status::invalid_argument("trace name must not be empty");
|
||||
}
|
||||
|
||||
if (bounded_c_string_length(component, max_trace_label_bytes) > max_trace_label_bytes) {
|
||||
return pp::foundation::Status::out_of_range("trace component exceeds the configured limit");
|
||||
}
|
||||
|
||||
if (bounded_c_string_length(name, max_trace_label_bytes) > max_trace_label_bytes) {
|
||||
return pp::foundation::Status::out_of_range("trace name exceeds the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept
|
||||
{
|
||||
const auto extent_status = validate_extent(desc.extent);
|
||||
if (!extent_status.ok()) {
|
||||
return extent_status;
|
||||
const auto desc_status = validate_texture_desc(desc);
|
||||
if (!desc_status.ok()) {
|
||||
return desc_status;
|
||||
}
|
||||
|
||||
if (region.width == 0 || region.height == 0) {
|
||||
@@ -196,6 +587,242 @@ pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::uint64_t> readback_byte_size(TextureDesc desc, ReadbackRegion region) noexcept
|
||||
{
|
||||
const auto region_status = validate_readback_region(desc, region);
|
||||
if (!region_status.ok()) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(region_status);
|
||||
}
|
||||
|
||||
const auto bpp = static_cast<std::uint64_t>(bytes_per_pixel(desc.format));
|
||||
if (bpp == 0) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("texture format is not supported"));
|
||||
}
|
||||
|
||||
const auto width = static_cast<std::uint64_t>(region.width);
|
||||
const auto height = static_cast<std::uint64_t>(region.height);
|
||||
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::out_of_range("readback pixel count overflows uint64"));
|
||||
}
|
||||
|
||||
const auto pixels = width * height;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / bpp) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::out_of_range("readback byte size overflows uint64"));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * bpp;
|
||||
if (bytes > max_texture_bytes) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::out_of_range("readback byte size exceeds the configured limit"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::uint64_t>::success(bytes);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::uint64_t> frame_capture_byte_size(TextureDesc desc) noexcept
|
||||
{
|
||||
const auto desc_status = validate_texture_desc(desc);
|
||||
if (!desc_status.ok()) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(desc_status);
|
||||
}
|
||||
|
||||
if (!has_texture_usage(desc.usage, TextureUsage::render_target)) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("frame capture source must be a render target"));
|
||||
}
|
||||
|
||||
if (!has_texture_usage(desc.usage, TextureUsage::readback_source)) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("frame capture source must allow readback"));
|
||||
}
|
||||
|
||||
return texture_byte_size(desc);
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_texture_copy_descs(
|
||||
TextureDesc source,
|
||||
ReadbackRegion source_region,
|
||||
TextureDesc destination,
|
||||
ReadbackRegion destination_region) noexcept
|
||||
{
|
||||
if (!has_texture_usage(source.usage, TextureUsage::copy_source)) {
|
||||
return pp::foundation::Status::invalid_argument("texture copy source must allow copy_source usage");
|
||||
}
|
||||
|
||||
if (!has_texture_usage(destination.usage, TextureUsage::copy_destination)) {
|
||||
return pp::foundation::Status::invalid_argument("texture copy destination must allow copy_destination usage");
|
||||
}
|
||||
|
||||
if (source.format != destination.format) {
|
||||
return pp::foundation::Status::invalid_argument("texture copy endpoints must use matching formats");
|
||||
}
|
||||
|
||||
if (source_region.width != destination_region.width || source_region.height != destination_region.height) {
|
||||
return pp::foundation::Status::invalid_argument("texture copy regions must have matching dimensions");
|
||||
}
|
||||
|
||||
const auto source_status = validate_readback_region(source, source_region);
|
||||
if (!source_status.ok()) {
|
||||
return source_status;
|
||||
}
|
||||
|
||||
return validate_readback_region(destination, destination_region);
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_mipmap_generation_desc(TextureDesc desc) noexcept
|
||||
{
|
||||
const auto desc_status = validate_texture_desc(desc);
|
||||
if (!desc_status.ok()) {
|
||||
return desc_status;
|
||||
}
|
||||
|
||||
if (desc.mip_levels <= 1U) {
|
||||
return pp::foundation::Status::invalid_argument("mipmap generation requires more than one mip level");
|
||||
}
|
||||
|
||||
if (!has_texture_usage(desc.usage, TextureUsage::sampled)) {
|
||||
return pp::foundation::Status::invalid_argument("mipmap texture must allow sampled usage");
|
||||
}
|
||||
|
||||
if (!has_texture_usage(desc.usage, TextureUsage::copy_source)) {
|
||||
return pp::foundation::Status::invalid_argument("mipmap texture must allow copy_source usage");
|
||||
}
|
||||
|
||||
if (!has_texture_usage(desc.usage, TextureUsage::copy_destination)) {
|
||||
return pp::foundation::Status::invalid_argument("mipmap texture must allow copy_destination usage");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_texture_state(TextureState state) noexcept
|
||||
{
|
||||
switch (state) {
|
||||
case TextureState::undefined:
|
||||
case TextureState::shader_read:
|
||||
case TextureState::render_target:
|
||||
case TextureState::upload_destination:
|
||||
case TextureState::copy_source:
|
||||
case TextureState::copy_destination:
|
||||
case TextureState::readback_source:
|
||||
case TextureState::present:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("texture state is not supported");
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_texture_transition_desc(
|
||||
TextureDesc desc,
|
||||
TextureState before,
|
||||
TextureState after) noexcept
|
||||
{
|
||||
const auto desc_status = validate_texture_desc(desc);
|
||||
if (!desc_status.ok()) {
|
||||
return desc_status;
|
||||
}
|
||||
|
||||
const auto before_status = validate_texture_state(before);
|
||||
if (!before_status.ok()) {
|
||||
return before_status;
|
||||
}
|
||||
|
||||
const auto after_status = validate_texture_state(after);
|
||||
if (!after_status.ok()) {
|
||||
return after_status;
|
||||
}
|
||||
|
||||
if (before == after) {
|
||||
return pp::foundation::Status::invalid_argument("texture transition must change state");
|
||||
}
|
||||
|
||||
if (after == TextureState::undefined) {
|
||||
return pp::foundation::Status::invalid_argument("texture transition destination must not be undefined");
|
||||
}
|
||||
|
||||
if (after == TextureState::shader_read && !has_texture_usage(desc.usage, TextureUsage::sampled)) {
|
||||
return pp::foundation::Status::invalid_argument("shader-read transition requires sampled usage");
|
||||
}
|
||||
|
||||
if (after == TextureState::render_target && !has_texture_usage(desc.usage, TextureUsage::render_target)) {
|
||||
return pp::foundation::Status::invalid_argument("render-target transition requires render_target usage");
|
||||
}
|
||||
|
||||
if (after == TextureState::upload_destination && !has_texture_usage(desc.usage, TextureUsage::upload_destination)) {
|
||||
return pp::foundation::Status::invalid_argument("upload transition requires upload_destination usage");
|
||||
}
|
||||
|
||||
if (after == TextureState::copy_source && !has_texture_usage(desc.usage, TextureUsage::copy_source)) {
|
||||
return pp::foundation::Status::invalid_argument("copy-source transition requires copy_source usage");
|
||||
}
|
||||
|
||||
if (after == TextureState::copy_destination && !has_texture_usage(desc.usage, TextureUsage::copy_destination)) {
|
||||
return pp::foundation::Status::invalid_argument("copy-destination transition requires copy_destination usage");
|
||||
}
|
||||
|
||||
if (after == TextureState::readback_source && !has_texture_usage(desc.usage, TextureUsage::readback_source)) {
|
||||
return pp::foundation::Status::invalid_argument("readback transition requires readback_source usage");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept
|
||||
{
|
||||
switch (filter) {
|
||||
case BlitFilter::nearest:
|
||||
case BlitFilter::linear:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("blit filter is not supported");
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc destination) noexcept
|
||||
{
|
||||
const auto source_status = validate_texture_desc(source);
|
||||
if (!source_status.ok()) {
|
||||
return source_status;
|
||||
}
|
||||
|
||||
const auto destination_status = validate_texture_desc(destination);
|
||||
if (!destination_status.ok()) {
|
||||
return destination_status;
|
||||
}
|
||||
|
||||
if (!has_texture_usage(source.usage, TextureUsage::render_target)
|
||||
|| !has_texture_usage(destination.usage, TextureUsage::render_target)) {
|
||||
return pp::foundation::Status::invalid_argument("blit endpoints must be render targets");
|
||||
}
|
||||
|
||||
if (!has_texture_usage(source.usage, TextureUsage::copy_source)) {
|
||||
return pp::foundation::Status::invalid_argument("blit source must allow copy_source usage");
|
||||
}
|
||||
|
||||
if (!has_texture_usage(destination.usage, TextureUsage::copy_destination)) {
|
||||
return pp::foundation::Status::invalid_argument("blit destination must allow copy_destination usage");
|
||||
}
|
||||
|
||||
if (source.format != destination.format) {
|
||||
return pp::foundation::Status::invalid_argument("blit endpoints must use matching texture formats");
|
||||
}
|
||||
|
||||
const auto source_bytes = texture_byte_size(source);
|
||||
if (!source_bytes.ok()) {
|
||||
return source_bytes.status();
|
||||
}
|
||||
|
||||
const auto destination_bytes = texture_byte_size(destination);
|
||||
if (!destination_bytes.ok()) {
|
||||
return destination_bytes.status();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
const char* texture_format_name(TextureFormat format) noexcept
|
||||
{
|
||||
switch (format) {
|
||||
@@ -210,6 +837,30 @@ const char* texture_format_name(TextureFormat format) noexcept
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* texture_state_name(TextureState state) noexcept
|
||||
{
|
||||
switch (state) {
|
||||
case TextureState::undefined:
|
||||
return "undefined";
|
||||
case TextureState::shader_read:
|
||||
return "shader_read";
|
||||
case TextureState::render_target:
|
||||
return "render_target";
|
||||
case TextureState::upload_destination:
|
||||
return "upload_destination";
|
||||
case TextureState::copy_source:
|
||||
return "copy_source";
|
||||
case TextureState::copy_destination:
|
||||
return "copy_destination";
|
||||
case TextureState::readback_source:
|
||||
return "readback_source";
|
||||
case TextureState::present:
|
||||
return "present";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* primitive_topology_name(PrimitiveTopology topology) noexcept
|
||||
{
|
||||
switch (topology) {
|
||||
@@ -224,4 +875,102 @@ const char* primitive_topology_name(PrimitiveTopology topology) noexcept
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* blit_filter_name(BlitFilter filter) noexcept
|
||||
{
|
||||
switch (filter) {
|
||||
case BlitFilter::nearest:
|
||||
return "nearest";
|
||||
case BlitFilter::linear:
|
||||
return "linear";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* blend_factor_name(BlendFactor factor) noexcept
|
||||
{
|
||||
switch (factor) {
|
||||
case BlendFactor::zero:
|
||||
return "zero";
|
||||
case BlendFactor::one:
|
||||
return "one";
|
||||
case BlendFactor::source_alpha:
|
||||
return "source_alpha";
|
||||
case BlendFactor::one_minus_source_alpha:
|
||||
return "one_minus_source_alpha";
|
||||
case BlendFactor::destination_alpha:
|
||||
return "destination_alpha";
|
||||
case BlendFactor::one_minus_destination_alpha:
|
||||
return "one_minus_destination_alpha";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* blend_op_name(BlendOp op) noexcept
|
||||
{
|
||||
switch (op) {
|
||||
case BlendOp::add:
|
||||
return "add";
|
||||
case BlendOp::subtract:
|
||||
return "subtract";
|
||||
case BlendOp::reverse_subtract:
|
||||
return "reverse_subtract";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* compare_op_name(CompareOp op) noexcept
|
||||
{
|
||||
switch (op) {
|
||||
case CompareOp::never:
|
||||
return "never";
|
||||
case CompareOp::less:
|
||||
return "less";
|
||||
case CompareOp::equal:
|
||||
return "equal";
|
||||
case CompareOp::less_or_equal:
|
||||
return "less_or_equal";
|
||||
case CompareOp::greater:
|
||||
return "greater";
|
||||
case CompareOp::not_equal:
|
||||
return "not_equal";
|
||||
case CompareOp::greater_or_equal:
|
||||
return "greater_or_equal";
|
||||
case CompareOp::always:
|
||||
return "always";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* sampler_filter_name(SamplerFilter filter) noexcept
|
||||
{
|
||||
switch (filter) {
|
||||
case SamplerFilter::nearest:
|
||||
return "nearest";
|
||||
case SamplerFilter::linear:
|
||||
return "linear";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* sampler_address_mode_name(SamplerAddressMode mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
case SamplerAddressMode::clamp_to_edge:
|
||||
return "clamp_to_edge";
|
||||
case SamplerAddressMode::repeat:
|
||||
return "repeat";
|
||||
case SamplerAddressMode::mirrored_repeat:
|
||||
return "mirrored_repeat";
|
||||
case SamplerAddressMode::clamp_to_border:
|
||||
return "clamp_to_border";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,13 +4,20 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
namespace pp::renderer {
|
||||
|
||||
constexpr std::uint32_t max_texture_dimension = 32768;
|
||||
constexpr std::uint32_t max_texture_mip_levels = 16;
|
||||
constexpr std::uint32_t max_mesh_vertices = 16777216;
|
||||
constexpr std::uint32_t max_texture_slots = 32;
|
||||
constexpr std::uint64_t max_texture_bytes = 1024ULL * 1024ULL * 1024ULL;
|
||||
constexpr std::size_t max_shader_source_bytes = 4ULL * 1024ULL * 1024ULL;
|
||||
constexpr std::size_t max_shader_uniform_bytes = 64ULL * 1024ULL;
|
||||
constexpr std::size_t max_trace_label_bytes = 256;
|
||||
constexpr std::size_t max_resource_label_bytes = 256;
|
||||
|
||||
enum class TextureFormat : std::uint8_t {
|
||||
rgba8,
|
||||
@@ -18,6 +25,45 @@ enum class TextureFormat : std::uint8_t {
|
||||
depth24_stencil8,
|
||||
};
|
||||
|
||||
enum class TextureUsage : std::uint32_t {
|
||||
none = 0,
|
||||
sampled = 1U << 0U,
|
||||
render_target = 1U << 1U,
|
||||
upload_destination = 1U << 2U,
|
||||
readback_source = 1U << 3U,
|
||||
copy_source = 1U << 4U,
|
||||
copy_destination = 1U << 5U,
|
||||
};
|
||||
|
||||
enum class TextureState : std::uint8_t {
|
||||
undefined,
|
||||
shader_read,
|
||||
render_target,
|
||||
upload_destination,
|
||||
copy_source,
|
||||
copy_destination,
|
||||
readback_source,
|
||||
present,
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr TextureUsage operator|(TextureUsage lhs, TextureUsage rhs) noexcept
|
||||
{
|
||||
return static_cast<TextureUsage>(
|
||||
static_cast<std::uint32_t>(lhs) | static_cast<std::uint32_t>(rhs));
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr TextureUsage operator&(TextureUsage lhs, TextureUsage rhs) noexcept
|
||||
{
|
||||
return static_cast<TextureUsage>(
|
||||
static_cast<std::uint32_t>(lhs) & static_cast<std::uint32_t>(rhs));
|
||||
}
|
||||
|
||||
constexpr TextureUsage& operator|=(TextureUsage& lhs, TextureUsage rhs) noexcept
|
||||
{
|
||||
lhs = lhs | rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
struct Extent2D {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
@@ -26,7 +72,13 @@ struct Extent2D {
|
||||
struct TextureDesc {
|
||||
Extent2D extent;
|
||||
TextureFormat format = TextureFormat::rgba8;
|
||||
bool render_target = false;
|
||||
std::uint32_t mip_levels = 1;
|
||||
TextureUsage usage = TextureUsage::sampled
|
||||
| TextureUsage::upload_destination
|
||||
| TextureUsage::readback_source
|
||||
| TextureUsage::copy_source
|
||||
| TextureUsage::copy_destination;
|
||||
const char* debug_name = "";
|
||||
};
|
||||
|
||||
struct ReadbackRegion {
|
||||
@@ -45,6 +97,14 @@ struct Viewport {
|
||||
float max_depth = 1.0F;
|
||||
};
|
||||
|
||||
struct ScissorRect {
|
||||
bool enabled = false;
|
||||
std::int32_t x = 0;
|
||||
std::int32_t y = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
};
|
||||
|
||||
struct ClearColor {
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
@@ -52,16 +112,106 @@ struct ClearColor {
|
||||
float a = 0.0F;
|
||||
};
|
||||
|
||||
struct RenderPassDesc {
|
||||
bool clear_color_enabled = true;
|
||||
ClearColor clear_color;
|
||||
bool clear_depth_enabled = false;
|
||||
float clear_depth = 1.0F;
|
||||
bool clear_stencil_enabled = false;
|
||||
std::uint8_t clear_stencil = 0;
|
||||
};
|
||||
|
||||
enum class PrimitiveTopology : std::uint8_t {
|
||||
triangles,
|
||||
triangle_strip,
|
||||
lines,
|
||||
};
|
||||
|
||||
enum class BlitFilter : std::uint8_t {
|
||||
nearest,
|
||||
linear,
|
||||
};
|
||||
|
||||
enum class BlendFactor : std::uint8_t {
|
||||
zero,
|
||||
one,
|
||||
source_alpha,
|
||||
one_minus_source_alpha,
|
||||
destination_alpha,
|
||||
one_minus_destination_alpha,
|
||||
};
|
||||
|
||||
enum class BlendOp : std::uint8_t {
|
||||
add,
|
||||
subtract,
|
||||
reverse_subtract,
|
||||
};
|
||||
|
||||
enum class CompareOp : std::uint8_t {
|
||||
never,
|
||||
less,
|
||||
equal,
|
||||
less_or_equal,
|
||||
greater,
|
||||
not_equal,
|
||||
greater_or_equal,
|
||||
always,
|
||||
};
|
||||
|
||||
enum class SamplerFilter : std::uint8_t {
|
||||
nearest,
|
||||
linear,
|
||||
};
|
||||
|
||||
enum class SamplerAddressMode : std::uint8_t {
|
||||
clamp_to_edge,
|
||||
repeat,
|
||||
mirrored_repeat,
|
||||
clamp_to_border,
|
||||
};
|
||||
|
||||
struct BlendState {
|
||||
bool enabled = false;
|
||||
BlendFactor source_color = BlendFactor::one;
|
||||
BlendFactor destination_color = BlendFactor::zero;
|
||||
BlendOp color_op = BlendOp::add;
|
||||
BlendFactor source_alpha = BlendFactor::one;
|
||||
BlendFactor destination_alpha = BlendFactor::zero;
|
||||
BlendOp alpha_op = BlendOp::add;
|
||||
bool write_r = true;
|
||||
bool write_g = true;
|
||||
bool write_b = true;
|
||||
bool write_a = true;
|
||||
};
|
||||
|
||||
struct DepthState {
|
||||
bool test_enabled = false;
|
||||
bool write_enabled = false;
|
||||
CompareOp compare = CompareOp::less_or_equal;
|
||||
};
|
||||
|
||||
struct SamplerDesc {
|
||||
SamplerFilter min_filter = SamplerFilter::linear;
|
||||
SamplerFilter mag_filter = SamplerFilter::linear;
|
||||
SamplerFilter mip_filter = SamplerFilter::linear;
|
||||
SamplerAddressMode address_u = SamplerAddressMode::clamp_to_edge;
|
||||
SamplerAddressMode address_v = SamplerAddressMode::clamp_to_edge;
|
||||
SamplerAddressMode address_w = SamplerAddressMode::clamp_to_edge;
|
||||
};
|
||||
|
||||
struct MeshDesc {
|
||||
std::uint32_t vertex_count = 0;
|
||||
std::uint32_t index_count = 0;
|
||||
PrimitiveTopology topology = PrimitiveTopology::triangles;
|
||||
const char* debug_name = "";
|
||||
};
|
||||
|
||||
struct DrawDesc {
|
||||
std::uint32_t first_vertex = 0;
|
||||
std::uint32_t vertex_count = 0;
|
||||
std::uint32_t first_index = 0;
|
||||
std::uint32_t index_count = 0;
|
||||
std::uint32_t instance_count = 1;
|
||||
};
|
||||
|
||||
struct ShaderStageSource {
|
||||
@@ -76,6 +226,16 @@ struct ShaderProgramDesc {
|
||||
ShaderStageSource fragment;
|
||||
};
|
||||
|
||||
struct RenderDeviceFeatures {
|
||||
bool framebuffer_fetch = false;
|
||||
bool explicit_texture_transitions = false;
|
||||
bool texture_copy = false;
|
||||
bool render_target_blit = false;
|
||||
bool frame_capture = false;
|
||||
bool float16_render_targets = false;
|
||||
bool float32_render_targets = false;
|
||||
};
|
||||
|
||||
class ITexture2D {
|
||||
public:
|
||||
virtual ~ITexture2D() = default;
|
||||
@@ -109,7 +269,9 @@ public:
|
||||
class IRenderTrace {
|
||||
public:
|
||||
virtual ~IRenderTrace() = default;
|
||||
virtual void marker(const char* component, const char* name) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status marker(const char* component, const char* name) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status begin_scope(const char* component, const char* name) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status end_scope() noexcept = 0;
|
||||
};
|
||||
|
||||
class ICommandContext {
|
||||
@@ -117,11 +279,51 @@ public:
|
||||
virtual ~ICommandContext() = default;
|
||||
[[nodiscard]] virtual pp::foundation::Status begin_render_pass(
|
||||
IRenderTarget& target,
|
||||
ClearColor clear_color) noexcept = 0;
|
||||
RenderPassDesc desc) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status set_viewport(Viewport viewport) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status set_scissor(ScissorRect scissor) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status set_blend_state(BlendState state) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status set_depth_state(DepthState state) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status set_shader_uniform(
|
||||
const char* name,
|
||||
std::span<const std::byte> bytes) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status bind_texture(
|
||||
std::uint32_t slot,
|
||||
ITexture2D& texture) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status bind_sampler(
|
||||
std::uint32_t slot,
|
||||
SamplerDesc sampler) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status bind_mesh(IMesh& mesh) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status draw() noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status draw(DrawDesc desc) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status read_texture(
|
||||
ITexture2D& texture,
|
||||
ReadbackRegion region,
|
||||
IReadbackBuffer& destination) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status upload_texture(
|
||||
ITexture2D& texture,
|
||||
ReadbackRegion region,
|
||||
std::span<const std::byte> rgba_or_channel_bytes) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status generate_mipmaps(
|
||||
ITexture2D& texture) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status transition_texture(
|
||||
ITexture2D& texture,
|
||||
TextureState before,
|
||||
TextureState after) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status copy_texture(
|
||||
ITexture2D& source,
|
||||
ReadbackRegion source_region,
|
||||
ITexture2D& destination,
|
||||
ReadbackRegion destination_region) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status capture_frame(
|
||||
IRenderTarget& target,
|
||||
IReadbackBuffer& destination) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status blit_render_target(
|
||||
IRenderTarget& source,
|
||||
ReadbackRegion source_region,
|
||||
IRenderTarget& destination,
|
||||
ReadbackRegion destination_region,
|
||||
BlitFilter filter) noexcept = 0;
|
||||
virtual void end_render_pass() noexcept = 0;
|
||||
};
|
||||
|
||||
@@ -129,18 +331,76 @@ class IRenderDevice {
|
||||
public:
|
||||
virtual ~IRenderDevice() = default;
|
||||
[[nodiscard]] virtual const char* backend_name() const noexcept = 0;
|
||||
[[nodiscard]] virtual RenderDeviceFeatures features() const noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<ITexture2D>> create_texture(
|
||||
TextureDesc desc) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<IRenderTarget>> create_render_target(
|
||||
TextureDesc color_desc) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<IShaderProgram>> create_shader_program(
|
||||
ShaderProgramDesc desc) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<IMesh>> create_mesh(
|
||||
MeshDesc desc) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<IReadbackBuffer>> create_readback_buffer(
|
||||
std::uint64_t size_bytes) noexcept = 0;
|
||||
[[nodiscard]] virtual ICommandContext& immediate_context() noexcept = 0;
|
||||
[[nodiscard]] virtual IRenderTrace* trace() noexcept = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::uint32_t bytes_per_pixel(TextureFormat format) noexcept;
|
||||
[[nodiscard]] std::uint32_t max_mip_levels_for_extent(Extent2D extent) noexcept;
|
||||
[[nodiscard]] bool has_texture_usage(TextureUsage usage, TextureUsage required) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_extent(Extent2D extent) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_texture_usage(TextureUsage usage) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_resource_label(const char* label) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_texture_desc(TextureDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_scissor(ScissorRect scissor, Extent2D target_extent) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_render_pass_desc(RenderPassDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_blend_factor(BlendFactor factor) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_blend_op(BlendOp op) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_blend_state(BlendState state) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_compare_op(CompareOp op) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_depth_state(DepthState state) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_sampler_filter(SamplerFilter filter) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_sampler_address_mode(SamplerAddressMode mode) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_sampler_desc(SamplerDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_draw_desc(MeshDesc mesh, DrawDesc draw) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_texture_slot(std::uint32_t slot) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_shader_uniform_write(
|
||||
const char* name,
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_trace_label(const char* component, const char* name) noexcept;
|
||||
[[nodiscard]] pp::foundation::Result<std::uint64_t> texture_byte_size(TextureDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Result<std::uint64_t> readback_byte_size(
|
||||
TextureDesc desc,
|
||||
ReadbackRegion region) noexcept;
|
||||
[[nodiscard]] pp::foundation::Result<std::uint64_t> frame_capture_byte_size(TextureDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_texture_copy_descs(
|
||||
TextureDesc source,
|
||||
ReadbackRegion source_region,
|
||||
TextureDesc destination,
|
||||
ReadbackRegion destination_region) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_mipmap_generation_desc(TextureDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_texture_state(TextureState state) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_texture_transition_desc(
|
||||
TextureDesc desc,
|
||||
TextureState before,
|
||||
TextureState after) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_blit_descs(
|
||||
TextureDesc source,
|
||||
TextureDesc destination) noexcept;
|
||||
[[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept;
|
||||
[[nodiscard]] const char* texture_state_name(TextureState state) noexcept;
|
||||
[[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept;
|
||||
[[nodiscard]] const char* blit_filter_name(BlitFilter filter) noexcept;
|
||||
[[nodiscard]] const char* blend_factor_name(BlendFactor factor) noexcept;
|
||||
[[nodiscard]] const char* blend_op_name(BlendOp op) noexcept;
|
||||
[[nodiscard]] const char* compare_op_name(CompareOp op) noexcept;
|
||||
[[nodiscard]] const char* sampler_filter_name(SamplerFilter filter) noexcept;
|
||||
[[nodiscard]] const char* sampler_address_mode_name(SamplerAddressMode mode) noexcept;
|
||||
|
||||
}
|
||||
|
||||
91
src/renderer_api/shader_catalog.cpp
Normal file
91
src/renderer_api/shader_catalog.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "renderer_api/shader_catalog.h"
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::renderer {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<ShaderCatalogEntry, 25> pano_catalog {
|
||||
ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.glsl" },
|
||||
ShaderCatalogEntry { .name = "texture-alpha", .path = "data/shaders/texture-alpha.glsl" },
|
||||
ShaderCatalogEntry { .name = "texture-mask", .path = "data/shaders/texture-mask.glsl" },
|
||||
ShaderCatalogEntry { .name = "texture-colorize", .path = "data/shaders/texture-colorize.glsl" },
|
||||
ShaderCatalogEntry { .name = "texture-blend", .path = "data/shaders/texture-blend.glsl" },
|
||||
ShaderCatalogEntry { .name = "stroke-preview", .path = "data/shaders/stroke-preview.glsl" },
|
||||
ShaderCatalogEntry { .name = "comp-erase", .path = "data/shaders/comp-erase.glsl" },
|
||||
ShaderCatalogEntry { .name = "comp-draw", .path = "data/shaders/comp-draw.glsl" },
|
||||
ShaderCatalogEntry { .name = "color", .path = "data/shaders/color.glsl" },
|
||||
ShaderCatalogEntry { .name = "color-quad", .path = "data/shaders/color-quad.glsl" },
|
||||
ShaderCatalogEntry { .name = "color-tri", .path = "data/shaders/color-tri.glsl" },
|
||||
ShaderCatalogEntry { .name = "color-hue", .path = "data/shaders/color-hue.glsl" },
|
||||
ShaderCatalogEntry { .name = "uvs", .path = "data/shaders/uvs.glsl" },
|
||||
ShaderCatalogEntry { .name = "font", .path = "data/shaders/font.glsl" },
|
||||
ShaderCatalogEntry { .name = "atlas", .path = "data/shaders/atlas.glsl" },
|
||||
ShaderCatalogEntry { .name = "stroke", .path = "data/shaders/stroke.glsl" },
|
||||
ShaderCatalogEntry { .name = "stroke-pad", .path = "data/shaders/stroke-pad.glsl" },
|
||||
ShaderCatalogEntry { .name = "stroke-dilate", .path = "data/shaders/stroke-dilate.glsl" },
|
||||
ShaderCatalogEntry { .name = "checkerboard", .path = "data/shaders/checkerboard.glsl" },
|
||||
ShaderCatalogEntry { .name = "equirect", .path = "data/shaders/equirect.glsl" },
|
||||
ShaderCatalogEntry { .name = "brush-stroke", .path = "data/shaders/stroke-instanced.glsl" },
|
||||
ShaderCatalogEntry { .name = "vertex-color", .path = "data/shaders/vertex-color.glsl" },
|
||||
ShaderCatalogEntry { .name = "lambert", .path = "data/shaders/lambert.glsl" },
|
||||
ShaderCatalogEntry { .name = "lambert-lightmap", .path = "data/shaders/lightmap.glsl" },
|
||||
ShaderCatalogEntry { .name = "bakeuv", .path = "data/shaders/bake-uv.glsl" },
|
||||
};
|
||||
|
||||
[[nodiscard]] bool is_empty_c_string(const char* text) noexcept
|
||||
{
|
||||
return text == nullptr || text[0] == '\0';
|
||||
}
|
||||
|
||||
[[nodiscard]] bool has_shader_extension(std::string_view path) noexcept
|
||||
{
|
||||
constexpr std::string_view extension = ".glsl";
|
||||
return path.size() >= extension.size()
|
||||
&& path.substr(path.size() - extension.size()) == extension;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::span<const ShaderCatalogEntry> panopainter_shader_catalog() noexcept
|
||||
{
|
||||
return pano_catalog;
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_shader_catalog(std::span<const ShaderCatalogEntry> catalog) noexcept
|
||||
{
|
||||
if (catalog.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("shader catalog must not be empty");
|
||||
}
|
||||
|
||||
if (catalog.size() > max_shader_catalog_entries) {
|
||||
return pp::foundation::Status::out_of_range("shader catalog exceeds the configured limit");
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < catalog.size(); ++i) {
|
||||
const auto& entry = catalog[i];
|
||||
if (is_empty_c_string(entry.name)) {
|
||||
return pp::foundation::Status::invalid_argument("shader catalog entry name must not be empty");
|
||||
}
|
||||
|
||||
if (is_empty_c_string(entry.path)) {
|
||||
return pp::foundation::Status::invalid_argument("shader catalog entry path must not be empty");
|
||||
}
|
||||
|
||||
if (!has_shader_extension(entry.path)) {
|
||||
return pp::foundation::Status::invalid_argument("shader catalog path must end with .glsl");
|
||||
}
|
||||
|
||||
for (std::size_t j = i + 1U; j < catalog.size(); ++j) {
|
||||
if (std::string_view(entry.name) == std::string_view(catalog[j].name)) {
|
||||
return pp::foundation::Status::invalid_argument("shader catalog entry name is duplicated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
21
src/renderer_api/shader_catalog.h
Normal file
21
src/renderer_api/shader_catalog.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
|
||||
namespace pp::renderer {
|
||||
|
||||
constexpr std::size_t max_shader_catalog_entries = 256;
|
||||
|
||||
struct ShaderCatalogEntry {
|
||||
const char* name = "";
|
||||
const char* path = "";
|
||||
};
|
||||
|
||||
[[nodiscard]] std::span<const ShaderCatalogEntry> panopainter_shader_catalog() noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_shader_catalog(
|
||||
std::span<const ShaderCatalogEntry> catalog) noexcept;
|
||||
|
||||
}
|
||||
442
src/renderer_gl/command_plan.cpp
Normal file
442
src/renderer_gl/command_plan.cpp
Normal file
@@ -0,0 +1,442 @@
|
||||
#include "renderer_gl/command_plan.h"
|
||||
|
||||
namespace pp::renderer::gl {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool texture_format_supported(OpenGlRendererTextureFormat format) noexcept
|
||||
{
|
||||
return format.internal_format != 0U
|
||||
&& format.pixel_format != 0U
|
||||
&& format.component_type != 0U
|
||||
&& format.bytes_per_pixel != 0U;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool texture_state_supported(pp::renderer::TextureState state) noexcept
|
||||
{
|
||||
switch (state) {
|
||||
case pp::renderer::TextureState::undefined:
|
||||
case pp::renderer::TextureState::shader_read:
|
||||
case pp::renderer::TextureState::render_target:
|
||||
case pp::renderer::TextureState::upload_destination:
|
||||
case pp::renderer::TextureState::copy_source:
|
||||
case pp::renderer::TextureState::copy_destination:
|
||||
case pp::renderer::TextureState::readback_source:
|
||||
case pp::renderer::TextureState::present:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool non_empty_name(const char* name) noexcept
|
||||
{
|
||||
return name != nullptr && name[0] != '\0';
|
||||
}
|
||||
|
||||
[[nodiscard]] bool requires_render_pass(pp::renderer::RecordedRenderCommandKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case pp::renderer::RecordedRenderCommandKind::set_viewport:
|
||||
case pp::renderer::RecordedRenderCommandKind::set_scissor:
|
||||
case pp::renderer::RecordedRenderCommandKind::set_blend_state:
|
||||
case pp::renderer::RecordedRenderCommandKind::set_depth_state:
|
||||
case pp::renderer::RecordedRenderCommandKind::bind_shader:
|
||||
case pp::renderer::RecordedRenderCommandKind::set_shader_uniform:
|
||||
case pp::renderer::RecordedRenderCommandKind::bind_texture:
|
||||
case pp::renderer::RecordedRenderCommandKind::bind_sampler:
|
||||
case pp::renderer::RecordedRenderCommandKind::bind_mesh:
|
||||
case pp::renderer::RecordedRenderCommandKind::draw:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void record_unsupported_command(OpenGlCommandPlan& plan, std::size_t index) noexcept
|
||||
{
|
||||
++plan.unsupported_command_count;
|
||||
if (plan.first_unsupported_command == OpenGlCommandPlan::npos) {
|
||||
plan.first_unsupported_command = index;
|
||||
}
|
||||
}
|
||||
|
||||
void record_render_pass_order_error(OpenGlCommandPlan& plan, std::size_t index) noexcept
|
||||
{
|
||||
++plan.render_pass_order_error_count;
|
||||
if (plan.first_render_pass_order_error == OpenGlCommandPlan::npos) {
|
||||
plan.first_render_pass_order_error = index;
|
||||
}
|
||||
}
|
||||
|
||||
void record_dependency_error(OpenGlCommandPlan& plan, std::size_t index) noexcept
|
||||
{
|
||||
++plan.dependency_error_count;
|
||||
if (plan.first_dependency_error == OpenGlCommandPlan::npos) {
|
||||
plan.first_dependency_error = index;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const char* planned_command_kind_name(OpenGlPlannedCommandKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case OpenGlPlannedCommandKind::unknown:
|
||||
return "unknown";
|
||||
case OpenGlPlannedCommandKind::begin_render_pass:
|
||||
return "begin_render_pass";
|
||||
case OpenGlPlannedCommandKind::set_viewport:
|
||||
return "set_viewport";
|
||||
case OpenGlPlannedCommandKind::set_scissor:
|
||||
return "set_scissor";
|
||||
case OpenGlPlannedCommandKind::set_blend_state:
|
||||
return "set_blend_state";
|
||||
case OpenGlPlannedCommandKind::set_depth_state:
|
||||
return "set_depth_state";
|
||||
case OpenGlPlannedCommandKind::bind_shader:
|
||||
return "bind_shader";
|
||||
case OpenGlPlannedCommandKind::set_shader_uniform:
|
||||
return "set_shader_uniform";
|
||||
case OpenGlPlannedCommandKind::bind_texture:
|
||||
return "bind_texture";
|
||||
case OpenGlPlannedCommandKind::bind_sampler:
|
||||
return "bind_sampler";
|
||||
case OpenGlPlannedCommandKind::bind_mesh:
|
||||
return "bind_mesh";
|
||||
case OpenGlPlannedCommandKind::draw:
|
||||
return "draw";
|
||||
case OpenGlPlannedCommandKind::upload_texture:
|
||||
return "upload_texture";
|
||||
case OpenGlPlannedCommandKind::generate_mipmaps:
|
||||
return "generate_mipmaps";
|
||||
case OpenGlPlannedCommandKind::transition_texture:
|
||||
return "transition_texture";
|
||||
case OpenGlPlannedCommandKind::copy_texture:
|
||||
return "copy_texture";
|
||||
case OpenGlPlannedCommandKind::read_texture:
|
||||
return "read_texture";
|
||||
case OpenGlPlannedCommandKind::capture_frame:
|
||||
return "capture_frame";
|
||||
case OpenGlPlannedCommandKind::blit_render_target:
|
||||
return "blit_render_target";
|
||||
case OpenGlPlannedCommandKind::end_render_pass:
|
||||
return "end_render_pass";
|
||||
case OpenGlPlannedCommandKind::trace:
|
||||
return "trace";
|
||||
case OpenGlPlannedCommandKind::passthrough:
|
||||
return "passthrough";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
OpenGlPlannedCommand plan_recorded_render_command(pp::renderer::RecordedRenderCommand command) noexcept
|
||||
{
|
||||
OpenGlPlannedCommand planned {};
|
||||
planned.requires_render_pass = requires_render_pass(command.kind);
|
||||
planned.supported = true;
|
||||
|
||||
switch (command.kind) {
|
||||
case pp::renderer::RecordedRenderCommandKind::begin_render_pass:
|
||||
planned.kind = OpenGlPlannedCommandKind::begin_render_pass;
|
||||
planned.clear_mask = clear_mask_for_render_pass(pp::renderer::RenderPassDesc {
|
||||
.clear_color_enabled = command.clear_color_enabled,
|
||||
.clear_color = command.clear_color,
|
||||
.clear_depth_enabled = command.clear_depth_enabled,
|
||||
.clear_depth = command.clear_depth,
|
||||
.clear_stencil_enabled = command.clear_stencil_enabled,
|
||||
.clear_stencil = command.clear_stencil,
|
||||
});
|
||||
planned.clear_values = clear_values_for_render_pass(pp::renderer::RenderPassDesc {
|
||||
.clear_color_enabled = command.clear_color_enabled,
|
||||
.clear_color = command.clear_color,
|
||||
.clear_depth_enabled = command.clear_depth_enabled,
|
||||
.clear_depth = command.clear_depth,
|
||||
.clear_stencil_enabled = command.clear_stencil_enabled,
|
||||
.clear_stencil = command.clear_stencil,
|
||||
});
|
||||
planned.texture_format = texture_format_for_renderer_format(command.target_desc.format);
|
||||
planned.supported = texture_format_supported(planned.texture_format);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::set_viewport:
|
||||
planned.kind = OpenGlPlannedCommandKind::set_viewport;
|
||||
planned.viewport = viewport_for_renderer_viewport(command.viewport);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::set_scissor:
|
||||
planned.kind = OpenGlPlannedCommandKind::set_scissor;
|
||||
planned.scissor = scissor_rect_for_renderer_scissor(command.scissor);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::set_blend_state:
|
||||
planned.kind = OpenGlPlannedCommandKind::set_blend_state;
|
||||
planned.blend = blend_state_for_renderer_blend_state(command.blend_state);
|
||||
planned.supported = planned.blend.supported;
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::set_depth_state:
|
||||
planned.kind = OpenGlPlannedCommandKind::set_depth_state;
|
||||
planned.depth = depth_state_for_renderer_depth_state(command.depth_state);
|
||||
planned.supported = planned.depth.supported;
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::bind_shader:
|
||||
planned.kind = OpenGlPlannedCommandKind::bind_shader;
|
||||
planned.name = command.name;
|
||||
planned.supported = non_empty_name(planned.name);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::set_shader_uniform:
|
||||
planned.kind = OpenGlPlannedCommandKind::set_shader_uniform;
|
||||
planned.name = command.name;
|
||||
planned.uniform_bytes = command.uniform_bytes;
|
||||
planned.supported = non_empty_name(planned.name) && planned.uniform_bytes > 0U;
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::bind_texture:
|
||||
planned.kind = OpenGlPlannedCommandKind::bind_texture;
|
||||
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
|
||||
planned.texture_slot = command.texture_slot;
|
||||
planned.supported = texture_format_supported(planned.texture_format)
|
||||
&& planned.texture_slot < pp::renderer::max_texture_slots;
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::bind_sampler:
|
||||
planned.kind = OpenGlPlannedCommandKind::bind_sampler;
|
||||
planned.sampler = sampler_state_for_renderer_sampler_desc(command.sampler_desc);
|
||||
planned.sampler_slot = command.sampler_slot;
|
||||
planned.supported = planned.sampler.supported
|
||||
&& planned.sampler_slot < pp::renderer::max_texture_slots;
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::bind_mesh:
|
||||
planned.kind = OpenGlPlannedCommandKind::bind_mesh;
|
||||
planned.primitive_mode = primitive_mode_for_renderer_topology(command.mesh_desc.topology);
|
||||
planned.supported = planned.primitive_mode != 0U;
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::draw:
|
||||
planned.kind = OpenGlPlannedCommandKind::draw;
|
||||
planned.primitive_mode = primitive_mode_for_renderer_topology(command.mesh_desc.topology);
|
||||
planned.draw_vertex_count = command.draw_desc.vertex_count;
|
||||
planned.draw_index_count = command.draw_desc.index_count;
|
||||
planned.supported = planned.primitive_mode != 0U;
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::upload_texture:
|
||||
planned.kind = OpenGlPlannedCommandKind::upload_texture;
|
||||
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
|
||||
planned.readback_region = command.readback_region;
|
||||
planned.upload_bytes = command.upload_bytes;
|
||||
planned.supported = texture_format_supported(planned.texture_format);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::generate_mipmaps:
|
||||
planned.kind = OpenGlPlannedCommandKind::generate_mipmaps;
|
||||
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
|
||||
planned.generated_mip_levels = command.generated_mip_levels;
|
||||
planned.generated_mip_bytes = command.generated_mip_bytes;
|
||||
planned.supported = texture_format_supported(planned.texture_format);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::transition_texture:
|
||||
planned.kind = OpenGlPlannedCommandKind::transition_texture;
|
||||
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
|
||||
planned.before_state = command.before_state;
|
||||
planned.after_state = command.after_state;
|
||||
planned.supported = texture_format_supported(planned.texture_format)
|
||||
&& texture_state_supported(planned.before_state)
|
||||
&& texture_state_supported(planned.after_state);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::copy_texture:
|
||||
planned.kind = OpenGlPlannedCommandKind::copy_texture;
|
||||
planned.source_texture_format = texture_format_for_renderer_format(command.source_desc.format);
|
||||
planned.destination_texture_format = texture_format_for_renderer_format(command.destination_desc.format);
|
||||
planned.source_region = command.source_region;
|
||||
planned.destination_region = command.destination_region;
|
||||
planned.copy_source_bytes = command.copy_source_bytes;
|
||||
planned.copy_destination_bytes = command.copy_destination_bytes;
|
||||
planned.supported = texture_format_supported(planned.source_texture_format)
|
||||
&& texture_format_supported(planned.destination_texture_format);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::read_texture:
|
||||
planned.kind = OpenGlPlannedCommandKind::read_texture;
|
||||
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
|
||||
planned.readback_region = command.readback_region;
|
||||
planned.readback_bytes = command.readback_bytes;
|
||||
planned.supported = texture_format_supported(planned.texture_format);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::capture_frame:
|
||||
planned.kind = OpenGlPlannedCommandKind::capture_frame;
|
||||
planned.texture_format = texture_format_for_renderer_format(command.target_desc.format);
|
||||
planned.capture_bytes = command.capture_bytes;
|
||||
planned.supported = texture_format_supported(planned.texture_format);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::blit_render_target:
|
||||
planned.kind = OpenGlPlannedCommandKind::blit_render_target;
|
||||
planned.blit_filter = blit_filter_for_renderer_filter(command.blit_filter);
|
||||
planned.source_texture_format = texture_format_for_renderer_format(command.source_desc.format);
|
||||
planned.destination_texture_format = texture_format_for_renderer_format(command.destination_desc.format);
|
||||
planned.source_region = command.source_region;
|
||||
planned.destination_region = command.destination_region;
|
||||
planned.blit_source_bytes = command.blit_source_bytes;
|
||||
planned.blit_destination_bytes = command.blit_destination_bytes;
|
||||
planned.supported = planned.blit_filter.supported
|
||||
&& texture_format_supported(planned.source_texture_format)
|
||||
&& texture_format_supported(planned.destination_texture_format);
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::end_render_pass:
|
||||
planned.kind = OpenGlPlannedCommandKind::end_render_pass;
|
||||
break;
|
||||
case pp::renderer::RecordedRenderCommandKind::trace_marker:
|
||||
case pp::renderer::RecordedRenderCommandKind::trace_begin_scope:
|
||||
case pp::renderer::RecordedRenderCommandKind::trace_end_scope:
|
||||
planned.kind = OpenGlPlannedCommandKind::trace;
|
||||
break;
|
||||
default:
|
||||
planned.kind = OpenGlPlannedCommandKind::unknown;
|
||||
planned.supported = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return planned;
|
||||
}
|
||||
|
||||
OpenGlCommandPlan plan_recorded_render_commands(
|
||||
std::span<const pp::renderer::RecordedRenderCommand> commands)
|
||||
{
|
||||
OpenGlCommandPlan plan;
|
||||
plan.commands.reserve(commands.size());
|
||||
|
||||
bool in_render_pass = false;
|
||||
bool shader_bound_in_pass = false;
|
||||
bool mesh_bound_in_pass = false;
|
||||
for (std::size_t index = 0; index < commands.size(); ++index) {
|
||||
OpenGlPlannedCommand planned = plan_recorded_render_command(commands[index]);
|
||||
|
||||
if (!planned.supported) {
|
||||
record_unsupported_command(plan, index);
|
||||
}
|
||||
|
||||
switch (planned.kind) {
|
||||
case OpenGlPlannedCommandKind::begin_render_pass:
|
||||
++plan.render_pass_count;
|
||||
if (in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
in_render_pass = true;
|
||||
shader_bound_in_pass = false;
|
||||
mesh_bound_in_pass = false;
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::end_render_pass:
|
||||
if (!in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
in_render_pass = false;
|
||||
shader_bound_in_pass = false;
|
||||
mesh_bound_in_pass = false;
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::draw:
|
||||
++plan.draw_command_count;
|
||||
if (!in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
if (!shader_bound_in_pass || !mesh_bound_in_pass) {
|
||||
record_dependency_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::bind_shader:
|
||||
++plan.shader_bind_command_count;
|
||||
if (!in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
shader_bound_in_pass = true;
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::set_shader_uniform:
|
||||
++plan.uniform_command_count;
|
||||
if (!in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
if (!shader_bound_in_pass) {
|
||||
record_dependency_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::bind_texture:
|
||||
++plan.texture_bind_command_count;
|
||||
if (!in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::bind_sampler:
|
||||
++plan.sampler_bind_command_count;
|
||||
if (!in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::bind_mesh:
|
||||
if (!in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
mesh_bound_in_pass = true;
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::upload_texture:
|
||||
++plan.upload_command_count;
|
||||
if (in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::generate_mipmaps:
|
||||
++plan.mipmap_command_count;
|
||||
if (in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::transition_texture:
|
||||
++plan.transition_command_count;
|
||||
if (in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::copy_texture:
|
||||
++plan.copy_command_count;
|
||||
if (in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::read_texture:
|
||||
++plan.readback_command_count;
|
||||
if (in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::capture_frame:
|
||||
++plan.capture_command_count;
|
||||
if (in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::passthrough:
|
||||
++plan.passthrough_command_count;
|
||||
if (planned.requires_render_pass && !in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::trace:
|
||||
++plan.trace_command_count;
|
||||
break;
|
||||
case OpenGlPlannedCommandKind::blit_render_target:
|
||||
if (in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (planned.requires_render_pass && !in_render_pass) {
|
||||
record_render_pass_order_error(plan, index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
plan.commands.push_back(planned);
|
||||
}
|
||||
|
||||
plan.ended_in_render_pass = in_render_pass;
|
||||
if (in_render_pass) {
|
||||
record_render_pass_order_error(plan, commands.size());
|
||||
}
|
||||
|
||||
plan.supported = plan.unsupported_command_count == 0U
|
||||
&& plan.render_pass_order_error_count == 0U
|
||||
&& plan.dependency_error_count == 0U;
|
||||
return plan;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user