Compare commits
42 Commits
main
...
ad255a6ddf
| Author | SHA1 | Date | |
|---|---|---|---|
| ad255a6ddf | |||
| 88507df90e | |||
| 10e5d5b5ae | |||
| c16cab87bd | |||
| 7319cb9aa9 | |||
| 677d0b33a8 | |||
| f1ee1b28a1 | |||
| 2da247f0fb | |||
| 37854ea8b9 | |||
| dc252b2f24 | |||
| d0ef88be89 | |||
| 4ec2d093e8 | |||
| 4eee018367 | |||
| 44aebf61b2 | |||
| f6d3de8cbf | |||
| c62bc4d744 | |||
| 8ebb22325c | |||
| e5d98c2dc3 | |||
| abe578a338 | |||
| 313a360c01 | |||
| 551013c771 | |||
| cc377b5eb5 | |||
| 6c435dafb7 | |||
| 3f5711773e | |||
| a7bb04f54b | |||
| 6604f30ef3 | |||
| 93d8aaaffd | |||
| f9e4bcaeea | |||
| 3d80791245 | |||
| 126280ff7c | |||
| 20b5dba41e | |||
| dfdb7a4468 | |||
| 4d715afd60 | |||
| ac0d0ab49c | |||
| a67e7fc9bb | |||
| 31322bbd83 | |||
| 23eba07901 | |||
| 8014345b99 | |||
| 99eda95cee | |||
| ec5ecbdb54 | |||
| e0ea4597e6 | |||
| c38ff8209b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -53,3 +53,5 @@ linux/Makefile
|
||||
|
||||
webgl/build
|
||||
webgl/.vscode
|
||||
|
||||
out/
|
||||
|
||||
291
CMakeLists.txt
Normal file
291
CMakeLists.txt
Normal file
@@ -0,0 +1,291 @@
|
||||
cmake_minimum_required(VERSION 3.29)
|
||||
|
||||
project(PanoPainter
|
||||
VERSION 0.0.0
|
||||
DESCRIPTION "Panoramic painting and animation application"
|
||||
LANGUAGES C CXX)
|
||||
|
||||
if(POLICY CMP0091)
|
||||
cmake_policy(SET CMP0091 NEW)
|
||||
endif()
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
|
||||
include(PanoPainterOptions)
|
||||
if(PP_ENABLE_ASAN AND MSVC AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||
else()
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
include(PanoPainterWarnings)
|
||||
include(PanoPainterSources)
|
||||
include(PanoPainterVersion)
|
||||
|
||||
if(PP_ENABLE_CLANG_TIDY)
|
||||
find_program(PP_CLANG_TIDY_EXE NAMES clang-tidy)
|
||||
if(PP_CLANG_TIDY_EXE)
|
||||
set(CMAKE_CXX_CLANG_TIDY "${PP_CLANG_TIDY_EXE}")
|
||||
else()
|
||||
message(WARNING "PP_ENABLE_CLANG_TIDY is ON but clang-tidy was not found.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_CPPCHECK)
|
||||
find_program(PP_CPPCHECK_EXE NAMES cppcheck)
|
||||
if(PP_CPPCHECK_EXE)
|
||||
set(CMAKE_CXX_CPPCHECK
|
||||
"${PP_CPPCHECK_EXE}"
|
||||
"--enable=warning,style,performance,portability"
|
||||
"--inline-suppr"
|
||||
"--suppress=missingIncludeSystem")
|
||||
else()
|
||||
message(WARNING "PP_ENABLE_CPPCHECK is ON but cppcheck was not found.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(pp_project_options INTERFACE)
|
||||
target_compile_features(pp_project_options INTERFACE cxx_std_23)
|
||||
|
||||
add_library(pp_project_warnings INTERFACE)
|
||||
pp_configure_project_warnings(pp_project_warnings)
|
||||
|
||||
if(PP_USE_VCPKG_TINYXML2)
|
||||
find_package(tinyxml2 CONFIG REQUIRED)
|
||||
add_library(pp_xml_tinyxml2 INTERFACE)
|
||||
target_link_libraries(pp_xml_tinyxml2
|
||||
INTERFACE
|
||||
tinyxml2::tinyxml2)
|
||||
else()
|
||||
add_library(pp_vendor_tinyxml2 STATIC
|
||||
libs/tinyxml2/tinyxml2.cpp)
|
||||
target_include_directories(pp_vendor_tinyxml2
|
||||
SYSTEM PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/tinyxml2")
|
||||
target_link_libraries(pp_vendor_tinyxml2
|
||||
PUBLIC
|
||||
pp_project_options)
|
||||
add_library(pp_xml_tinyxml2 ALIAS pp_vendor_tinyxml2)
|
||||
endif()
|
||||
|
||||
add_custom_target(panopainter_modernization_status
|
||||
COMMAND "${CMAKE_COMMAND}" -E echo "PanoPainter modernization scaffold configured."
|
||||
COMMAND "${CMAKE_COMMAND}" -E echo "Roadmap: docs/modernization/roadmap.md"
|
||||
COMMAND "${CMAKE_COMMAND}" -E echo "Debt log: docs/modernization/debt.md"
|
||||
VERBATIM)
|
||||
|
||||
add_custom_target(panopainter_validate_shaders
|
||||
COMMAND "${CMAKE_COMMAND}"
|
||||
"-DPP_SHADER_DIR=${CMAKE_CURRENT_SOURCE_DIR}/data/shaders"
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ValidatePanoPainterShaders.cmake"
|
||||
VERBATIM)
|
||||
|
||||
add_library(pp_foundation STATIC
|
||||
src/foundation/binary_stream.cpp
|
||||
src/foundation/event.cpp
|
||||
src/foundation/log.cpp
|
||||
src/foundation/parse.cpp
|
||||
src/foundation/task_queue.cpp
|
||||
src/foundation/trace.cpp)
|
||||
target_include_directories(pp_foundation
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_foundation
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_assets STATIC
|
||||
src/assets/image_format.cpp
|
||||
src/assets/image_metadata.cpp
|
||||
src/assets/image_pixels.cpp
|
||||
src/assets/ppi_header.cpp
|
||||
src/assets/settings_document.cpp)
|
||||
target_include_directories(pp_assets
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_include_directories(pp_assets
|
||||
SYSTEM PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/stb")
|
||||
if(MSVC)
|
||||
set_source_files_properties(src/assets/image_pixels.cpp
|
||||
PROPERTIES
|
||||
COMPILE_OPTIONS "/analyze-")
|
||||
endif()
|
||||
target_link_libraries(pp_assets
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_paint STATIC
|
||||
src/paint/brush.cpp
|
||||
src/paint/blend.cpp
|
||||
src/paint/stroke.cpp
|
||||
src/paint/stroke_script.cpp)
|
||||
target_include_directories(pp_paint
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_paint
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_document STATIC
|
||||
src/document/document.cpp
|
||||
src/document/ppi_import.cpp)
|
||||
target_include_directories(pp_document
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_document
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_assets
|
||||
pp_paint
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_renderer_api STATIC
|
||||
src/renderer_api/renderer_api.cpp)
|
||||
target_include_directories(pp_renderer_api
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_renderer_api
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_paint_renderer STATIC
|
||||
src/paint_renderer/compositor.cpp)
|
||||
target_include_directories(pp_paint_renderer
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_paint_renderer
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_paint
|
||||
pp_renderer_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_ui_core STATIC
|
||||
src/ui_core/color.cpp
|
||||
src/ui_core/layout_value.cpp
|
||||
src/ui_core/layout_xml.cpp)
|
||||
target_include_directories(pp_ui_core
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_ui_core
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_xml_tinyxml2
|
||||
pp_project_warnings)
|
||||
|
||||
if(PP_BUILD_TOOLS)
|
||||
add_subdirectory(tools/pano_cli)
|
||||
endif()
|
||||
|
||||
if(PP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
if(PP_BUILD_APP)
|
||||
if(WIN32)
|
||||
add_library(pp_legacy_app STATIC
|
||||
${PP_LEGACY_APP_SOURCES}
|
||||
${PP_VENDOR_SOURCES})
|
||||
|
||||
target_link_libraries(pp_legacy_app
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
target_include_directories(pp_legacy_app
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
|
||||
target_compile_definitions(pp_legacy_app
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_app PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
target_precompile_headers(pp_legacy_app PRIVATE src/pch.h)
|
||||
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
|
||||
pp_legacy_app
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.lib"
|
||||
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.lib>"
|
||||
"$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.lib>"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/yuv.lib"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.lib"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.lib"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/lib/win64/openvr_api.lib"
|
||||
comdlg32
|
||||
gdi32
|
||||
opengl32
|
||||
ole32
|
||||
shell32
|
||||
shlwapi
|
||||
user32
|
||||
wbemuuid)
|
||||
|
||||
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)
|
||||
else()
|
||||
message(WARNING "PP_BUILD_APP is enabled, but the root CMake app target is currently Windows-only. Platform alignment is tracked in Phase 6.")
|
||||
endif()
|
||||
endif()
|
||||
255
CMakePresets.json
Normal file
255
CMakePresets.json
Normal file
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"version": 8,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 29,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "base",
|
||||
"hidden": true,
|
||||
"binaryDir": "${sourceDir}/out/build/${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||
"PP_BUILD_APP": "ON",
|
||||
"PP_BUILD_TESTS": "ON",
|
||||
"PP_BUILD_TOOLS": "ON",
|
||||
"PP_ENABLE_OPENGL": "ON",
|
||||
"PP_ENABLE_VULKAN_EXPERIMENTAL": "OFF",
|
||||
"PP_ENABLE_VR": "ON",
|
||||
"PP_ENABLE_CLOUD": "ON",
|
||||
"PP_ENABLE_VIDEO": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "platform-headless-base",
|
||||
"hidden": true,
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"PP_BUILD_APP": "OFF",
|
||||
"PP_ENABLE_CLOUD": "OFF",
|
||||
"PP_ENABLE_VIDEO": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-vs2026-x64",
|
||||
"inherits": "base",
|
||||
"displayName": "Windows VS 2026 x64",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": "x64"
|
||||
},
|
||||
{
|
||||
"name": "windows-msvc-default",
|
||||
"inherits": "base",
|
||||
"displayName": "Windows MSVC default generator",
|
||||
"architecture": "x64"
|
||||
},
|
||||
{
|
||||
"name": "windows-msvc-vcpkg-headless",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "Windows MSVC vcpkg headless",
|
||||
"architecture": "x64",
|
||||
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||
"cacheVariables": {
|
||||
"PP_USE_VCPKG_TINYXML2": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-clangcl-asan",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "Windows clang-cl ASan",
|
||||
"generator": "Ninja",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "clang-cl",
|
||||
"CMAKE_CXX_COMPILER": "clang-cl",
|
||||
"CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreadedDLL",
|
||||
"PP_ENABLE_ASAN": "ON",
|
||||
"PP_ENABLE_UBSAN": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-clang",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "Linux clang",
|
||||
"generator": "Ninja",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "clang",
|
||||
"CMAKE_CXX_COMPILER": "clang++"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-base",
|
||||
"hidden": true,
|
||||
"inherits": "platform-headless-base",
|
||||
"generator": "Ninja",
|
||||
"toolchainFile": "$env{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake",
|
||||
"cacheVariables": {
|
||||
"ANDROID_PLATFORM": "android-26",
|
||||
"ANDROID_STL": "c++_shared",
|
||||
"PP_ENABLE_VR": "OFF",
|
||||
"PP_ENABLE_OPENGL": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-arm64",
|
||||
"inherits": "android-base",
|
||||
"displayName": "Android arm64-v8a",
|
||||
"cacheVariables": {
|
||||
"ANDROID_ABI": "arm64-v8a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-x64",
|
||||
"inherits": "android-base",
|
||||
"displayName": "Android x86_64",
|
||||
"cacheVariables": {
|
||||
"ANDROID_ABI": "x86_64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-quest-arm64",
|
||||
"inherits": "android-base",
|
||||
"displayName": "Android Quest arm64-v8a",
|
||||
"cacheVariables": {
|
||||
"ANDROID_ABI": "arm64-v8a",
|
||||
"PP_ENABLE_VR": "ON",
|
||||
"PP_ANDROID_FLAVOR": "quest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-focus-arm64",
|
||||
"inherits": "android-base",
|
||||
"displayName": "Android Focus/Wave arm64-v8a",
|
||||
"cacheVariables": {
|
||||
"ANDROID_ABI": "arm64-v8a",
|
||||
"PP_ENABLE_VR": "ON",
|
||||
"PP_ANDROID_FLAVOR": "focus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "emscripten",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "Emscripten WebGL",
|
||||
"generator": "Ninja",
|
||||
"cacheVariables": {
|
||||
"PP_ENABLE_VR": "OFF",
|
||||
"PP_ENABLE_VIDEO": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "macos",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "macOS",
|
||||
"generator": "Ninja"
|
||||
},
|
||||
{
|
||||
"name": "ios-device",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "iOS device",
|
||||
"generator": "Xcode",
|
||||
"cacheVariables": {
|
||||
"CMAKE_SYSTEM_NAME": "iOS",
|
||||
"CMAKE_OSX_SYSROOT": "iphoneos"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ios-simulator",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "iOS simulator",
|
||||
"generator": "Xcode",
|
||||
"cacheVariables": {
|
||||
"CMAKE_SYSTEM_NAME": "iOS",
|
||||
"CMAKE_OSX_SYSROOT": "iphonesimulator"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "windows-vs2026-x64",
|
||||
"configurePreset": "windows-vs2026-x64"
|
||||
},
|
||||
{
|
||||
"name": "windows-msvc-default",
|
||||
"configurePreset": "windows-msvc-default"
|
||||
},
|
||||
{
|
||||
"name": "windows-msvc-vcpkg-headless",
|
||||
"configurePreset": "windows-msvc-vcpkg-headless"
|
||||
},
|
||||
{
|
||||
"name": "windows-clangcl-asan",
|
||||
"configurePreset": "windows-clangcl-asan"
|
||||
},
|
||||
{
|
||||
"name": "linux-clang",
|
||||
"configurePreset": "linux-clang"
|
||||
},
|
||||
{
|
||||
"name": "android-arm64",
|
||||
"configurePreset": "android-arm64"
|
||||
},
|
||||
{
|
||||
"name": "android-x64",
|
||||
"configurePreset": "android-x64"
|
||||
},
|
||||
{
|
||||
"name": "android-quest-arm64",
|
||||
"configurePreset": "android-quest-arm64"
|
||||
},
|
||||
{
|
||||
"name": "android-focus-arm64",
|
||||
"configurePreset": "android-focus-arm64"
|
||||
}
|
||||
],
|
||||
"testPresets": [
|
||||
{
|
||||
"name": "desktop-fast",
|
||||
"configurePreset": "windows-msvc-default",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"exclude": {
|
||||
"label": "gpu|slow|platform"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "desktop-fast-vs2026",
|
||||
"configurePreset": "windows-vs2026-x64",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"exclude": {
|
||||
"label": "gpu|slow|platform"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "desktop-fast-vcpkg",
|
||||
"configurePreset": "windows-msvc-vcpkg-headless",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"exclude": {
|
||||
"label": "gpu|slow|platform"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "desktop-gpu",
|
||||
"configurePreset": "windows-msvc-default",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"include": {
|
||||
"label": "gpu"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.28010.2026
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PanoPainter", "PanoPainter.vcxproj", "{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}"
|
||||
EndProject
|
||||
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "PanoPainterPackage", "PanoPainterPackage\PanoPainterPackage.wapproj", "{3A716FB6-DE62-439F-83B6-3C40915D6678}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|Any CPU.ActiveCfg = Debug|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.Build.0 = Debug|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.Deploy.0 = Debug|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x86.Build.0 = Debug|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|Any CPU.ActiveCfg = Release|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.ActiveCfg = Release|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.Build.0 = Release|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.Deploy.0 = Release|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x86.ActiveCfg = Release|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x86.Build.0 = Release|Win32
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.Build.0 = Debug|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.Deploy.0 = Debug|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.Build.0 = Debug|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.Deploy.0 = Debug|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.ActiveCfg = Release|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.Build.0 = Release|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.Deploy.0 = Release|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.ActiveCfg = Release|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.Build.0 = Release|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.Deploy.0 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {3E8EFC4B-CEA1-4408-8628-7D2C0F6C43C8}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,634 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>PanoPainter</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
|
||||
<ProjectName>PanoPainter</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v141</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v141</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\source\Client;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\bin;$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;libs\bugtrap-client\include;libs\poly2tri\poly2tri;libs\base64;libs\sqlite3;libs\openvr\headers;libs\nanort;libs\hash-library;libs\fmt\include;libs\glad\include;libs\openh264\include;libs\mp4v2\include;libs\libyuv\include;C:\Program Files\RenderDoc;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);libs\bugtrap-client\lib;libs\openvr\lib\win64;libs\openh264\lib;libs\mp4v2\lib\win;libs\libyuv\lib\win;$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\source\Client;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\bin;$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;libs\bugtrap-client\include;libs\poly2tri\poly2tri;libs\base64;libs\sqlite3;libs\openvr\headers;libs\nanort;libs\hash-library;libs\fmt\include;libs\glad\include;libs\openh264\include;libs\mp4v2\include;libs\libyuv\include;C:\Program Files\RenderDoc;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);libs\bugtrap-client\lib;libs\openvr\lib\win64;libs\openh264\lib;libs\mp4v2\lib\win;libs\libyuv\lib\win;$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>
|
||||
</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>ENUM_BITFIELDS_NOT_SUPPORTED;DEBUG;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<ExceptionHandling>false</ExceptionHandling>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>python .\scripts\pre-build.py debug</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>
|
||||
</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>ENUM_BITFIELDS_NOT_SUPPORTED;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<ExceptionHandling>false</ExceptionHandling>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>python .\scripts\pre-build.py release</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="libs\fmt\src\format.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\fmt\src\posix.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\glad\src\glad.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\glad\src\glad_wgl.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\hash-library\md5.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\nanort\nanort.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\sqlite3\sqlite3.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\event\event.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\internal\experiments.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\log.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\Utils.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGConfig.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGEnums.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGLayout.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGNode.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGNodePrint.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGStyle.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGValue.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\Yoga.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\abr.cpp" />
|
||||
<ClCompile Include="src\action.cpp" />
|
||||
<ClCompile Include="src\app.cpp" />
|
||||
<ClCompile Include="src\app_cloud.cpp" />
|
||||
<ClCompile Include="src\app_commands.cpp" />
|
||||
<ClCompile Include="src\app_dialogs.cpp" />
|
||||
<ClCompile Include="src\app_events.cpp" />
|
||||
<ClCompile Include="src\app_layout.cpp" />
|
||||
<ClCompile Include="src\app_shaders.cpp" />
|
||||
<ClCompile Include="src\app_vr.cpp" />
|
||||
<ClCompile Include="src\asset.cpp" />
|
||||
<ClCompile Include="src\bezier.cpp" />
|
||||
<ClCompile Include="src\binary_stream.cpp" />
|
||||
<ClCompile Include="src\brush.cpp" />
|
||||
<ClCompile Include="src\canvas.cpp" />
|
||||
<ClCompile Include="src\canvas_actions.cpp" />
|
||||
<ClCompile Include="src\canvas_layer.cpp" />
|
||||
<ClCompile Include="src\canvas_modes.cpp" />
|
||||
<ClCompile Include="src\event.cpp" />
|
||||
<ClCompile Include="src\font.cpp" />
|
||||
<ClCompile Include="src\hmd.cpp" />
|
||||
<ClCompile Include="src\image.cpp" />
|
||||
<ClCompile Include="src\layout.cpp" />
|
||||
<ClCompile Include="src\log.cpp" />
|
||||
<ClCompile Include="src\main.cpp" />
|
||||
<ClCompile Include="src\mp4enc.cpp" />
|
||||
<ClCompile Include="src\node.cpp" />
|
||||
<ClCompile Include="src\node_about.cpp" />
|
||||
<ClCompile Include="src\node_border.cpp" />
|
||||
<ClCompile Include="src\node_button.cpp" />
|
||||
<ClCompile Include="src\node_button_custom.cpp" />
|
||||
<ClCompile Include="src\node_canvas.cpp" />
|
||||
<ClCompile Include="src\node_changelog.cpp" />
|
||||
<ClCompile Include="src\node_checkbox.cpp" />
|
||||
<ClCompile Include="src\node_colorwheel.cpp" />
|
||||
<ClCompile Include="src\node_color_quad.cpp" />
|
||||
<ClCompile Include="src\node_combobox.cpp" />
|
||||
<ClCompile Include="src\node_dialog_browse.cpp" />
|
||||
<ClCompile Include="src\node_dialog_cloud.cpp" />
|
||||
<ClCompile Include="src\node_dialog_export_ppbr.cpp" />
|
||||
<ClCompile Include="src\node_dialog_layer_rename.cpp" />
|
||||
<ClCompile Include="src\node_dialog_open.cpp" />
|
||||
<ClCompile Include="src\node_dialog_picker.cpp" />
|
||||
<ClCompile Include="src\node_dialog_resize.cpp" />
|
||||
<ClCompile Include="src\node_icon.cpp" />
|
||||
<ClCompile Include="src\node_image.cpp" />
|
||||
<ClCompile Include="src\node_image_texture.cpp" />
|
||||
<ClCompile Include="src\node_input_box.cpp" />
|
||||
<ClCompile Include="src\node_message_box.cpp" />
|
||||
<ClCompile Include="src\node_metadata.cpp" />
|
||||
<ClCompile Include="src\node_panel_brush.cpp" />
|
||||
<ClCompile Include="src\node_panel_color.cpp" />
|
||||
<ClCompile Include="src\node_panel_floating.cpp" />
|
||||
<ClCompile Include="src\node_panel_grid.cpp" />
|
||||
<ClCompile Include="src\node_panel_layer.cpp" />
|
||||
<ClCompile Include="src\node_panel_quick.cpp" />
|
||||
<ClCompile Include="src\node_panel_stroke.cpp" />
|
||||
<ClCompile Include="src\node_panel_animation.cpp" />
|
||||
<ClCompile Include="src\node_popup_menu.cpp" />
|
||||
<ClCompile Include="src\node_progress_bar.cpp" />
|
||||
<ClCompile Include="src\node_remote_page.cpp" />
|
||||
<ClCompile Include="src\node_scroll.cpp" />
|
||||
<ClCompile Include="src\node_settings.cpp" />
|
||||
<ClCompile Include="src\node_shorcuts.cpp" />
|
||||
<ClCompile Include="src\node_slider.cpp" />
|
||||
<ClCompile Include="src\node_stroke_preview.cpp" />
|
||||
<ClCompile Include="src\node_text.cpp" />
|
||||
<ClCompile Include="src\node_text_input.cpp" />
|
||||
<ClCompile Include="src\node_tool_bucket.cpp" />
|
||||
<ClCompile Include="src\node_usermanual.cpp" />
|
||||
<ClCompile Include="src\node_viewport.cpp" />
|
||||
<ClCompile Include="src\pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\rtt.cpp" />
|
||||
<ClCompile Include="src\serializer.cpp" />
|
||||
<ClCompile Include="src\settings.cpp" />
|
||||
<ClCompile Include="src\shader.cpp" />
|
||||
<ClCompile Include="src\shape.cpp" />
|
||||
<ClCompile Include="src\texture.cpp" />
|
||||
<ClCompile Include="src\util.cpp" />
|
||||
<ClCompile Include="src\version.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\wacom.cpp" />
|
||||
<ClCompile Include="libs\jpeg\jpgd.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\jpeg\jpge.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\common\shapes.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\advancing_front.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\cdt.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep_context.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\tinyxml2\tinyxml2.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\wacom\WinTab\Utils.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="libs\hash-library\md5.h" />
|
||||
<ClInclude Include="libs\nanort\nanort.h" />
|
||||
<ClInclude Include="libs\sqlite3\sqlite3.h" />
|
||||
<ClInclude Include="libs\sqlite3\sqlite3ext.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="src\abr.h" />
|
||||
<ClInclude Include="src\action.h" />
|
||||
<ClInclude Include="src\app.h" />
|
||||
<ClInclude Include="src\asset.h" />
|
||||
<ClInclude Include="src\bezier.h" />
|
||||
<ClInclude Include="src\binary_stream.h" />
|
||||
<ClInclude Include="src\brush.h" />
|
||||
<ClInclude Include="src\canvas.h" />
|
||||
<ClInclude Include="src\canvas_actions.h" />
|
||||
<ClInclude Include="src\canvas_layer.h" />
|
||||
<ClInclude Include="src\canvas_modes.h" />
|
||||
<ClInclude Include="src\event.h" />
|
||||
<ClInclude Include="src\font.h" />
|
||||
<ClInclude Include="src\hmd.h" />
|
||||
<ClInclude Include="src\image.h" />
|
||||
<ClInclude Include="src\keymap.h" />
|
||||
<ClInclude Include="src\layout.h" />
|
||||
<ClInclude Include="src\log.h" />
|
||||
<ClInclude Include="src\mp4enc.h" />
|
||||
<ClInclude Include="src\node.h" />
|
||||
<ClInclude Include="src\node_about.h" />
|
||||
<ClInclude Include="src\node_border.h" />
|
||||
<ClInclude Include="src\node_button.h" />
|
||||
<ClInclude Include="src\node_button_custom.h" />
|
||||
<ClInclude Include="src\node_canvas.h" />
|
||||
<ClInclude Include="src\node_changelog.h" />
|
||||
<ClInclude Include="src\node_checkbox.h" />
|
||||
<ClInclude Include="src\node_colorwheel.h" />
|
||||
<ClInclude Include="src\node_color_quad.h" />
|
||||
<ClInclude Include="src\node_combobox.h" />
|
||||
<ClInclude Include="src\node_dialog_browse.h" />
|
||||
<ClInclude Include="src\node_dialog_cloud.h" />
|
||||
<ClInclude Include="src\node_dialog_export_ppbr.h" />
|
||||
<ClInclude Include="src\node_dialog_layer_rename.h" />
|
||||
<ClInclude Include="src\node_dialog_open.h" />
|
||||
<ClInclude Include="src\node_dialog_picker.h" />
|
||||
<ClInclude Include="src\node_dialog_resize.h" />
|
||||
<ClInclude Include="src\node_icon.h" />
|
||||
<ClInclude Include="src\node_image.h" />
|
||||
<ClInclude Include="src\node_image_texture.h" />
|
||||
<ClInclude Include="src\node_input_box.h" />
|
||||
<ClInclude Include="src\node_message_box.h" />
|
||||
<ClInclude Include="src\node_metadata.h" />
|
||||
<ClInclude Include="src\node_panel_brush.h" />
|
||||
<ClInclude Include="src\node_panel_color.h" />
|
||||
<ClInclude Include="src\node_panel_floating.h" />
|
||||
<ClInclude Include="src\node_panel_grid.h" />
|
||||
<ClInclude Include="src\node_panel_layer.h" />
|
||||
<ClInclude Include="src\node_panel_quick.h" />
|
||||
<ClInclude Include="src\node_panel_stroke.h" />
|
||||
<ClInclude Include="src\node_panel_animation.h" />
|
||||
<ClInclude Include="src\node_popup_menu.h" />
|
||||
<ClInclude Include="src\node_progress_bar.h" />
|
||||
<ClInclude Include="src\node_remote_page.h" />
|
||||
<ClInclude Include="src\node_scroll.h" />
|
||||
<ClInclude Include="src\node_settings.h" />
|
||||
<ClInclude Include="src\node_shorcuts.h" />
|
||||
<ClInclude Include="src\node_slider.h" />
|
||||
<ClInclude Include="src\node_stroke_preview.h" />
|
||||
<ClInclude Include="src\node_text.h" />
|
||||
<ClInclude Include="src\node_text_input.h" />
|
||||
<ClInclude Include="src\node_tool_bucket.h" />
|
||||
<ClInclude Include="src\node_usermanual.h" />
|
||||
<ClInclude Include="src\node_viewport.h" />
|
||||
<ClInclude Include="src\pch.h" />
|
||||
<ClInclude Include="src\rtt.h" />
|
||||
<ClInclude Include="src\serializer.h" />
|
||||
<ClInclude Include="src\settings.h" />
|
||||
<ClInclude Include="src\shader.h" />
|
||||
<ClInclude Include="src\shape.h" />
|
||||
<ClInclude Include="src\texture.h" />
|
||||
<ClInclude Include="src\util.h" />
|
||||
<ClInclude Include="src\version.gen.h" />
|
||||
<ClInclude Include="src\version.h" />
|
||||
<ClInclude Include="src\wacom.h" />
|
||||
<ClInclude Include="libs\jpeg\jpgd.h" />
|
||||
<ClInclude Include="libs\jpeg\jpge.h" />
|
||||
<ClInclude Include="libs\tinyxml2\tinyxml2.h" />
|
||||
<ClInclude Include="libs\wacom\WinTab\MSGPACK.H" />
|
||||
<ClInclude Include="libs\wacom\WinTab\PKTDEF.H" />
|
||||
<ClInclude Include="libs\wacom\WinTab\Utils.h" />
|
||||
<ClInclude Include="libs\wacom\WinTab\WINTAB.H" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="PanoPainter.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="libs\glm\util\glm.natvis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Xml Include="data\dialogs\about.xml" />
|
||||
<Xml Include="data\dialogs\brush-export.xml" />
|
||||
<Xml Include="data\dialogs\changelog.xml" />
|
||||
<Xml Include="data\dialogs\cloud-browse.xml" />
|
||||
<Xml Include="data\dialogs\color-picker.xml" />
|
||||
<Xml Include="data\dialogs\doc-browse.xml" />
|
||||
<Xml Include="data\dialogs\doc-new.xml" />
|
||||
<Xml Include="data\dialogs\doc-open.xml" />
|
||||
<Xml Include="data\dialogs\doc-resize.xml" />
|
||||
<Xml Include="data\dialogs\doc-save.xml" />
|
||||
<Xml Include="data\dialogs\input-box.xml" />
|
||||
<Xml Include="data\dialogs\layer-rename.xml" />
|
||||
<Xml Include="data\dialogs\message-box.xml" />
|
||||
<Xml Include="data\dialogs\panel-animation.xml" />
|
||||
<Xml Include="data\dialogs\panel-floating.xml" />
|
||||
<Xml Include="data\dialogs\panel-grid.xml" />
|
||||
<Xml Include="data\dialogs\panel-layers.xml" />
|
||||
<Xml Include="data\dialogs\panel-brushes.xml" />
|
||||
<Xml Include="data\dialogs\panel-presets.xml" />
|
||||
<Xml Include="data\dialogs\panel-quick.xml" />
|
||||
<Xml Include="data\dialogs\panel-stroke.xml" />
|
||||
<Xml Include="data\dialogs\progress-bar.xml" />
|
||||
<Xml Include="data\dialogs\remote-page.xml" />
|
||||
<Xml Include="data\dialogs\settings.xml" />
|
||||
<Xml Include="data\dialogs\shortcuts.xml" />
|
||||
<Xml Include="data\dialogs\usermanual.xml" />
|
||||
<Xml Include="data\layout.xml">
|
||||
<SubType>Designer</SubType>
|
||||
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
</DeploymentContent>
|
||||
</Xml>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="data\shaders\atlas.glsl" />
|
||||
<None Include="data\shaders\bake-uv.glsl" />
|
||||
<None Include="data\shaders\checkerboard.glsl" />
|
||||
<None Include="data\shaders\color-hue.glsl" />
|
||||
<None Include="data\shaders\color-quad.glsl" />
|
||||
<None Include="data\shaders\color-tri.glsl" />
|
||||
<None Include="data\shaders\color.glsl" />
|
||||
<None Include="data\shaders\comp-draw.glsl" />
|
||||
<None Include="data\shaders\comp-erase.glsl" />
|
||||
<None Include="data\shaders\equirect.glsl" />
|
||||
<None Include="data\shaders\font.glsl" />
|
||||
<None Include="data\shaders\include\blend-stroke.glsl" />
|
||||
<None Include="data\shaders\include\blend.glsl" />
|
||||
<None Include="data\shaders\include\blur.glsl" />
|
||||
<None Include="data\shaders\include\color.glsl" />
|
||||
<None Include="data\shaders\include\ext-fb-fetch.glsl" />
|
||||
<None Include="data\shaders\include\hsv.glsl" />
|
||||
<None Include="data\shaders\include\rand.glsl" />
|
||||
<None Include="data\shaders\lambert.glsl" />
|
||||
<None Include="data\shaders\lightmap.glsl" />
|
||||
<None Include="data\shaders\stroke-dilate.glsl" />
|
||||
<None Include="data\shaders\stroke-instanced.glsl" />
|
||||
<None Include="data\shaders\stroke-pad.glsl" />
|
||||
<None Include="data\shaders\stroke-preview.glsl" />
|
||||
<None Include="data\shaders\stroke.glsl" />
|
||||
<None Include="data\shaders\texture-alpha.glsl" />
|
||||
<None Include="data\shaders\texture-blend.glsl" />
|
||||
<None Include="data\shaders\texture-colorize.glsl" />
|
||||
<None Include="data\shaders\texture-mask.glsl" />
|
||||
<None Include="data\shaders\texture.glsl" />
|
||||
<None Include="data\shaders\uvs.glsl" />
|
||||
<None Include="data\shaders\vertex-color.glsl" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="icon.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Xsd Include="extra\layout.xsd">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
||||
</Xsd>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -1,854 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\ui">
|
||||
<UniqueIdentifier>{600b8daa-4234-4c37-b4ba-c22cad7d1dc3}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs">
|
||||
<UniqueIdentifier>{6d64b115-02d1-43e0-86c8-c8212f51162d}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\jpeg">
|
||||
<UniqueIdentifier>{dc178d53-6a6d-4a18-a93c-d4994340515f}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\WinTab">
|
||||
<UniqueIdentifier>{54dc9f46-d2e0-466c-90d2-eb5d72d5799d}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\yoga">
|
||||
<UniqueIdentifier>{a4a12057-835e-47ff-be4d-ce58b36cecf5}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\tinyxml2">
|
||||
<UniqueIdentifier>{6fe315aa-e2b9-4f01-8291-683a5fda123b}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\poly2tri">
|
||||
<UniqueIdentifier>{bda6fa93-a186-41ca-9bd9-49b7e0fd1ca4}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="extras">
|
||||
<UniqueIdentifier>{e631ac80-1b9b-424f-8adf-e2bab71a566d}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\sqlite3">
|
||||
<UniqueIdentifier>{ef44d179-f28b-458c-b3df-be2895553149}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\nanort">
|
||||
<UniqueIdentifier>{be0c0053-abd8-4e2d-a294-7c54511b05a6}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\hash">
|
||||
<UniqueIdentifier>{2a784067-6741-47a3-b668-cc45f2224286}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\fmt">
|
||||
<UniqueIdentifier>{7b4f5b47-7a8b-4e4c-9e82-399bb5047ffc}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="shaders">
|
||||
<UniqueIdentifier>{b55fb692-a845-4ef2-9b0e-5b2dd8bd125f}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="shaders\include">
|
||||
<UniqueIdentifier>{a2cacb13-2854-44ee-9511-6cb8ac587428}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\glad">
|
||||
<UniqueIdentifier>{ca37521b-213f-4bcf-acfd-eda1483a30b2}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="extras\dialogs">
|
||||
<UniqueIdentifier>{5ecb54ed-7c3d-46fd-9b5d-227abdbc5954}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\app.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\image.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\shader.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\shape.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\texture.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\pch.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\font.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\util.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\asset.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\rtt.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\bezier.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\canvas.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\brush.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\log.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\action.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\event.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\canvas_modes.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_border.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_button.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_button_custom.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_canvas.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_checkbox.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_color_quad.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_open.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_icon.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_image.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_image_texture.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_message_box.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_brush.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_color.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_layer.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_stroke.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_popup_menu.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_settings.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_slider.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_stroke_preview.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_text.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_text_input.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_viewport.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\layout.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_scroll.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_shaders.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_layout.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_events.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\jpeg\jpgd.cpp">
|
||||
<Filter>libs\jpeg</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\jpeg\jpge.cpp">
|
||||
<Filter>libs\jpeg</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\tinyxml2\tinyxml2.cpp">
|
||||
<Filter>libs\tinyxml2</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\wacom.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\wacom\WinTab\Utils.cpp">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_layer_rename.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_dialogs.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\common\shapes.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\advancing_front.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\cdt.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep_context.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_progress_bar.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_browse.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_commands.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_cloud.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_cloud.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_combobox.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_picker.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_colorwheel.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_grid.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\version.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_about.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_changelog.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_usermanual.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_resize.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\sqlite3\sqlite3.c">
|
||||
<Filter>libs\sqlite3</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\hmd.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_vr.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\nanort\nanort.cc">
|
||||
<Filter>libs\nanort</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\hash-library\md5.cpp">
|
||||
<Filter>libs\hash</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\abr.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\fmt\src\format.cc">
|
||||
<Filter>libs\fmt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\fmt\src\posix.cc">
|
||||
<Filter>libs\fmt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_quick.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\binary_stream.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\serializer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGConfig.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGEnums.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGLayout.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGNode.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGNodePrint.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGStyle.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGValue.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\Yoga.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\log.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\Utils.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_floating.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\settings.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\canvas_actions.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\canvas_layer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_tool_bucket.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\glad\src\glad.c">
|
||||
<Filter>libs\glad</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\glad\src\glad_wgl.c">
|
||||
<Filter>libs\glad</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_export_ppbr.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_input_box.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_animation.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\internal\experiments.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\event\event.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\mp4enc.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_remote_page.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_metadata.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_shorcuts.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="libs\jpeg\jpgd.h">
|
||||
<Filter>libs\jpeg</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\jpeg\jpge.h">
|
||||
<Filter>libs\jpeg</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\tinyxml2\tinyxml2.h">
|
||||
<Filter>libs\tinyxml2</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\wacom\WinTab\PKTDEF.H">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\wacom\WinTab\Utils.h">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\wacom\WinTab\WINTAB.H">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\wacom\WinTab\MSGPACK.H">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\sqlite3\sqlite3.h">
|
||||
<Filter>libs\sqlite3</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\sqlite3\sqlite3ext.h">
|
||||
<Filter>libs\sqlite3</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\nanort\nanort.h">
|
||||
<Filter>libs\nanort</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\hash-library\md5.h">
|
||||
<Filter>libs\hash</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\abr.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\action.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\app.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\asset.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\bezier.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\binary_stream.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\brush.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\canvas.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\canvas_actions.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\canvas_layer.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\canvas_modes.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\event.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\font.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\hmd.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\image.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\keymap.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\log.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\pch.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\rtt.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\serializer.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\settings.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\shader.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\shape.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\texture.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\util.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\version.gen.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\version.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\wacom.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\layout.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_about.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_border.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_button.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_button_custom.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_canvas.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_changelog.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_checkbox.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_color_quad.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_colorwheel.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_combobox.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_browse.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_cloud.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_export_ppbr.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_layer_rename.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_open.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_picker.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_resize.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_icon.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_image.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_image_texture.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_input_box.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_message_box.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_brush.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_color.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_floating.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_grid.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_layer.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_quick.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_stroke.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_popup_menu.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_progress_bar.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_scroll.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_settings.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_slider.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_stroke_preview.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_text.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_text_input.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_tool_bucket.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_usermanual.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_viewport.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_animation.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_remote_page.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_metadata.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_shorcuts.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\mp4enc.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="PanoPainter.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="libs\glm\util\glm.natvis">
|
||||
<Filter>extras</Filter>
|
||||
</Natvis>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Xml Include="data\layout.xml">
|
||||
<Filter>extras</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\changelog.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\about.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\usermanual.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\brush-export.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-layers.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-brushes.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-stroke.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-grid.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-quick.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\color-picker.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\input-box.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\message-box.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\progress-bar.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\layer-rename.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-resize.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-browse.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-new.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-save.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\cloud-browse.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\settings.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-open.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-floating.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-presets.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-animation.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\remote-page.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\shortcuts.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="data\shaders\texture.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\blend-stroke.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\blur.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\color.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\ext-fb-fetch.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\hsv.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\rand.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\blend.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\comp-draw.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\comp-erase.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\equirect.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\font.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\lambert.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\lightmap.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke-instanced.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke-preview.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\texture-alpha.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\texture-blend.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\uvs.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\vertex-color.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\atlas.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\bake-uv.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\checkerboard.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\color.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\color-hue.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\color-quad.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\color-tri.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\texture-colorize.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\texture-mask.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke-dilate.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke-pad.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="icon.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Xsd Include="extra\layout.xsd">
|
||||
<Filter>extras</Filter>
|
||||
</Xsd>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
20
cmake/PanoPainterOptions.cmake
Normal file
20
cmake/PanoPainterOptions.cmake
Normal file
@@ -0,0 +1,20 @@
|
||||
option(PP_BUILD_APP "Build the PanoPainter application target from root CMake." ON)
|
||||
option(PP_BUILD_TESTS "Build PanoPainter tests." ON)
|
||||
option(PP_BUILD_TOOLS "Build PanoPainter automation tools." ON)
|
||||
|
||||
option(PP_ENABLE_OPENGL "Enable the OpenGL renderer backend." ON)
|
||||
option(PP_ENABLE_VULKAN_EXPERIMENTAL "Enable non-production Vulkan experiments." OFF)
|
||||
option(PP_ENABLE_VR "Enable VR support." ON)
|
||||
option(PP_ENABLE_CLOUD "Enable cloud/network features." ON)
|
||||
option(PP_ENABLE_VIDEO "Enable MP4/timelapse video features." ON)
|
||||
|
||||
option(PP_ENABLE_ASAN "Enable AddressSanitizer where supported." OFF)
|
||||
option(PP_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer where supported." OFF)
|
||||
option(PP_ENABLE_TSAN "Enable ThreadSanitizer for headless targets where supported." OFF)
|
||||
option(PP_ENABLE_MSVC_ANALYZE "Enable MSVC static analysis." OFF)
|
||||
option(PP_ENABLE_CLANG_TIDY "Enable clang-tidy integration." OFF)
|
||||
option(PP_ENABLE_CPPCHECK "Enable cppcheck integration." OFF)
|
||||
option(PP_USE_VCPKG_TINYXML2 "Use the vcpkg tinyxml2 package for component targets." OFF)
|
||||
|
||||
set(PP_ANDROID_FLAVOR "standard" CACHE STRING "Android package flavor: standard, quest, or focus.")
|
||||
set_property(CACHE PP_ANDROID_FLAVOR PROPERTY STRINGS standard quest focus)
|
||||
143
cmake/PanoPainterSources.cmake
Normal file
143
cmake/PanoPainterSources.cmake
Normal file
@@ -0,0 +1,143 @@
|
||||
set(PP_LEGACY_APP_SOURCES
|
||||
src/abr.cpp
|
||||
src/action.cpp
|
||||
src/app.cpp
|
||||
src/app_cloud.cpp
|
||||
src/app_commands.cpp
|
||||
src/app_dialogs.cpp
|
||||
src/app_events.cpp
|
||||
src/app_layout.cpp
|
||||
src/app_shaders.cpp
|
||||
src/app_vr.cpp
|
||||
src/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/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
|
||||
src/node_dialog_layer_rename.cpp
|
||||
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
|
||||
src/node_panel_brush.cpp
|
||||
src/node_panel_color.cpp
|
||||
src/node_panel_floating.cpp
|
||||
src/node_panel_grid.cpp
|
||||
src/node_panel_layer.cpp
|
||||
src/node_panel_quick.cpp
|
||||
src/node_panel_stroke.cpp
|
||||
src/node_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_APP_SOURCES
|
||||
src/main.cpp
|
||||
PanoPainter.rc
|
||||
)
|
||||
|
||||
set(PP_VENDOR_SOURCES
|
||||
libs/fmt/src/format.cc
|
||||
libs/fmt/src/posix.cc
|
||||
libs/glad/src/glad.c
|
||||
libs/glad/src/glad_wgl.c
|
||||
libs/hash-library/md5.cpp
|
||||
libs/jpeg/jpgd.cpp
|
||||
libs/jpeg/jpge.cpp
|
||||
libs/nanort/nanort.cc
|
||||
libs/poly2tri/poly2tri/common/shapes.cc
|
||||
libs/poly2tri/poly2tri/sweep/advancing_front.cc
|
||||
libs/poly2tri/poly2tri/sweep/cdt.cc
|
||||
libs/poly2tri/poly2tri/sweep/sweep.cc
|
||||
libs/poly2tri/poly2tri/sweep/sweep_context.cc
|
||||
libs/sqlite3/sqlite3.c
|
||||
libs/tinyxml2/tinyxml2.cpp
|
||||
libs/wacom/WinTab/Utils.cpp
|
||||
libs/yoga/yoga/event/event.cpp
|
||||
libs/yoga/yoga/internal/experiments.cpp
|
||||
libs/yoga/yoga/log.cpp
|
||||
libs/yoga/yoga/Utils.cpp
|
||||
libs/yoga/yoga/YGConfig.cpp
|
||||
libs/yoga/yoga/YGEnums.cpp
|
||||
libs/yoga/yoga/YGLayout.cpp
|
||||
libs/yoga/yoga/YGNode.cpp
|
||||
libs/yoga/yoga/YGNodePrint.cpp
|
||||
libs/yoga/yoga/YGStyle.cpp
|
||||
libs/yoga/yoga/YGValue.cpp
|
||||
libs/yoga/yoga/Yoga.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_INCLUDE_DIRS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/base64"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/fmt/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/glad/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/glm"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/hash-library"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/jpeg"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/nanort"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/headers"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/poly2tri/poly2tri"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/sqlite3"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/stb"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/tinyxml2"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/wacom"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/yoga"
|
||||
)
|
||||
|
||||
17
cmake/PanoPainterVersion.cmake
Normal file
17
cmake/PanoPainterVersion.cmake
Normal file
@@ -0,0 +1,17 @@
|
||||
function(pp_add_version_generation target config_name)
|
||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/version.gen.h"
|
||||
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pre-build.py" "${config_name}"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pre-build.py"
|
||||
COMMENT "Generating src/version.gen.h"
|
||||
VERBATIM)
|
||||
|
||||
add_custom_target(pp_generate_version
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/version.gen.h")
|
||||
|
||||
add_dependencies(${target} pp_generate_version)
|
||||
endfunction()
|
||||
|
||||
40
cmake/PanoPainterWarnings.cmake
Normal file
40
cmake/PanoPainterWarnings.cmake
Normal file
@@ -0,0 +1,40 @@
|
||||
function(pp_configure_project_warnings target)
|
||||
if(MSVC)
|
||||
target_compile_options(${target} INTERFACE
|
||||
/W4
|
||||
/permissive-
|
||||
/Zc:__cplusplus
|
||||
/Zc:preprocessor)
|
||||
if(PP_ENABLE_MSVC_ANALYZE)
|
||||
target_compile_options(${target} INTERFACE /analyze)
|
||||
endif()
|
||||
else()
|
||||
target_compile_options(${target} INTERFACE
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
-Wconversion
|
||||
-Wshadow
|
||||
-Wnull-dereference)
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_ASAN)
|
||||
if(MSVC)
|
||||
target_compile_options(${target} INTERFACE /fsanitize=address)
|
||||
target_link_options(${target} INTERFACE /fsanitize=address)
|
||||
else()
|
||||
target_compile_options(${target} INTERFACE -fsanitize=address)
|
||||
target_link_options(${target} INTERFACE -fsanitize=address)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_UBSAN AND NOT MSVC)
|
||||
target_compile_options(${target} INTERFACE -fsanitize=undefined)
|
||||
target_link_options(${target} INTERFACE -fsanitize=undefined)
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_TSAN AND NOT MSVC)
|
||||
target_compile_options(${target} INTERFACE -fsanitize=thread)
|
||||
target_link_options(${target} INTERFACE -fsanitize=thread)
|
||||
endif()
|
||||
endfunction()
|
||||
75
cmake/ValidatePanoPainterShaders.cmake
Normal file
75
cmake/ValidatePanoPainterShaders.cmake
Normal file
@@ -0,0 +1,75 @@
|
||||
if(NOT DEFINED PP_SHADER_DIR)
|
||||
message(FATAL_ERROR "PP_SHADER_DIR is required")
|
||||
endif()
|
||||
|
||||
file(REAL_PATH "${PP_SHADER_DIR}" pp_shader_dir)
|
||||
if(NOT IS_DIRECTORY "${pp_shader_dir}")
|
||||
message(FATAL_ERROR "Shader directory does not exist: ${pp_shader_dir}")
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE pp_shader_files
|
||||
"${pp_shader_dir}/*.glsl")
|
||||
|
||||
if(NOT pp_shader_files)
|
||||
message(FATAL_ERROR "No shader files found under: ${pp_shader_dir}")
|
||||
endif()
|
||||
|
||||
set(pp_shader_errors "")
|
||||
set(pp_top_level_count 0)
|
||||
set(pp_include_count 0)
|
||||
|
||||
foreach(pp_shader_file IN LISTS pp_shader_files)
|
||||
file(RELATIVE_PATH pp_shader_rel "${pp_shader_dir}" "${pp_shader_file}")
|
||||
file(READ "${pp_shader_file}" pp_shader_contents)
|
||||
|
||||
string(REGEX MATCHALL "#[ \t]*include[ \t]+\"[^\"]+\"" pp_include_lines "${pp_shader_contents}")
|
||||
foreach(pp_include_line IN LISTS pp_include_lines)
|
||||
string(REGEX REPLACE ".*\"([^\"]+)\".*" "\\1" pp_include_path "${pp_include_line}")
|
||||
if(pp_include_path MATCHES "^/")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must be relative: ${pp_include_path}")
|
||||
endif()
|
||||
if(pp_include_path MATCHES "^[A-Za-z]:")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must not be drive-absolute: ${pp_include_path}")
|
||||
endif()
|
||||
if(pp_include_path MATCHES "\\.\\.")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must not traverse parent directories: ${pp_include_path}")
|
||||
endif()
|
||||
if(NOT EXISTS "${pp_shader_dir}/${pp_include_path}")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: missing include: ${pp_include_path}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(pp_shader_rel MATCHES "^include/")
|
||||
math(EXPR pp_include_count "${pp_include_count} + 1")
|
||||
if(pp_shader_contents MATCHES "\\[\\[(vertex|fragment)\\]\\]")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: include shaders must not declare stage markers")
|
||||
endif()
|
||||
else()
|
||||
math(EXPR pp_top_level_count "${pp_top_level_count} + 1")
|
||||
|
||||
string(REGEX MATCHALL "\\[\\[vertex\\]\\]" pp_vertex_markers "${pp_shader_contents}")
|
||||
string(REGEX MATCHALL "\\[\\[fragment\\]\\]" pp_fragment_markers "${pp_shader_contents}")
|
||||
list(LENGTH pp_vertex_markers pp_vertex_count)
|
||||
list(LENGTH pp_fragment_markers pp_fragment_count)
|
||||
|
||||
if(NOT pp_vertex_count EQUAL 1)
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: expected exactly one [[vertex]] marker")
|
||||
endif()
|
||||
if(NOT pp_fragment_count EQUAL 1)
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: expected exactly one [[fragment]] marker")
|
||||
endif()
|
||||
|
||||
string(FIND "${pp_shader_contents}" "[[vertex]]" pp_vertex_pos)
|
||||
string(FIND "${pp_shader_contents}" "[[fragment]]" pp_fragment_pos)
|
||||
if(pp_vertex_pos GREATER_EQUAL 0 AND pp_fragment_pos GREATER_EQUAL 0 AND NOT pp_vertex_pos LESS pp_fragment_pos)
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: [[vertex]] marker must appear before [[fragment]]")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(pp_shader_errors)
|
||||
list(JOIN pp_shader_errors "\n" pp_shader_error_text)
|
||||
message(FATAL_ERROR "Shader validation failed:\n${pp_shader_error_text}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Validated ${pp_top_level_count} shader programs and ${pp_include_count} shader includes under ${pp_shader_dir}")
|
||||
49
docs/adr/0001-modernization-boundaries.md
Normal file
49
docs/adr/0001-modernization-boundaries.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# ADR 0001: Incremental Component Boundaries
|
||||
|
||||
Status: accepted
|
||||
Date: 2026-05-31
|
||||
|
||||
## Context
|
||||
|
||||
PanoPainter currently has a flat `src/` layout with broad dependencies through
|
||||
`pch.h`, global singletons such as `App::I` and `Canvas::I`, OpenGL types in
|
||||
high-level painting/document headers, and duplicated platform source lists.
|
||||
The modernization work must retain existing behavior across Windows desktop
|
||||
and AppX, macOS, iOS, Android standard, Quest, Focus/Wave, Linux, and WebGL.
|
||||
|
||||
## Decision
|
||||
|
||||
Modernization will proceed incrementally. OpenGL remains the production
|
||||
renderer while component boundaries and tests are introduced. Vulkan, Metal,
|
||||
and WebGPU-related work must stay out of the production path until OpenGL
|
||||
parity tests exist.
|
||||
|
||||
The target dependency direction is:
|
||||
|
||||
```text
|
||||
pp_foundation
|
||||
-> pp_assets
|
||||
-> pp_paint
|
||||
-> pp_document
|
||||
-> pp_renderer_api
|
||||
-> pp_renderer_gl
|
||||
-> pp_paint_renderer
|
||||
-> pp_ui_core
|
||||
-> pp_panopainter_ui
|
||||
-> pp_platform_*
|
||||
-> panopainter_app
|
||||
```
|
||||
|
||||
Pure component headers must not include platform SDK headers or graphics API
|
||||
headers. Temporary shims are allowed only when recorded in
|
||||
`docs/modernization/debt.md`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The first implementation steps are documentation, inventory, CMake skeleton,
|
||||
diagnostics, and tests, not a renderer rewrite.
|
||||
- Existing project files remain until the shared CMake targets are validated.
|
||||
- Refactors should prefer additive compatibility layers before moving behavior.
|
||||
- Every extracted component must gain its own tests before the next component
|
||||
boundary is extracted.
|
||||
|
||||
136
docs/modernization/build-inventory.md
Normal file
136
docs/modernization/build-inventory.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Build And Platform Inventory
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-01
|
||||
|
||||
This inventory records the known build surfaces during the CMake migration.
|
||||
Keep it updated as platform paths move to shared CMake targets.
|
||||
|
||||
## Existing Build Entrypoints
|
||||
|
||||
| Platform/Target | Current Entrypoint | Notes |
|
||||
| --- | --- | --- |
|
||||
| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022 |
|
||||
| Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging |
|
||||
| macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today |
|
||||
| iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES today |
|
||||
| Android standard | `android/android/build.gradle`, `android/android/CMakeLists.txt` | Native library target `native-lib` |
|
||||
| Android Quest | `android/quest/build.gradle`, `android/quest/CMakeLists.txt` | OVR SDK imported libraries |
|
||||
| Android Focus/Wave | `android/focus/build.gradle`, `android/focus/CMakeLists.txt` | Wave SDK imported libraries |
|
||||
| Linux | `linux/CMakeLists.txt` | Old CMake 3.4, C++14 flag |
|
||||
| WebGL/Emscripten | `webgl/CMakeLists.txt` | Old CMake 3.4, WebGL2 flags |
|
||||
|
||||
## Existing Version Generation
|
||||
|
||||
- Script: `scripts/pre-build.py`
|
||||
- Output: `src/version.gen.h`
|
||||
- Current behavior: derives version from git branch, latest tag, short hash,
|
||||
commit count, and configuration argument.
|
||||
- Migration requirement: root CMake should call this script through a custom
|
||||
command and avoid unnecessary tracked-file churn where possible.
|
||||
|
||||
## Existing Dependency Sources
|
||||
|
||||
Hybrid policy: migrate reliable packages to vcpkg and retain SDK/patched
|
||||
dependencies until each platform triplet is proven.
|
||||
|
||||
| Dependency | Current Source | Initial Policy |
|
||||
| --- | --- | --- |
|
||||
| fmt | `libs/fmt` | Move to vcpkg |
|
||||
| GLM | `libs/glm` | Move to vcpkg |
|
||||
| tinyxml2 | `libs/tinyxml2` | Move to vcpkg |
|
||||
| stb | `libs/stb` | Move to vcpkg or interface target if package friction |
|
||||
| CURL | `libs/curl-win`, `libs/curl-android-ios` | Move to vcpkg where triplets work |
|
||||
| SQLite | `libs/sqlite3` | Move to vcpkg |
|
||||
| GLAD | `libs/glad` | Move to vcpkg or generated backend target |
|
||||
| Catch2 | none yet | Add through vcpkg |
|
||||
| OpenVR | `libs/openvr` | Retain initially |
|
||||
| OVR Platform/Mobile | `libs/ovr_platform`, `libs/ovr_mobile` | Retain initially |
|
||||
| Wave SDK | `libs/wave_sdk` | Retain initially |
|
||||
| Wacom WinTab | `libs/wacom` | Retain initially |
|
||||
| AppCenter Apple | `libs/appcenter-apple` | Retain initially |
|
||||
| openh264/mp4v2/libyuv | `libs/openh264`, `libs/mp4v2`, `libs/libyuv` | Retain initially |
|
||||
| jpeg helpers | `libs/jpeg` | Evaluate after image tests exist |
|
||||
| poly2tri/nanort/base64/hash-library | `libs/*` | Evaluate after component split |
|
||||
|
||||
## Current Validation Commands
|
||||
|
||||
These commands are the current local baseline.
|
||||
|
||||
```powershell
|
||||
cmake --preset windows-msvc-default
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
|
||||
ctest --preset desktop-fast --build-config Debug
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
|
||||
cmake --build --preset windows-msvc-default --target panopainter_validate_shaders
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\analyze.ps1 -Preset windows-msvc-default -NoApp
|
||||
$env:VCPKG_ROOT = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"
|
||||
cmake --preset windows-msvc-vcpkg-headless
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets windows-msvc-vcpkg-headless
|
||||
ctest --preset desktop-fast-vcpkg --build-config Debug
|
||||
cmake --preset android-arm64
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug
|
||||
cmake --fresh --preset windows-clangcl-asan
|
||||
```
|
||||
|
||||
Known local toolchain state:
|
||||
|
||||
- CMake: 4.0.0-rc4
|
||||
- Local Visual Studio generator selected by CMake: Visual Studio 17 2022
|
||||
- Bundled vcpkg: `C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg`
|
||||
(`vcpkg version` reports 2025-11-19)
|
||||
- Android SDK: `C:\Users\omara\AppData\Local\Android\Sdk`
|
||||
- Android NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865`
|
||||
- clang-cl: `C:\Program Files\LLVM\bin\clang-cl.exe` reports 18.1.8, but the
|
||||
selected VS 2026-preview STL expects Clang 20 or newer; see DEBT-0014 before
|
||||
treating `windows-clangcl-asan` as a passing sanitizer gate.
|
||||
- Android arm64 headless configure/build passes through root CMake and the
|
||||
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
|
||||
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_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.
|
||||
- `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.
|
||||
- `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.
|
||||
- `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`.
|
||||
- `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and
|
||||
is covered by `pano_cli_create_animation_document_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`.
|
||||
- `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.
|
||||
- `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.
|
||||
- `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON`
|
||||
through the vcpkg preset; default and Android validation still use the
|
||||
retained vendored fallback tracked by DEBT-0012.
|
||||
|
||||
Known warnings after the current CMake app build:
|
||||
|
||||
- Legacy code/vendor warnings under `/W4`.
|
||||
- Visual Studio vcpkg manifest warning because manifest mode is not enabled.
|
||||
- `LNK4099` missing `yuv.pdb` for retained libyuv binaries.
|
||||
- `LNK4098` runtime library conflict from retained vendor binaries.
|
||||
|
||||
Platform-specific commands should be added here when verified locally.
|
||||
83
docs/modernization/capability-map.md
Normal file
83
docs/modernization/capability-map.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# PanoPainter Capability Map
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-05-31
|
||||
|
||||
This map is the preservation checklist for the modernization. When a component
|
||||
is extracted, update the relevant rows with the owning component, test label,
|
||||
and validation command.
|
||||
|
||||
## Project And Documents
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture |
|
||||
| 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 |
|
||||
|
||||
## 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 |
|
||||
| Cube face export | `Canvas` | `pp_paint_renderer` | Six-face golden set |
|
||||
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |
|
||||
|
||||
## Brush And Painting
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Brush settings serialization | `Brush`, `Serializer` | `pp_paint`, `pp_assets` | Round-trip and boundary values |
|
||||
| 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 |
|
||||
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
|
||||
|
||||
## Layers And Animation
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Layer add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document` | Undo/redo invariant tests |
|
||||
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_paint_renderer` | CPU model and render golden |
|
||||
| Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases |
|
||||
| Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek |
|
||||
| MP4/timelapse export | `MP4Encoder`, recording thread | `pp_assets`, `pp_paint_renderer`, app | Smoke export and cancellation |
|
||||
|
||||
## UI And Workflow
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| XML layout parsing | `LayoutManager`, `Node` | `pp_ui_core` | Layout fixtures and malformed XML |
|
||||
| Yoga layout | `Node` | `pp_ui_core` | Deterministic geometry fixtures |
|
||||
| Generic controls | `NodeButton`, sliders, text, images | `pp_ui_core` | Event dispatch and layout tests |
|
||||
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui` | UI automation scripts |
|
||||
| Canvas viewport UI | `NodeCanvas` | `pp_panopainter_ui`, `pp_paint_renderer` | Input-to-command automation |
|
||||
| Settings UI | `Settings`, `NodeSettings` | `pp_assets`, `pp_panopainter_ui` | Round-trip settings |
|
||||
|
||||
## Input, Platform, And Devices
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Mouse/keyboard/touch/gestures | `App`, platform entrypoints | `pp_platform_*`, app | 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 |
|
||||
| OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests |
|
||||
| Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate |
|
||||
| Focus/Wave | Android Focus files | `pp_platform_android_wave` | Compile/package gate |
|
||||
|
||||
## Cloud, Logging, And Automation
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Upload/download/browse | `app_cloud`, CURL helpers | app service, `pp_platform_*` | 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 |
|
||||
|
||||
41
docs/modernization/debt.md
Normal file
41
docs/modernization/debt.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Modernization Debt Log
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-01
|
||||
|
||||
Every shortcut, temporary adapter, retained vendored dependency, skipped
|
||||
platform gate, compatibility shim, or incomplete automation path must be
|
||||
recorded here before it lands. Entries must be specific enough for a future
|
||||
agent or engineer to remove them without reconstructing context from chat.
|
||||
|
||||
## Entry Rules
|
||||
|
||||
- Add an entry before merging the shortcut.
|
||||
- Reference the debt id in code comments, TODOs, ADRs, or roadmap notes.
|
||||
- Include an owner, reason, validation command, and removal condition.
|
||||
- Do not close an entry until the removal condition is met and validated.
|
||||
- Prefer deleting shortcuts over expanding this log.
|
||||
|
||||
## Open Debt
|
||||
|
||||
| ID | Status | Owner | Item | Reason | Validation | Removal Condition |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets |
|
||||
| DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation |
|
||||
| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split | Avoid behavior changes while introducing component boundaries | App launch and component tests | Replace singleton reaches with context/service injection at component boundaries |
|
||||
| DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path |
|
||||
| DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated |
|
||||
| DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions |
|
||||
| DEBT-0008 | Open | Modernization | `windows-msvc-default` preset is used for local validation because the VS 2026 generator is not installed here | The target VS 2026 preset must remain, but this machine configures with Visual Studio 17 2022 | `cmake --preset windows-msvc-default`; `ctest --preset desktop-fast --build-config Debug` | Validate `windows-vs2026-x64` on a machine with Visual Studio 2026 installed and make it the default Windows validation preset |
|
||||
| DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands |
|
||||
| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, and renderer-free RGBA8 face payload storage, but it is not yet wired to legacy `Canvas`, selection masks, save, or legacy action commands | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pp_document_ppi_import_tests` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade |
|
||||
| DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact only, not AppX/APK/Apple/WebGL package outputs | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Package-smoke covers Windows AppX, Android APK variants, Apple bundles, and WebGL output where local toolchains are present |
|
||||
| DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception |
|
||||
| DEBT-0013 | Open | Modernization | `pp_assets`, `pano_cli inspect-project`, and `pano_cli load-project` validate the fixed PPI header, thumbnail/body byte layout, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, and decoded pixel attachment to `pp_document`, but full PPI save/round-trip fixtures are not yet extracted | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke` | Full PPI load/save fixtures cover thumbnail, decoded layer face payloads attached to documents, frames, metadata, corrupt payloads, and round-trip compatibility |
|
||||
| DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix |
|
||||
|
||||
## Closed Debt
|
||||
|
||||
| ID | Status | Owner | Item | Reason | Validation | Removal Condition |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| DEBT-0006 | Closed | Modernization | `pano_cli create-document` validates and emits JSON command contracts but does not yet invoke the legacy document/app model | The document model had not been extracted from `Canvas`/`App` yet | `ctest --preset desktop-fast --build-config Debug`; `pano_cli_create_document_smoke` | Closed on 2026-05-31: command now constructs a real `pp_document::CanvasDocument` |
|
||||
678
docs/modernization/roadmap.md
Normal file
678
docs/modernization/roadmap.md
Normal file
@@ -0,0 +1,678 @@
|
||||
# PanoPainter Modernization Roadmap
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-01
|
||||
|
||||
This is the living roadmap for modernizing PanoPainter into independently
|
||||
testable C++23 components while retaining all existing functionality. Keep this
|
||||
file current as phases are implemented. Do not let shortcuts, skipped platforms,
|
||||
or temporary adapters live only in chat history.
|
||||
|
||||
## How To Keep This Roadmap Live
|
||||
|
||||
- Update the phase status before and after each implementation pass.
|
||||
- When a shortcut is introduced, add it to the debt log section in this file
|
||||
until `docs/modernization/debt.md` exists, then move debt entries there.
|
||||
- When a major architectural decision is made, add an ADR under `docs/adr/`
|
||||
once that directory exists.
|
||||
- Every phase must preserve old behavior unless the roadmap explicitly says
|
||||
otherwise.
|
||||
- Each phase must leave the repo in a buildable and testable state.
|
||||
- Do not add stubs without a debt entry, validation command, and removal
|
||||
condition.
|
||||
|
||||
## Locked Decisions
|
||||
|
||||
- Graphics path: keep OpenGL working first; add Vulkan and Metal after the
|
||||
renderer boundary exists.
|
||||
- Required platforms at phase gates: Windows desktop/AppX, macOS, iOS,
|
||||
Android standard, Quest, Focus/Wave, Linux, and WebGL.
|
||||
- Dependency policy: use vcpkg where reliable; keep SDK, patched, or
|
||||
vendor-only dependencies with documented reasons.
|
||||
- Test stack: Catch2, golden/approval tests, and fuzz/property tests where
|
||||
useful.
|
||||
- Automation: local reproducible matrix first; hosted CI can be added later.
|
||||
- Documentation: ADRs, debt log, and this living roadmap.
|
||||
- "vkpkg" in older notes means `vcpkg`.
|
||||
- Target C++ standard: C++23.
|
||||
- Initial Windows CMake generator target: Visual Studio 2026 when available.
|
||||
|
||||
## Phase Status
|
||||
|
||||
| Phase | Name | Status | Gate |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 | Inventory, Safety Rails, And Memory | Complete | No behavior changes; old builds still work |
|
||||
| 1 | Unified CMake Skeleton | In progress | Root CMake builds the Windows app and owns the source list |
|
||||
| 2 | Toolchain, Diagnostics, And Dependencies | In progress | Strict desktop library builds compile cleanly |
|
||||
| 3 | Test Harness And Agent-Ready Automation | In progress | `ctest --preset desktop-fast` runs headlessly |
|
||||
| 4 | Component Split Without Behavior Change | Started | Each extracted target builds and tests |
|
||||
| 5 | Renderer Boundary And OpenGL Parity | Started | OpenGL output matches golden readbacks |
|
||||
| 6 | Platform Alignment | Not started | Every supported platform has named validation |
|
||||
| 7 | Hardening, Coverage, And Breaking-Point Tests | Not started | Each component has edge/failure tests |
|
||||
| 8 | Future Backend Readiness | Not started | Vulkan/Metal lab targets remain non-default |
|
||||
|
||||
## Target Component Architecture
|
||||
|
||||
The refactor should move toward one-way dependencies:
|
||||
|
||||
```text
|
||||
pp_foundation
|
||||
-> pp_assets
|
||||
-> pp_paint
|
||||
-> pp_document
|
||||
-> pp_renderer_api
|
||||
-> pp_renderer_gl
|
||||
-> pp_paint_renderer
|
||||
-> pp_ui_core
|
||||
-> pp_panopainter_ui
|
||||
-> pp_platform_*
|
||||
-> panopainter_app
|
||||
```
|
||||
|
||||
Intended responsibilities:
|
||||
|
||||
- `pp_foundation`: logging facade, math/util helpers, events, task queues,
|
||||
binary streams.
|
||||
- `pp_assets`: `Asset`, `Image`, `Settings`, serialization, ABR, PPBR, and PPI
|
||||
helpers.
|
||||
- `pp_paint`: pure `Brush`, `Stroke`, stroke sampling, and CPU reference blend
|
||||
math.
|
||||
- `pp_document`: canvas document model, layers, animation frames, and undo/redo
|
||||
model.
|
||||
- `pp_renderer_api`: renderer-neutral interfaces for textures, render targets,
|
||||
shaders, meshes, readback, frame capture, and tracing.
|
||||
- `pp_renderer_gl`: current OpenGL implementation behind renderer interfaces.
|
||||
- `pp_paint_renderer`: stroke rasterization, layer compositing, cube/equirect
|
||||
export using `pp_renderer_api`.
|
||||
- `pp_ui_core`: `Node`, layout, generic controls, text/image primitives.
|
||||
- `pp_panopainter_ui`: panels, dialogs, `NodeCanvas`, and app-specific
|
||||
workflows.
|
||||
- `pp_platform_*`: Windows, macOS/iOS, Android, Linux, and WebGL shells.
|
||||
- `panopainter_app`: composition root only.
|
||||
|
||||
Rules:
|
||||
|
||||
- Component headers must not include platform SDK or graphics API headers unless
|
||||
the component name includes that backend or platform.
|
||||
- Pure libraries must build and test without a window, GL context, network,
|
||||
tablet, VR headset, or filesystem outside test temp directories.
|
||||
- Public APIs should return explicit status/result objects. PanoPainter app
|
||||
code keeps exceptions disabled unless isolated SDK wrappers require them.
|
||||
- Singleton access should be replaced at component boundaries with context or
|
||||
service objects. Temporary facade shims require debt entries.
|
||||
|
||||
## Phase 0: Inventory, Safety Rails, And Memory
|
||||
|
||||
Status: complete on 2026-05-31. Created this roadmap,
|
||||
`docs/modernization/debt.md`, `docs/modernization/capability-map.md`,
|
||||
`docs/modernization/build-inventory.md`, and ADR 0001.
|
||||
|
||||
Goal: create durable project memory and prevent silent shortcuts before large
|
||||
refactors begin.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
- Add `docs/modernization/roadmap.md`, `docs/modernization/debt.md`, and
|
||||
`docs/adr/`.
|
||||
- Add a shortcut rule: every temporary adapter, fallback, skipped platform, or
|
||||
retained vendored dependency must have owner, reason, validation command, and
|
||||
removal condition.
|
||||
- Generate a current capability map covering:
|
||||
- project open/save and PPI compatibility
|
||||
- image import/export and thumbnails
|
||||
- brush presets, ABR import, PPBR export/import
|
||||
- layers, blend modes, alpha lock, selection mask
|
||||
- animation frames and MP4/timelapse recording
|
||||
- VR, tablet, touch, mouse, keyboard, gestures
|
||||
- cloud upload/download/browse
|
||||
- UI dialogs, panels, layout XML, settings
|
||||
- Windows/AppX, macOS, iOS, Android standard, Quest, Focus/Wave, Linux, WebGL
|
||||
- Record current build commands and known platform prerequisites.
|
||||
|
||||
Gate:
|
||||
|
||||
- No behavior changes.
|
||||
- Existing Visual Studio, platform CMake, Gradle, Apple, Linux, and WebGL paths
|
||||
are not removed.
|
||||
|
||||
## Phase 1: Unified CMake Skeleton
|
||||
|
||||
Goal: make CMake the canonical source list without breaking existing projects.
|
||||
|
||||
Status: in progress. Root `CMakeLists.txt`, `CMakePresets.json`, and project
|
||||
option targets exist. The Windows desktop app builds through CMake as
|
||||
`PanoPainter`; the raw Visual Studio solution/project files were removed on
|
||||
2026-05-31 by user decision. Android arm64 now configures and builds headless
|
||||
foundation/tool targets through the root CMake/NDK path. Non-Windows platform
|
||||
app/package files remain during Phase 6 alignment.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
- Add root `CMakeLists.txt` and shared CMake modules under `cmake/`.
|
||||
- Add `CMakePresets.json` with at least:
|
||||
- `windows-vs2026-x64`
|
||||
- `windows-clangcl-asan`
|
||||
- `linux-clang`
|
||||
- `android-arm64`
|
||||
- `android-x64`
|
||||
- `emscripten`
|
||||
- `macos`
|
||||
- `ios-device`
|
||||
- `ios-simulator`
|
||||
- Keep Android CMake, Linux CMake, WebGL CMake, Apple project files, and AppX
|
||||
packaging during the transition until each consumes shared component targets.
|
||||
- Move version generation into a CMake custom command using
|
||||
`scripts/pre-build.py`.
|
||||
- Fix `scripts/pre-build.py` only if required to avoid unnecessary rewrites or
|
||||
missing-tag failures.
|
||||
- Add CMake options:
|
||||
- `PP_BUILD_APP`
|
||||
- `PP_BUILD_TESTS`
|
||||
- `PP_BUILD_TOOLS`
|
||||
- `PP_ENABLE_OPENGL`
|
||||
- `PP_ENABLE_VULKAN_EXPERIMENTAL=OFF`
|
||||
- `PP_ENABLE_VR`
|
||||
- `PP_ENABLE_CLOUD`
|
||||
- `PP_ENABLE_VIDEO`
|
||||
- Define source-list helper targets so per-platform source duplication can be
|
||||
reduced incrementally.
|
||||
|
||||
Gate:
|
||||
|
||||
- Windows desktop app builds through CMake.
|
||||
- New CMake can configure on Windows.
|
||||
- Source list differences are understood and documented.
|
||||
- Non-Windows platform migration is debt-tracked until Phase 6.
|
||||
|
||||
## Phase 2: Toolchain, Diagnostics, And Dependencies
|
||||
|
||||
Goal: turn the build into an error-finding system before deep refactors.
|
||||
|
||||
Status: in progress. Initial warning/sanitizer option targets, `vcpkg.json`,
|
||||
a validated Windows headless vcpkg preset, `pp_ui_core` support for vcpkg
|
||||
tinyxml2 on that preset, and a headless `panopainter_validate_shaders` target
|
||||
exist. `windows-clangcl-asan` now configures as a headless Ninja/clang-cl ASan
|
||||
preset and uses the release MSVC runtime required by clang-cl ASan, but local
|
||||
ASan builds are blocked by DEBT-0014 until Clang and the selected MSVC STL are
|
||||
compatible. Dependency migration is not complete until remaining component
|
||||
dependencies and mobile/Apple triplets are validated.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
- Set C++23 through target features, not raw compiler flags.
|
||||
- Add warning profiles:
|
||||
- MSVC: `/W4 /permissive- /Zc:__cplusplus /Zc:preprocessor`.
|
||||
- Optional MSVC analysis preset: `/analyze`.
|
||||
- Clang/GCC: `-Wall -Wextra -Wpedantic -Wconversion -Wshadow
|
||||
-Wnull-dereference`.
|
||||
- Keep exceptions disabled for PanoPainter targets, except isolated SDK wrapper
|
||||
targets when unavoidable.
|
||||
- Add sanitizer presets:
|
||||
- Clang/GCC ASan and UBSan for headless libraries.
|
||||
- MSVC ASan where supported.
|
||||
- TSan only for pure/headless targets.
|
||||
- Add tooling hooks:
|
||||
- `clang-tidy`
|
||||
- `cppcheck`
|
||||
- shader validation or compile checks
|
||||
- CTest dashboard output
|
||||
- Add `vcpkg.json`.
|
||||
- Move reliable dependencies to vcpkg first:
|
||||
- `fmt`
|
||||
- `glm`
|
||||
- `tinyxml2`
|
||||
- `stb`
|
||||
- `curl`
|
||||
- `sqlite3`
|
||||
- `glad`
|
||||
- `Catch2`
|
||||
- Keep vendored until proven:
|
||||
- OpenVR
|
||||
- OVR/Wave SDKs
|
||||
- Wacom WinTab
|
||||
- AppCenter
|
||||
- openh264
|
||||
- mp4v2
|
||||
- libyuv
|
||||
- patched or SDK-specific libraries
|
||||
|
||||
Gate:
|
||||
|
||||
- Desktop library targets compile with strict diagnostics.
|
||||
- New warnings caused by refactor are fixed or locally justified.
|
||||
- No global blanket warning suppression for project code.
|
||||
|
||||
## Phase 3: Test Harness And Agent-Ready Automation
|
||||
|
||||
Goal: make each component reachable by automated tools and future agents.
|
||||
|
||||
Status: in progress. `tests/` exists, `desktop-fast` runs headlessly, and
|
||||
PowerShell/bash wrappers exist for
|
||||
configure/build/test/analyze/platform-build/package-smoke. `pano_cli` exists
|
||||
with JSON automation commands for creating a `pp_document` model, metadata-only
|
||||
PPI project loading, and inspecting image signatures, PPI headers, and layout
|
||||
XML; full document/app integration is debt-tracked as DEBT-0010 and full PPI
|
||||
body parsing is debt-tracked as DEBT-0013.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
- Add `tests/` with one executable per component.
|
||||
- Register CTest labels:
|
||||
- `foundation`
|
||||
- `assets`
|
||||
- `paint`
|
||||
- `document`
|
||||
- `renderer`
|
||||
- `ui`
|
||||
- `platform`
|
||||
- `integration`
|
||||
- `fuzz`
|
||||
- `slow`
|
||||
- `gpu`
|
||||
- Add `tools/pano_cli` for headless automation.
|
||||
- `pano_cli` should support:
|
||||
- create document
|
||||
- load project
|
||||
- save project
|
||||
- apply scripted strokes
|
||||
- import/export images
|
||||
- inspect layers
|
||||
- run layout parse
|
||||
- emit JSON results
|
||||
- Add local automation wrappers under `scripts/automation/`:
|
||||
- configure
|
||||
- build
|
||||
- test
|
||||
- analyze
|
||||
- package smoke
|
||||
- All wrappers must return machine-readable logs or summaries.
|
||||
- Establish `tests/data/` fixtures:
|
||||
- tiny PPI files
|
||||
- corrupt/truncated PPI cases
|
||||
- PNG/JPEG fixtures
|
||||
- ABR/PPBR samples
|
||||
- layout XML
|
||||
- shader snippets
|
||||
- brush stroke scripts
|
||||
|
||||
Gate:
|
||||
|
||||
- `ctest --preset desktop-fast --build-config Debug` runs without a GL
|
||||
context.
|
||||
- Non-render components can be tested on a headless machine.
|
||||
|
||||
## Phase 4: Component Split Without Behavior Change
|
||||
|
||||
Goal: split libraries while keeping current app behavior.
|
||||
|
||||
Status: started. `pp_foundation` exists with binary stream utilities and
|
||||
boundary/overread tests. It also owns strict decimal `uint32` parsing used by
|
||||
`pano_cli`, with rejection tests for empty, signed, mixed, and overflowing
|
||||
input. A synchronous event dispatcher, structured logging facade, bounded FIFO
|
||||
task queue, and deterministic `TraceRecorder` now record
|
||||
component/name/thread/frame/stroke metadata with filtering, capacity, and
|
||||
invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection,
|
||||
PNG IHDR metadata parsing, PPI header/project byte-layout/body-summary
|
||||
recognition, layer/frame indexing, dirty-face PNG payload metadata validation,
|
||||
asset-level RGBA PNG payload decoding, and a pure typed settings document
|
||||
model, with
|
||||
corrupt/truncated/unsupported, extreme-dimension, and key/value limit tests.
|
||||
`pp_paint` has started with pure brush parameter validation/stamp evaluation,
|
||||
CPU reference math for the five current shader blend modes, and deterministic
|
||||
stroke spacing/interpolation plus a pure text stroke-script parser.
|
||||
`pp_document` has
|
||||
started with a pure canvas/layer/frame model, alpha-lock metadata, snapshot
|
||||
construction, per-layer frame metadata, layer metadata operations, frame
|
||||
move/duration queries, renderer-free RGBA8 cube-face payload storage, PPI image
|
||||
import, and layer/frame/undo-redo history invariant tests.
|
||||
`pp_renderer_api` has started with renderer-neutral
|
||||
texture/readback descriptors and validation tests. `pp_paint_renderer` has
|
||||
started with deterministic CPU layer compositing over renderer extents using
|
||||
the paint blend reference. `pp_ui_core` has started with XML-layout-facing
|
||||
length parsing, color parsing, tinyxml-backed layout XML parsing, and invalid
|
||||
input tests.
|
||||
`pano_cli inspect-image` exposes PNG IHDR metadata as JSON,
|
||||
`pano_cli inspect-project` reports validated PPI thumbnail/body byte layout,
|
||||
body summary, layer/frame descriptors, dirty-face PNG payload metadata, and
|
||||
asset-level decode coverage, and
|
||||
`pano_cli load-project` creates a `pp_document` projection with per-layer frame
|
||||
counts, durations, and decoded face-pixel payload attachment when PPI image
|
||||
payloads are present.
|
||||
`pano_cli create-document` can create simple animation documents with explicit
|
||||
frame count/duration, and `pano_cli simulate-stroke` exercises the pure stroke
|
||||
sampler for scripted-stroke automation. `pano_cli simulate-stroke-script`
|
||||
loads stroke script fixtures, parses them through `pp_paint`, and samples every
|
||||
stroke. `pano_cli parse-layout` exercises the XML layout path. Continue
|
||||
expanding document behavior toward legacy Canvas parity and then port OpenGL
|
||||
classes behind the renderer boundary.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
- Extract components in this order:
|
||||
1. `pp_foundation`
|
||||
2. `pp_assets`
|
||||
3. `pp_paint`
|
||||
4. `pp_document`
|
||||
5. `pp_renderer_api`
|
||||
6. `pp_renderer_gl`
|
||||
7. `pp_paint_renderer`
|
||||
8. `pp_ui_core`
|
||||
9. `pp_panopainter_ui`
|
||||
10. `pp_platform_*`
|
||||
11. `panopainter_app`
|
||||
- Remove renderer/platform dependencies from pure headers first, especially:
|
||||
- `Brush`
|
||||
- document/layer model
|
||||
- serializer
|
||||
- UI core headers
|
||||
- Keep facade shims where needed, but debt-track every shim.
|
||||
- Avoid large behavioral rewrites during extraction.
|
||||
- Each extracted component gets a focused test suite before moving to the next.
|
||||
|
||||
Gate:
|
||||
|
||||
- Old app still launches.
|
||||
- Component tests pass after every extraction.
|
||||
- No undocumented stubs or shortcuts.
|
||||
|
||||
## Phase 5: Renderer Boundary And OpenGL Parity
|
||||
|
||||
Goal: make OpenGL an implementation detail and establish parity tests before
|
||||
adding new backends.
|
||||
|
||||
Status: started. `pp_renderer_api` exists as a headless renderer-neutral target
|
||||
with texture descriptor, byte-size, viewport, mesh, readback bounds, command
|
||||
context, render device, shader program descriptor, mesh, render target,
|
||||
readback, and trace interface validation. OpenGL classes are not yet behind
|
||||
these interfaces.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
- Introduce renderer interfaces:
|
||||
- `IRenderDevice`
|
||||
- `ITexture2D`
|
||||
- `IRenderTarget`
|
||||
- `IShaderProgram`
|
||||
- `IMesh`
|
||||
- `ICommandContext`
|
||||
- `IReadbackBuffer`
|
||||
- `IRenderTrace`
|
||||
- Port current renderer classes behind OpenGL backend types:
|
||||
- `RTT`
|
||||
- `Texture2D`
|
||||
- `Sampler`
|
||||
- `ShaderManager`
|
||||
- `Shape`
|
||||
- Preserve current shader behavior and asset paths.
|
||||
- Add deterministic GPU tests:
|
||||
- clear
|
||||
- blit
|
||||
- texture upload/download
|
||||
- stroke composite
|
||||
- erase
|
||||
- layer blend
|
||||
- equirect export
|
||||
- readback bounds
|
||||
- Add CPU reference tests for blend modes.
|
||||
- Compare GPU output to golden/reference data with explicit tolerances.
|
||||
|
||||
Gate:
|
||||
|
||||
- OpenGL readbacks match golden data on Windows and Linux.
|
||||
- Mobile/WebGL compile gates remain green.
|
||||
|
||||
## Phase 6: Platform Alignment
|
||||
|
||||
Goal: every supported platform consumes the same component targets.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
- Convert these builds to shared component targets:
|
||||
- Windows desktop
|
||||
- Windows AppX
|
||||
- macOS
|
||||
- iOS
|
||||
- Android standard
|
||||
- Android Quest
|
||||
- Android Focus/Wave
|
||||
- Linux
|
||||
- WebGL/Emscripten
|
||||
- Keep platform entrypoints thin:
|
||||
- window lifecycle
|
||||
- input dispatch
|
||||
- clipboard
|
||||
- file picker/share
|
||||
- GL context creation
|
||||
- VR SDK bridge
|
||||
- packaging only
|
||||
- Add or refine CMake toolchain/preset support for:
|
||||
- Android NDK ABIs
|
||||
- iOS device
|
||||
- iOS simulator
|
||||
- macOS
|
||||
- Emscripten
|
||||
- Keep SDK-only imported libraries documented until vcpkg triplets are proven.
|
||||
|
||||
Gate:
|
||||
|
||||
- Every platform has a named configure/build command.
|
||||
- Missing local prerequisites are documented.
|
||||
- Each platform has at least compile or package validation.
|
||||
|
||||
## Phase 7: Hardening, Coverage, And Breaking-Point Tests
|
||||
|
||||
Goal: tests should try to break components, not only confirm current happy
|
||||
paths.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
- Add property/fuzz tests for:
|
||||
- binary streams
|
||||
- serializers
|
||||
- PPI parsing
|
||||
- ABR parsing
|
||||
- layout XML parsing
|
||||
- image metadata parsing
|
||||
- brush parameter extremes
|
||||
- layer/frame operations
|
||||
- undo/redo invariants
|
||||
- Add stress tests for:
|
||||
- thousands of stroke samples
|
||||
- extreme resolutions guarded by memory limits
|
||||
- rapid layer/frame edits
|
||||
- corrupt assets
|
||||
- cancellation during export
|
||||
- concurrent render/UI task scheduling
|
||||
- Add coverage for headless libraries on Clang/GCC.
|
||||
- Require coverage reports for changed components first; do not set a global
|
||||
threshold until the baseline is meaningful.
|
||||
- Add tracing spans around:
|
||||
- project load/save
|
||||
- render passes
|
||||
- stroke commit
|
||||
- readback
|
||||
- export
|
||||
- UI layout
|
||||
- platform I/O
|
||||
- Logs must include component, thread, frame/stroke id, and timing.
|
||||
|
||||
Gate:
|
||||
|
||||
- No shortcut remains undocumented.
|
||||
- Every component has unit tests and at least one failure or edge test.
|
||||
|
||||
## Phase 8: Future Backend Readiness
|
||||
|
||||
Goal: prepare Vulkan and Metal without destabilizing the OpenGL parity path.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
- Create non-default targets only after OpenGL backend parity:
|
||||
- `pp_renderer_vulkan_lab`
|
||||
- `pp_renderer_metal_lab`
|
||||
- Use `D:\Dev\vkpaint` as reference material for Vulkan painting experiments,
|
||||
not as direct production code.
|
||||
- Before integration, prove:
|
||||
- ping-pong compositing path
|
||||
- input-attachment/subpass path where applicable
|
||||
- feedback-loop or framebuffer-fetch-style path where supported
|
||||
- synchronization and layout correctness under validation layers
|
||||
- Keep WebGPU as an optional future portability backend, not the core renderer
|
||||
contract.
|
||||
|
||||
Gate:
|
||||
|
||||
- Vulkan/Metal lab targets are opt-in.
|
||||
- OpenGL production backend remains stable.
|
||||
|
||||
## Test Matrix
|
||||
|
||||
| Preset/Label | Purpose | Requires |
|
||||
| --- | --- | --- |
|
||||
| `desktop-fast` | Pure component unit tests | No GPU/window |
|
||||
| `desktop-gpu` | OpenGL backend golden/readback tests | GPU/GL context |
|
||||
| `fuzz` | Parser and serializer fuzzing | Fuzzer-capable compiler |
|
||||
| `stress` | Large and adversarial scenarios | Longer runtime |
|
||||
| `platform-build` | Configure/build each supported platform | Local toolchains |
|
||||
| `package-smoke` | AppX/APK/Apple/WebGL package smoke | Platform SDKs |
|
||||
|
||||
Acceptance for each phase:
|
||||
|
||||
- Previous phase tests still pass.
|
||||
- New component has its own tests.
|
||||
- No undocumented stubs.
|
||||
- No skipped platform without a debt entry.
|
||||
- Automation command is recorded in this roadmap or linked docs.
|
||||
|
||||
## Verified Commands
|
||||
|
||||
Last verified on 2026-06-01:
|
||||
|
||||
```powershell
|
||||
cmake --preset windows-msvc-default
|
||||
cmake --build --preset windows-msvc-default --config Debug --target 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_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 pano_cli PanoPainter
|
||||
ctest --preset desktop-fast --build-config Debug
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
|
||||
cmake --build --preset windows-msvc-default --target panopainter_validate_shaders
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\analyze.ps1 -Preset windows-msvc-default -NoApp
|
||||
set VCPKG_ROOT=C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg
|
||||
cmake --preset windows-msvc-vcpkg-headless
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets windows-msvc-vcpkg-headless
|
||||
ctest --preset desktop-fast-vcpkg --build-config Debug
|
||||
cmake --preset android-arm64
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug
|
||||
cmake --fresh --preset windows-clangcl-asan
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
- `pp_foundation_binary_stream_tests` passed.
|
||||
- `pp_foundation_event_tests` passed.
|
||||
- `pp_foundation_log_tests` passed.
|
||||
- `pp_foundation_parse_tests` passed.
|
||||
- `pp_foundation_task_queue_tests` passed.
|
||||
- `pp_foundation_trace_tests` passed.
|
||||
- `pp_assets_image_format_tests` passed.
|
||||
- `pp_assets_image_metadata_tests` passed.
|
||||
- `pp_assets_image_pixels_tests` passed, including RGBA8 PNG decode and corrupt
|
||||
payload rejection.
|
||||
- `pp_assets_ppi_header_tests` passed, including PPI thumbnail/body layout,
|
||||
body summary validation, layer/frame indexing, dirty-face PNG payload
|
||||
metadata validation, and decoded dirty-face payload coverage.
|
||||
- `pp_assets_settings_document_tests` passed.
|
||||
- `pp_paint_brush_tests` passed.
|
||||
- `pp_paint_blend_tests` passed.
|
||||
- `pp_paint_stroke_tests` passed.
|
||||
- `pp_paint_stroke_script_tests` passed.
|
||||
- `pp_document_tests` passed, including snapshot construction, alpha-lock
|
||||
metadata, per-layer frame metadata, frame move, duration, face-pixel payload
|
||||
storage/replacement/rejection, and history invariants.
|
||||
- `pp_document_ppi_import_tests` passed, including decoded PPI dirty-face
|
||||
payload attachment to `pp_document` layer/frame storage and out-of-range
|
||||
payload rejection.
|
||||
- `pp_renderer_api_tests` passed, including shader descriptor validation.
|
||||
- `pp_paint_renderer_compositor_tests` passed.
|
||||
- `pp_ui_core_color_tests` passed.
|
||||
- `pp_ui_core_layout_value_tests` passed.
|
||||
- `pp_ui_core_layout_xml_tests` passed.
|
||||
- `pano_cli_create_document_smoke` passed.
|
||||
- `pano_cli_create_animation_document_smoke` passed and reports animation
|
||||
duration JSON.
|
||||
- `pano_cli_inspect_image_rejects_unsupported` passed as an expected failure
|
||||
test.
|
||||
- `pano_cli_inspect_png_metadata_smoke` passed and reports PNG metadata JSON
|
||||
for the tiny IHDR fixture.
|
||||
- `pano_cli_inspect_project_layout_smoke` passed and reports PPI
|
||||
thumbnail/body byte layout, body summary, layer/frame descriptors, and
|
||||
dirty-face PNG payload metadata JSON.
|
||||
- `pano_cli_load_project_metadata_smoke` passed and reports a `pp_document`
|
||||
projection with per-layer frame counts, durations, and zero loaded face
|
||||
payloads for the minimal PPI fixture.
|
||||
- `pano_cli_parse_layout_smoke` passed.
|
||||
- `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke
|
||||
sample counts/distances.
|
||||
- `pano_cli_simulate_stroke_script_smoke` passed and reports deterministic
|
||||
aggregate stroke-script counts/distances.
|
||||
- `panopainter_validate_shaders` passed, validating 25 shader programs and 7
|
||||
shader includes for stage markers and include graph integrity.
|
||||
- PowerShell analyze automation returns JSON summaries and includes the shader
|
||||
validation target.
|
||||
- `windows-msvc-vcpkg-headless` configured through the Visual Studio bundled
|
||||
vcpkg root, installed the manifest dependencies, built the headless component
|
||||
matrix, and passed `desktop-fast-vcpkg`.
|
||||
- `pp_ui_core` built and tested against vcpkg tinyxml2 on
|
||||
`windows-msvc-vcpkg-headless` and against the vendored fallback on
|
||||
`windows-msvc-default` and `android-arm64`.
|
||||
- `windows-clangcl-asan` configures headlessly with clang-cl 18.1.8 and
|
||||
release MSVC runtime selection; build remains blocked and debt-tracked in
|
||||
DEBT-0014 because the selected VS 2026-preview STL requires Clang 20 or
|
||||
newer.
|
||||
- `PanoPainter.exe` built through CMake at
|
||||
`out/build/windows-msvc-default/Debug/PanoPainter.exe`.
|
||||
- PowerShell build/test automation wrappers return JSON summaries and passed
|
||||
local smoke checks.
|
||||
- PowerShell package-smoke wrapper validates the Windows CMake app executable
|
||||
and runtime `data/` copy.
|
||||
- Android arm64 configured with NDK 29.0.14206865 through the platform-build
|
||||
wrapper and compiled headless foundation/tool/test targets.
|
||||
- Known remaining warnings: legacy project/vendor diagnostics, Visual Studio
|
||||
vcpkg-manifest warning, `LNK4099` missing libyuv PDBs, and `LNK4098` runtime
|
||||
library conflict from retained vendor binaries.
|
||||
|
||||
## Current Debt Log
|
||||
|
||||
The canonical debt log is now `docs/modernization/debt.md`. Keep this section
|
||||
as a reminder only; do not add new debt entries here.
|
||||
|
||||
| ID | Status | Owner | Item | Reason | Validation | Removal Condition |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| DEBT-0001 | Open | TBD | Existing platform build files remain alongside new CMake | Required for incremental migration | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets |
|
||||
| DEBT-0002 | Open | TBD | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace or document permanent vendored status after vcpkg triplet evaluation |
|
||||
| DEBT-0003 | Open | TBD | Existing singletons remain during initial split | Avoid behavior changes while introducing boundaries | App launch and component tests | Replace singleton reaches with context/service injection at component boundaries |
|
||||
|
||||
## Current Capability Map Seed
|
||||
|
||||
Use this as the starting checklist for Phase 0 inventory.
|
||||
|
||||
- Project I/O: PPI open/save, thumbnails, version metadata, autosave/save-as
|
||||
flows.
|
||||
- Image I/O: JPEG/PNG import/export, cube faces, equirectangular export,
|
||||
depth export.
|
||||
- Brush system: ABR import, PPBR import/export, presets, tip/pattern/dual brush,
|
||||
pressure, jitter, blend modes.
|
||||
- Painting: six cube faces, temporary stroke buffers, erase, flood fill, masks,
|
||||
alpha lock, layer compositing.
|
||||
- Layers and animation: layer add/remove/move/merge, blend/opacity/visibility,
|
||||
frame add/remove/duplicate/duration, MP4/timelapse export.
|
||||
- UI: XML layout, Yoga layout, panels, dialogs, color tools, brush tools,
|
||||
layers, animation timeline, settings, shortcuts, manual/changelog/about.
|
||||
- Input: mouse, keyboard, touch, gestures, Wacom tablet, stylus pressure,
|
||||
VR controllers.
|
||||
- Platform services: clipboard, file picker, save picker, directory picker,
|
||||
share/display file, keyboard show/hide.
|
||||
- VR/platform variants: OpenVR desktop, Quest, Focus/Wave, Android standard,
|
||||
iOS/macOS, Linux, WebGL.
|
||||
- Cloud/network: upload, download, browse, license/check flows.
|
||||
- Recording/export: PBO readbacks, MP4 encoder, timelapse frames.
|
||||
@@ -62,7 +62,7 @@ BOOL LoadWintab( void )
|
||||
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wacom_Tablet.dll" );
|
||||
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wintab32.dll" );
|
||||
LOG("calling LoadLibrary");
|
||||
ghWintab = LoadLibrary(L"Wintab32.dll");
|
||||
ghWintab = LoadLibraryW(L"Wintab32.dll");
|
||||
LOG("LoadLibrary called");
|
||||
|
||||
if ( !ghWintab )
|
||||
|
||||
52
scripts/automation/analyze.ps1
Normal file
52
scripts/automation/analyze.ps1
Normal file
@@ -0,0 +1,52 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[switch]$NoApp
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
$argsList = @(
|
||||
"--preset", $Preset,
|
||||
"-DPP_ENABLE_MSVC_ANALYZE=ON",
|
||||
"-DPP_ENABLE_CLANG_TIDY=ON",
|
||||
"-DPP_ENABLE_CPPCHECK=ON"
|
||||
)
|
||||
if ($NoApp) {
|
||||
$argsList += "-DPP_BUILD_APP=OFF"
|
||||
}
|
||||
|
||||
& cmake @argsList
|
||||
$configureExitCode = $LASTEXITCODE
|
||||
$shaderExitCode = 0
|
||||
|
||||
if ($configureExitCode -eq 0) {
|
||||
& cmake --build --preset $Preset --target panopainter_validate_shaders
|
||||
$shaderExitCode = $LASTEXITCODE
|
||||
}
|
||||
|
||||
$exitCode = $configureExitCode
|
||||
if ($exitCode -eq 0 -and $shaderExitCode -ne 0) {
|
||||
$exitCode = $shaderExitCode
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "analyze"
|
||||
preset = $Preset
|
||||
exitCode = $exitCode
|
||||
checks = @(
|
||||
[ordered]@{
|
||||
name = "configure"
|
||||
exitCode = $configureExitCode
|
||||
},
|
||||
[ordered]@{
|
||||
name = "shader-validation"
|
||||
exitCode = $shaderExitCode
|
||||
}
|
||||
)
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress -Depth 4
|
||||
|
||||
exit $exitCode
|
||||
20
scripts/automation/analyze.sh
Normal file
20
scripts/automation/analyze.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
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"
|
||||
if [ "$configure_exit_code" -eq 0 ]; then
|
||||
cmake --build --preset "$preset" --target panopainter_validate_shaders
|
||||
shader_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
|
||||
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"
|
||||
exit "$exit_code"
|
||||
28
scripts/automation/build.ps1
Normal file
28
scripts/automation/build.ps1
Normal file
@@ -0,0 +1,28 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[string]$Configuration = "Debug",
|
||||
[string]$Target = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
$argsList = @("--build", "--preset", $Preset, "--config", $Configuration)
|
||||
if ($Target.Length -gt 0) {
|
||||
$argsList += @("--target", $Target)
|
||||
}
|
||||
|
||||
& cmake @argsList
|
||||
$exitCode = $LASTEXITCODE
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "build"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
exit $exitCode
|
||||
17
scripts/automation/build.sh
Normal file
17
scripts/automation/build.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-linux-clang}"
|
||||
configuration="${2:-Debug}"
|
||||
target="${3:-}"
|
||||
start="$(date +%s)"
|
||||
if [ -n "$target" ]; then
|
||||
cmake --build --preset "$preset" --config "$configuration" --target "$target"
|
||||
else
|
||||
cmake --build --preset "$preset" --config "$configuration"
|
||||
fi
|
||||
exit_code="$?"
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"build","preset":"%s","configuration":"%s","target":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$exit_code" "$elapsed_ms"
|
||||
exit "$exit_code"
|
||||
25
scripts/automation/configure.ps1
Normal file
25
scripts/automation/configure.ps1
Normal file
@@ -0,0 +1,25 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[switch]$NoApp
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
$argsList = @("--preset", $Preset)
|
||||
if ($NoApp) {
|
||||
$argsList += "-DPP_BUILD_APP=OFF"
|
||||
}
|
||||
|
||||
& cmake @argsList
|
||||
$exitCode = $LASTEXITCODE
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "configure"
|
||||
preset = $Preset
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
exit $exitCode
|
||||
11
scripts/automation/configure.sh
Normal file
11
scripts/automation/configure.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-linux-clang}"
|
||||
start="$(date +%s)"
|
||||
cmake --preset "$preset"
|
||||
exit_code="$?"
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"configure","preset":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$exit_code" "$elapsed_ms"
|
||||
exit "$exit_code"
|
||||
48
scripts/automation/package-smoke.ps1
Normal file
48
scripts/automation/package-smoke.ps1
Normal file
@@ -0,0 +1,48 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[string]$Configuration = "Debug",
|
||||
[string]$Target = "PanoPainter"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
|
||||
& cmake --build --preset $Preset --config $Configuration --target $Target
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
if ($buildExitCode -ne 0) {
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
command = "package-smoke"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
stage = "build"
|
||||
exitCode = $buildExitCode
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress
|
||||
exit $buildExitCode
|
||||
}
|
||||
|
||||
$binaryDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "$Target.exe"
|
||||
$dataDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "data"
|
||||
$checks = @(
|
||||
[ordered]@{ name = "executable"; path = $binaryDir; exists = Test-Path -LiteralPath $binaryDir -PathType Leaf },
|
||||
[ordered]@{ name = "data"; path = $dataDir; exists = Test-Path -LiteralPath $dataDir -PathType Container }
|
||||
)
|
||||
|
||||
$failed = @($checks | Where-Object { -not $_.exists })
|
||||
$exitCode = if ($failed.Count -eq 0) { 0 } else { 2 }
|
||||
$elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "package-smoke"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsedMs
|
||||
checks = $checks
|
||||
} | ConvertTo-Json -Compress -Depth 5
|
||||
|
||||
exit $exitCode
|
||||
28
scripts/automation/package-smoke.sh
Normal file
28
scripts/automation/package-smoke.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-linux-clang}"
|
||||
configuration="${2:-Debug}"
|
||||
target="${3:-PanoPainter}"
|
||||
artifact="${4:-out/build/$preset/$target}"
|
||||
start="$(date +%s)"
|
||||
|
||||
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"
|
||||
exit "$build_exit"
|
||||
fi
|
||||
|
||||
if [ -e "$artifact" ]; then
|
||||
exit_code=0
|
||||
else
|
||||
exit_code=2
|
||||
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"
|
||||
exit "$exit_code"
|
||||
52
scripts/automation/platform-build.ps1
Normal file
52
scripts/automation/platform-build.ps1
Normal file
@@ -0,0 +1,52 @@
|
||||
[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")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
$results = @()
|
||||
$overallExitCode = 0
|
||||
|
||||
foreach ($preset in $Presets) {
|
||||
& cmake --preset $preset
|
||||
$configureExitCode = $LASTEXITCODE
|
||||
if ($configureExitCode -ne 0) {
|
||||
$overallExitCode = $configureExitCode
|
||||
$results += [ordered]@{
|
||||
preset = $preset
|
||||
stage = "configure"
|
||||
exitCode = $configureExitCode
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$buildArgs = @("--build", "--preset", $preset)
|
||||
foreach ($target in $Targets) {
|
||||
$buildArgs += @("--target", $target)
|
||||
}
|
||||
|
||||
& cmake @buildArgs
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
if ($buildExitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $buildExitCode
|
||||
}
|
||||
|
||||
$results += [ordered]@{
|
||||
preset = $preset
|
||||
stage = "build"
|
||||
targets = $Targets
|
||||
exitCode = $buildExitCode
|
||||
}
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
command = "platform-build"
|
||||
exitCode = $overallExitCode
|
||||
elapsedMs = $elapsed
|
||||
results = $results
|
||||
} | ConvertTo-Json -Compress -Depth 6
|
||||
|
||||
exit $overallExitCode
|
||||
29
scripts/automation/platform-build.sh
Normal file
29
scripts/automation/platform-build.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-android-arm64}"
|
||||
shift || true
|
||||
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_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}"
|
||||
start="$(date +%s)"
|
||||
|
||||
cmake --preset "$preset"
|
||||
configure_exit="$?"
|
||||
if [ "$configure_exit" -ne 0 ]; then
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"platform-build","preset":"%s","stage":"configure","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configure_exit" "$elapsed_ms"
|
||||
exit "$configure_exit"
|
||||
fi
|
||||
|
||||
build_args=""
|
||||
for target in $targets; do
|
||||
build_args="$build_args --target $target"
|
||||
done
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
cmake --build --preset "$preset" $build_args
|
||||
build_exit="$?"
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"platform-build","preset":"%s","targets":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$targets" "$build_exit" "$elapsed_ms"
|
||||
exit "$build_exit"
|
||||
22
scripts/automation/test.ps1
Normal file
22
scripts/automation/test.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "desktop-fast",
|
||||
[string]$Configuration = "Debug"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
|
||||
& ctest --preset $Preset --build-config $Configuration
|
||||
$exitCode = $LASTEXITCODE
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "test"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
exit $exitCode
|
||||
12
scripts/automation/test.sh
Normal file
12
scripts/automation/test.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-desktop-fast}"
|
||||
configuration="${2:-Debug}"
|
||||
start="$(date +%s)"
|
||||
ctest --preset "$preset" --build-config "$configuration"
|
||||
exit_code="$?"
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"test","preset":"%s","configuration":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$exit_code" "$elapsed_ms"
|
||||
exit "$exit_code"
|
||||
94
src/assets/image_format.cpp
Normal file
94
src/assets/image_format.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "assets/image_format.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array png_signature {
|
||||
std::byte { 0x89 },
|
||||
std::byte { 0x50 },
|
||||
std::byte { 0x4e },
|
||||
std::byte { 0x47 },
|
||||
std::byte { 0x0d },
|
||||
std::byte { 0x0a },
|
||||
std::byte { 0x1a },
|
||||
std::byte { 0x0a },
|
||||
};
|
||||
|
||||
[[nodiscard]] bool starts_with(std::span<const std::byte> bytes, std::span<const std::byte> prefix) noexcept
|
||||
{
|
||||
if (bytes.size() < prefix.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < prefix.size(); ++i) {
|
||||
if (bytes[i] != prefix[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_prefix_of(std::span<const std::byte> bytes, std::span<const std::byte> signature) noexcept
|
||||
{
|
||||
if (bytes.size() >= signature.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < bytes.size(); ++i) {
|
||||
if (bytes[i] != signature[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<ImageFormat> detect_image_format(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
if (bytes.empty()) {
|
||||
return pp::foundation::Result<ImageFormat>::failure(
|
||||
pp::foundation::Status::invalid_argument("image data must not be empty"));
|
||||
}
|
||||
|
||||
if (starts_with(bytes, png_signature)) {
|
||||
return pp::foundation::Result<ImageFormat>::success(ImageFormat::png);
|
||||
}
|
||||
|
||||
if (is_prefix_of(bytes, png_signature)) {
|
||||
return pp::foundation::Result<ImageFormat>::failure(
|
||||
pp::foundation::Status::out_of_range("image data is a truncated PNG signature"));
|
||||
}
|
||||
|
||||
if (bytes.size() < 3U) {
|
||||
return pp::foundation::Result<ImageFormat>::failure(
|
||||
pp::foundation::Status::out_of_range("image data is too short to identify"));
|
||||
}
|
||||
|
||||
if (bytes[0] == std::byte { 0xff } && bytes[1] == std::byte { 0xd8 } && bytes[2] == std::byte { 0xff }) {
|
||||
return pp::foundation::Result<ImageFormat>::success(ImageFormat::jpeg);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<ImageFormat>::failure(
|
||||
pp::foundation::Status::invalid_argument("unsupported image signature"));
|
||||
}
|
||||
|
||||
const char* image_format_name(ImageFormat format) noexcept
|
||||
{
|
||||
switch (format) {
|
||||
case ImageFormat::png:
|
||||
return "png";
|
||||
case ImageFormat::jpeg:
|
||||
return "jpeg";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
20
src/assets/image_format.h
Normal file
20
src/assets/image_format.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
enum class ImageFormat {
|
||||
png,
|
||||
jpeg,
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<ImageFormat> detect_image_format(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] const char* image_format_name(ImageFormat format) noexcept;
|
||||
|
||||
}
|
||||
146
src/assets/image_metadata.cpp
Normal file
146
src/assets/image_metadata.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "assets/image_metadata.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::byte png_signature[] {
|
||||
std::byte { 0x89 },
|
||||
std::byte { 0x50 },
|
||||
std::byte { 0x4e },
|
||||
std::byte { 0x47 },
|
||||
std::byte { 0x0d },
|
||||
std::byte { 0x0a },
|
||||
std::byte { 0x1a },
|
||||
std::byte { 0x0a },
|
||||
};
|
||||
|
||||
[[nodiscard]] bool has_png_signature(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
if (bytes.size() < 8U) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < 8U; ++i) {
|
||||
if (bytes[i] != png_signature[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t read_u32_be(std::span<const std::byte> bytes, std::size_t offset) noexcept
|
||||
{
|
||||
return (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset])) << 24U)
|
||||
| (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 1U])) << 16U)
|
||||
| (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 2U])) << 8U)
|
||||
| static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 3U]));
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<ImageColorType> parse_png_color_type(std::byte value) noexcept
|
||||
{
|
||||
switch (std::to_integer<std::uint8_t>(value)) {
|
||||
case 0:
|
||||
return pp::foundation::Result<ImageColorType>::success(ImageColorType::grayscale);
|
||||
case 2:
|
||||
return pp::foundation::Result<ImageColorType>::success(ImageColorType::rgb);
|
||||
case 3:
|
||||
return pp::foundation::Result<ImageColorType>::success(ImageColorType::indexed);
|
||||
case 4:
|
||||
return pp::foundation::Result<ImageColorType>::success(ImageColorType::grayscale_alpha);
|
||||
case 6:
|
||||
return pp::foundation::Result<ImageColorType>::success(ImageColorType::rgba);
|
||||
default:
|
||||
return pp::foundation::Result<ImageColorType>::failure(
|
||||
pp::foundation::Status::invalid_argument("PNG color type is unsupported"));
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint8_t component_count(ImageColorType color_type) noexcept
|
||||
{
|
||||
switch (color_type) {
|
||||
case ImageColorType::grayscale:
|
||||
case ImageColorType::indexed:
|
||||
return 1;
|
||||
case ImageColorType::grayscale_alpha:
|
||||
return 2;
|
||||
case ImageColorType::rgb:
|
||||
return 3;
|
||||
case ImageColorType::rgba:
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<ImageMetadata> parse_png_metadata(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
constexpr std::size_t png_ihdr_end = 33;
|
||||
if (bytes.size() < png_ihdr_end) {
|
||||
return pp::foundation::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::out_of_range("PNG metadata is truncated"));
|
||||
}
|
||||
|
||||
if (!has_png_signature(bytes)) {
|
||||
return pp::foundation::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::invalid_argument("PNG signature is invalid"));
|
||||
}
|
||||
|
||||
const auto ihdr_length = read_u32_be(bytes, 8);
|
||||
if (ihdr_length != 13U || bytes[12] != std::byte { 'I' } || bytes[13] != std::byte { 'H' }
|
||||
|| bytes[14] != std::byte { 'D' } || bytes[15] != std::byte { 'R' }) {
|
||||
return pp::foundation::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::invalid_argument("PNG IHDR chunk is invalid"));
|
||||
}
|
||||
|
||||
const auto width = read_u32_be(bytes, 16);
|
||||
const auto height = read_u32_be(bytes, 20);
|
||||
if (width == 0 || height == 0 || width > max_image_dimension || height > max_image_dimension) {
|
||||
return pp::foundation::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::out_of_range("PNG dimensions are outside the configured range"));
|
||||
}
|
||||
|
||||
const auto bit_depth = std::to_integer<std::uint8_t>(bytes[24]);
|
||||
if (bit_depth == 0U) {
|
||||
return pp::foundation::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::invalid_argument("PNG bit depth is invalid"));
|
||||
}
|
||||
|
||||
const auto color_type = parse_png_color_type(bytes[25]);
|
||||
if (!color_type) {
|
||||
return pp::foundation::Result<ImageMetadata>::failure(color_type.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<ImageMetadata>::success(ImageMetadata {
|
||||
.width = width,
|
||||
.height = height,
|
||||
.bit_depth = bit_depth,
|
||||
.components = component_count(color_type.value()),
|
||||
.color_type = color_type.value(),
|
||||
});
|
||||
}
|
||||
|
||||
const char* image_color_type_name(ImageColorType color_type) noexcept
|
||||
{
|
||||
switch (color_type) {
|
||||
case ImageColorType::grayscale:
|
||||
return "grayscale";
|
||||
case ImageColorType::rgb:
|
||||
return "rgb";
|
||||
case ImageColorType::indexed:
|
||||
return "indexed";
|
||||
case ImageColorType::grayscale_alpha:
|
||||
return "grayscale_alpha";
|
||||
case ImageColorType::rgba:
|
||||
return "rgba";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
34
src/assets/image_metadata.h
Normal file
34
src/assets/image_metadata.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
constexpr std::uint32_t max_image_dimension = 262144;
|
||||
|
||||
enum class ImageColorType : std::uint8_t {
|
||||
grayscale,
|
||||
rgb,
|
||||
indexed,
|
||||
grayscale_alpha,
|
||||
rgba,
|
||||
};
|
||||
|
||||
struct ImageMetadata {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint8_t bit_depth = 0;
|
||||
std::uint8_t components = 0;
|
||||
ImageColorType color_type = ImageColorType::rgba;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<ImageMetadata> parse_png_metadata(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] const char* image_color_type_name(ImageColorType color_type) noexcept;
|
||||
|
||||
}
|
||||
94
src/assets/image_pixels.cpp
Normal file
94
src/assets/image_pixels.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "assets/image_pixels.h"
|
||||
|
||||
#include "assets/image_metadata.h"
|
||||
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#define STB_IMAGE_STATIC
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include <stb/stb_image.h>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> rgba_byte_size(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height) noexcept
|
||||
{
|
||||
const auto pixels = static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height);
|
||||
constexpr auto channels = 4ULL;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / channels) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("RGBA byte size overflows"));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * channels;
|
||||
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("RGBA byte size exceeds addressable memory"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<Rgba8Image> decode_png_rgba8(std::span<const std::byte> bytes)
|
||||
{
|
||||
const auto metadata = parse_png_metadata(bytes);
|
||||
if (!metadata) {
|
||||
return pp::foundation::Result<Rgba8Image>::failure(metadata.status());
|
||||
}
|
||||
|
||||
if (bytes.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
return pp::foundation::Result<Rgba8Image>::failure(
|
||||
pp::foundation::Status::out_of_range("PNG payload is too large for the decoder"));
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int source_components = 0;
|
||||
auto* decoded = stbi_load_from_memory(
|
||||
reinterpret_cast<const stbi_uc*>(bytes.data()),
|
||||
static_cast<int>(bytes.size()),
|
||||
&width,
|
||||
&height,
|
||||
&source_components,
|
||||
4);
|
||||
if (decoded == nullptr) {
|
||||
return pp::foundation::Result<Rgba8Image>::failure(
|
||||
pp::foundation::Status::invalid_argument("PNG payload could not be decoded"));
|
||||
}
|
||||
|
||||
const auto cleanup = [decoded]() noexcept {
|
||||
stbi_image_free(decoded);
|
||||
};
|
||||
|
||||
if (width <= 0 || height <= 0
|
||||
|| static_cast<std::uint32_t>(width) != metadata.value().width
|
||||
|| static_cast<std::uint32_t>(height) != metadata.value().height) {
|
||||
cleanup();
|
||||
return pp::foundation::Result<Rgba8Image>::failure(
|
||||
pp::foundation::Status::invalid_argument("decoded PNG dimensions are inconsistent"));
|
||||
}
|
||||
|
||||
const auto byte_count = rgba_byte_size(metadata.value().width, metadata.value().height);
|
||||
if (!byte_count) {
|
||||
cleanup();
|
||||
return pp::foundation::Result<Rgba8Image>::failure(byte_count.status());
|
||||
}
|
||||
|
||||
Rgba8Image image {
|
||||
.width = metadata.value().width,
|
||||
.height = metadata.value().height,
|
||||
.pixels = {},
|
||||
};
|
||||
image.pixels.assign(decoded, decoded + byte_count.value());
|
||||
cleanup();
|
||||
|
||||
return pp::foundation::Result<Rgba8Image>::success(std::move(image));
|
||||
}
|
||||
|
||||
}
|
||||
21
src/assets/image_pixels.h
Normal file
21
src/assets/image_pixels.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
struct Rgba8Image {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::vector<std::uint8_t> pixels;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<Rgba8Image> decode_png_rgba8(
|
||||
std::span<const std::byte> bytes);
|
||||
|
||||
}
|
||||
619
src/assets/ppi_header.cpp
Normal file
619
src/assets/ppi_header.cpp
Normal file
@@ -0,0 +1,619 @@
|
||||
#include "assets/ppi_header.h"
|
||||
|
||||
#include "assets/image_metadata.h"
|
||||
#include "foundation/binary_stream.h"
|
||||
|
||||
#include <bit>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_u32(pp::foundation::ByteReader& reader) noexcept
|
||||
{
|
||||
return reader.read_u32_le();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_positive_i32(
|
||||
pp::foundation::ByteReader& reader,
|
||||
const char* message) noexcept
|
||||
{
|
||||
const auto value = reader.read_u32_le();
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value.value() > static_cast<std::uint32_t>(std::numeric_limits<std::int32_t>::max())) {
|
||||
return pp::foundation::Result<std::uint32_t>::failure(
|
||||
pp::foundation::Status::out_of_range(message));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<float> read_f32(pp::foundation::ByteReader& reader) noexcept
|
||||
{
|
||||
const auto bits = reader.read_u32_le();
|
||||
if (!bits) {
|
||||
return pp::foundation::Result<float>::failure(bits.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<float>::success(std::bit_cast<float>(bits.value()));
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status skip_bytes(
|
||||
pp::foundation::ByteReader& reader,
|
||||
std::size_t bytes) noexcept
|
||||
{
|
||||
const auto skipped = reader.read_bytes(bytes);
|
||||
if (!skipped) {
|
||||
return skipped.status();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_canvas_size(std::uint32_t width, std::uint32_t height) noexcept
|
||||
{
|
||||
if (width == 0 || height == 0) {
|
||||
return pp::foundation::Status::invalid_argument("PPI canvas dimensions must be greater than zero");
|
||||
}
|
||||
|
||||
if (width > max_ppi_canvas_dimension || height > max_ppi_canvas_dimension) {
|
||||
return pp::foundation::Status::out_of_range("PPI canvas dimensions exceed the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status add_payload_bytes(PpiBodySummary& summary, std::uint32_t bytes) noexcept
|
||||
{
|
||||
const auto next = summary.compressed_face_bytes + static_cast<std::uint64_t>(bytes);
|
||||
if (next > max_ppi_face_payload_bytes) {
|
||||
return pp::foundation::Status::out_of_range("PPI compressed face payload exceeds the configured limit");
|
||||
}
|
||||
|
||||
summary.compressed_face_bytes = next;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<ImageMetadata> validate_face_png_payload(
|
||||
std::span<const std::byte> payload,
|
||||
std::uint32_t width,
|
||||
std::uint32_t height) noexcept
|
||||
{
|
||||
const auto metadata = parse_png_metadata(payload);
|
||||
if (!metadata) {
|
||||
return pp::foundation::Result<ImageMetadata>::failure(metadata.status());
|
||||
}
|
||||
|
||||
if (metadata.value().width != width || metadata.value().height != height) {
|
||||
return pp::foundation::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI face PNG dimensions do not match the dirty box"));
|
||||
}
|
||||
|
||||
if (metadata.value().bit_depth != 8U || metadata.value().components != 4U
|
||||
|| metadata.value().color_type != ImageColorType::rgba) {
|
||||
return pp::foundation::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI face PNG payload must be 8-bit RGBA"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<ImageMetadata>::success(metadata.value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
if (bytes.size() < ppi_header_size) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI header is truncated"));
|
||||
}
|
||||
|
||||
pp::foundation::ByteReader reader(bytes.subspan(0, ppi_header_size));
|
||||
const auto magic = reader.read_bytes(4);
|
||||
if (!magic || magic.value()[0] != std::byte { 'P' } || magic.value()[1] != std::byte { 'P' }
|
||||
|| magic.value()[2] != std::byte { 'I' } || magic.value()[3] != std::byte { 0 }) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI header magic is invalid"));
|
||||
}
|
||||
|
||||
PpiHeaderInfo info;
|
||||
const auto doc_major = read_u32(reader);
|
||||
const auto doc_minor = read_u32(reader);
|
||||
const auto soft_major = read_u32(reader);
|
||||
const auto soft_minor = read_u32(reader);
|
||||
const auto soft_fix = read_u32(reader);
|
||||
const auto soft_build = read_u32(reader);
|
||||
const auto thumb_width = read_u32(reader);
|
||||
const auto thumb_height = read_u32(reader);
|
||||
const auto thumb_components = read_u32(reader);
|
||||
if (!doc_major || !doc_minor || !soft_major || !soft_minor || !soft_fix || !soft_build
|
||||
|| !thumb_width || !thumb_height || !thumb_components) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI header is truncated"));
|
||||
}
|
||||
|
||||
info.document_version = { doc_major.value(), doc_minor.value() };
|
||||
info.software_version = {
|
||||
soft_major.value(),
|
||||
soft_minor.value(),
|
||||
soft_fix.value(),
|
||||
soft_build.value(),
|
||||
};
|
||||
info.thumbnail = {
|
||||
thumb_width.value(),
|
||||
thumb_height.value(),
|
||||
thumb_components.value(),
|
||||
};
|
||||
|
||||
if (info.document_version.major != 0 || info.document_version.minor < 1) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI document version is unsupported"));
|
||||
}
|
||||
|
||||
if (info.thumbnail.width != 128 || info.thumbnail.height != 128 || info.thumbnail.components != 4) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiHeaderInfo>::success(info);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::size_t> ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept
|
||||
{
|
||||
if (thumbnail.width == 0 || thumbnail.height == 0 || thumbnail.components == 0) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
|
||||
}
|
||||
|
||||
const auto width = static_cast<std::uint64_t>(thumbnail.width);
|
||||
const auto height = static_cast<std::uint64_t>(thumbnail.height);
|
||||
const auto components = static_cast<std::uint64_t>(thumbnail.components);
|
||||
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
|
||||
}
|
||||
|
||||
const auto pixels = width * height;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * components;
|
||||
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI thumbnail byte size exceeds addressable memory"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
const auto header = parse_ppi_header(bytes);
|
||||
if (!header) {
|
||||
return pp::foundation::Result<PpiProjectLayout>::failure(header.status());
|
||||
}
|
||||
|
||||
const auto thumbnail_bytes = ppi_thumbnail_byte_size(header.value().thumbnail);
|
||||
if (!thumbnail_bytes) {
|
||||
return pp::foundation::Result<PpiProjectLayout>::failure(thumbnail_bytes.status());
|
||||
}
|
||||
|
||||
if (thumbnail_bytes.value() > std::numeric_limits<std::size_t>::max() - ppi_header_size) {
|
||||
return pp::foundation::Result<PpiProjectLayout>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
|
||||
}
|
||||
|
||||
const auto body_offset = ppi_header_size + thumbnail_bytes.value();
|
||||
if (bytes.size() < body_offset) {
|
||||
return pp::foundation::Result<PpiProjectLayout>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI thumbnail payload is truncated"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiProjectLayout>::success(PpiProjectLayout {
|
||||
.header = header.value(),
|
||||
.thumbnail_offset = ppi_header_size,
|
||||
.thumbnail_bytes = thumbnail_bytes.value(),
|
||||
.body_offset = body_offset,
|
||||
.body_bytes = bytes.size() - body_offset,
|
||||
});
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body,
|
||||
PpiBodyIndex* index) noexcept
|
||||
{
|
||||
if (index != nullptr) {
|
||||
index->summary = {};
|
||||
index->layers.clear();
|
||||
}
|
||||
|
||||
pp::foundation::ByteReader reader(body);
|
||||
const auto width = read_positive_i32(reader, "PPI canvas width is outside the supported range");
|
||||
const auto height = read_positive_i32(reader, "PPI canvas height is outside the supported range");
|
||||
const auto layer_count = read_positive_i32(reader, "PPI layer count is outside the supported range");
|
||||
if (!width || !height || !layer_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
!width ? width.status() : (!height ? height.status() : layer_count.status()));
|
||||
}
|
||||
|
||||
const auto canvas_status = validate_canvas_size(width.value(), height.value());
|
||||
if (!canvas_status.ok()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(canvas_status);
|
||||
}
|
||||
|
||||
if (layer_count.value() == 0 || layer_count.value() > max_ppi_layer_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
|
||||
}
|
||||
|
||||
PpiBodySummary summary {
|
||||
.width = width.value(),
|
||||
.height = height.value(),
|
||||
.layer_count = layer_count.value(),
|
||||
.declared_frame_count = 1,
|
||||
};
|
||||
|
||||
std::vector<bool> seen_orders;
|
||||
if (index != nullptr) {
|
||||
index->layers.resize(summary.layer_count);
|
||||
seen_orders.assign(summary.layer_count, false);
|
||||
}
|
||||
|
||||
if (header.document_version.minor >= 3U) {
|
||||
const auto declared_frames = read_positive_i32(reader, "PPI declared frame count is outside the supported range");
|
||||
if (!declared_frames) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(declared_frames.status());
|
||||
}
|
||||
|
||||
if (declared_frames.value() == 0 || declared_frames.value() > max_ppi_frame_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI declared frame count is outside the configured range"));
|
||||
}
|
||||
summary.declared_frame_count = declared_frames.value();
|
||||
}
|
||||
|
||||
for (std::uint32_t layer_index = 0; layer_index < summary.layer_count; ++layer_index) {
|
||||
const auto order = read_positive_i32(reader, "PPI layer order is outside the supported range");
|
||||
const auto opacity = read_f32(reader);
|
||||
const auto name_length = read_positive_i32(reader, "PPI layer name length is outside the supported range");
|
||||
if (!order || !opacity || !name_length) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
!order ? order.status() : (!opacity ? opacity.status() : name_length.status()));
|
||||
}
|
||||
|
||||
if (order.value() >= summary.layer_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer order is outside the layer list"));
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
if (seen_orders[order.value()]) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI layer order is duplicated"));
|
||||
}
|
||||
seen_orders[order.value()] = true;
|
||||
}
|
||||
|
||||
if (opacity.value() < 0.0F || opacity.value() > 1.0F) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
|
||||
}
|
||||
|
||||
if (name_length.value() > max_ppi_layer_name_length) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
|
||||
}
|
||||
|
||||
const auto name_bytes = reader.read_bytes(name_length.value());
|
||||
if (!name_bytes) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(name_bytes.status());
|
||||
}
|
||||
|
||||
PpiLayerSummary layer_summary;
|
||||
if (index != nullptr) {
|
||||
layer_summary.stored_order = order.value();
|
||||
layer_summary.opacity = opacity.value();
|
||||
layer_summary.name.reserve(name_bytes.value().size());
|
||||
for (const auto byte : name_bytes.value()) {
|
||||
layer_summary.name.push_back(static_cast<char>(std::to_integer<unsigned char>(byte)));
|
||||
}
|
||||
}
|
||||
|
||||
if (header.document_version.minor >= 2U) {
|
||||
const auto blend_mode = read_positive_i32(reader, "PPI layer blend mode is outside the supported range");
|
||||
const auto alpha_locked = reader.read_u8();
|
||||
const auto visible = reader.read_u8();
|
||||
if (!blend_mode || !alpha_locked || !visible) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
!blend_mode ? blend_mode.status() : (!alpha_locked ? alpha_locked.status() : visible.status()));
|
||||
}
|
||||
|
||||
if (alpha_locked.value() > 1U || visible.value() > 1U) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI layer boolean field is invalid"));
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
layer_summary.blend_mode = blend_mode.value();
|
||||
layer_summary.alpha_locked = alpha_locked.value() != 0U;
|
||||
layer_summary.visible = visible.value() != 0U;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t layer_frames = 1;
|
||||
if (header.document_version.minor >= 3U) {
|
||||
const auto frame_count = read_positive_i32(reader, "PPI layer frame count is outside the supported range");
|
||||
if (!frame_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(frame_count.status());
|
||||
}
|
||||
|
||||
if (frame_count.value() == 0 || frame_count.value() > max_ppi_frame_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range"));
|
||||
}
|
||||
layer_frames = frame_count.value();
|
||||
}
|
||||
|
||||
if (summary.total_layer_frames > max_ppi_frame_count - layer_frames) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI total frame count exceeds the configured limit"));
|
||||
}
|
||||
summary.total_layer_frames += layer_frames;
|
||||
|
||||
if (index != nullptr) {
|
||||
layer_summary.frames.resize(layer_frames);
|
||||
}
|
||||
|
||||
for (std::uint32_t frame_index = 0; frame_index < layer_frames; ++frame_index) {
|
||||
if (header.document_version.minor >= 3U) {
|
||||
const auto duration = read_positive_i32(reader, "PPI frame duration is outside the supported range");
|
||||
if (!duration) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(duration.status());
|
||||
}
|
||||
|
||||
if (duration.value() == 0) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
layer_summary.frames[frame_index].duration_ms = duration.value();
|
||||
}
|
||||
}
|
||||
|
||||
for (std::uint32_t face = 0; face < 6U; ++face) {
|
||||
const auto has_data = read_positive_i32(reader, "PPI face data flag is outside the supported range");
|
||||
if (!has_data) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(has_data.status());
|
||||
}
|
||||
|
||||
if (has_data.value() > 1U) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI face data flag is invalid"));
|
||||
}
|
||||
|
||||
if (has_data.value() == 0U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
++summary.dirty_face_count;
|
||||
const auto x0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||
const auto y0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||
const auto x1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||
const auto y1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||
const auto data_size = read_positive_i32(reader, "PPI compressed face data size is outside the supported range");
|
||||
if (!x0 || !y0 || !x1 || !y1 || !data_size) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
!x0 ? x0.status()
|
||||
: (!y0 ? y0.status() : (!x1 ? x1.status() : (!y1 ? y1.status() : data_size.status()))));
|
||||
}
|
||||
|
||||
if (x0.value() >= x1.value() || y0.value() >= y1.value() || x1.value() > summary.width
|
||||
|| y1.value() > summary.height) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI dirty box is outside the canvas"));
|
||||
}
|
||||
|
||||
if (data_size.value() == 0U) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI compressed face payload must not be empty"));
|
||||
}
|
||||
|
||||
const auto byte_status = add_payload_bytes(summary, data_size.value());
|
||||
if (!byte_status.ok()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(byte_status);
|
||||
}
|
||||
|
||||
const auto payload_offset = reader.position();
|
||||
const auto payload = reader.read_bytes(data_size.value());
|
||||
if (!payload) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(payload.status());
|
||||
}
|
||||
|
||||
const auto png_metadata = validate_face_png_payload(
|
||||
payload.value(),
|
||||
x1.value() - x0.value(),
|
||||
y1.value() - y0.value());
|
||||
if (!png_metadata) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(png_metadata.status());
|
||||
}
|
||||
|
||||
++summary.rgba_face_payload_count;
|
||||
if (index != nullptr) {
|
||||
layer_summary.frames[frame_index].faces[face] = PpiFacePayloadSummary {
|
||||
.has_data = true,
|
||||
.x0 = x0.value(),
|
||||
.y0 = y0.value(),
|
||||
.x1 = x1.value(),
|
||||
.y1 = y1.value(),
|
||||
.body_payload_offset = static_cast<std::uint32_t>(payload_offset),
|
||||
.payload_bytes = data_size.value(),
|
||||
.png_width = png_metadata.value().width,
|
||||
.png_height = png_metadata.value().height,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
index->layers[order.value()] = std::move(layer_summary);
|
||||
}
|
||||
}
|
||||
|
||||
if (header.document_version.minor >= 3U && summary.total_layer_frames != summary.declared_frame_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI declared frame count does not match layer frames"));
|
||||
}
|
||||
|
||||
if (header.document_version.minor >= 4U) {
|
||||
const auto info_bytes = read_positive_i32(reader, "PPI info block size is outside the supported range");
|
||||
if (!info_bytes) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(info_bytes.status());
|
||||
}
|
||||
|
||||
summary.info_bytes = info_bytes.value();
|
||||
const auto info_status = skip_bytes(reader, summary.info_bytes);
|
||||
if (!info_status.ok()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(info_status);
|
||||
}
|
||||
}
|
||||
|
||||
if (!reader.empty()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI body has trailing bytes"));
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
index->summary = summary;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiBodySummary>::success(summary);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body) noexcept
|
||||
{
|
||||
return parse_ppi_body_impl(header, body, nullptr);
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiBodyIndex> parse_ppi_body_index(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body)
|
||||
{
|
||||
PpiBodyIndex index;
|
||||
const auto summary = parse_ppi_body_impl(header, body, &index);
|
||||
if (!summary) {
|
||||
return pp::foundation::Result<PpiBodyIndex>::failure(summary.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiBodyIndex>::success(std::move(index));
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
if (!layout) {
|
||||
return pp::foundation::Result<PpiProjectSummary>::failure(layout.status());
|
||||
}
|
||||
|
||||
const auto body = parse_ppi_body_summary(
|
||||
layout.value().header,
|
||||
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
|
||||
if (!body) {
|
||||
return pp::foundation::Result<PpiProjectSummary>::failure(body.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiProjectSummary>::success(PpiProjectSummary {
|
||||
.layout = layout.value(),
|
||||
.body = body.value(),
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(std::span<const std::byte> bytes)
|
||||
{
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
if (!layout) {
|
||||
return pp::foundation::Result<PpiProjectIndex>::failure(layout.status());
|
||||
}
|
||||
|
||||
const auto body = parse_ppi_body_index(
|
||||
layout.value().header,
|
||||
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
|
||||
if (!body) {
|
||||
return pp::foundation::Result<PpiProjectIndex>::failure(body.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiProjectIndex>::success(PpiProjectIndex {
|
||||
.layout = layout.value(),
|
||||
.body = body.value(),
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(std::span<const std::byte> bytes)
|
||||
{
|
||||
auto project = parse_ppi_project_index(bytes);
|
||||
if (!project) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::failure(project.status());
|
||||
}
|
||||
|
||||
PpiDecodedProjectImages decoded {
|
||||
.project = project.value(),
|
||||
.faces = {},
|
||||
};
|
||||
decoded.faces.reserve(decoded.project.body.summary.rgba_face_payload_count);
|
||||
|
||||
const auto body = bytes.subspan(decoded.project.layout.body_offset, decoded.project.layout.body_bytes);
|
||||
for (std::size_t layer_index = 0; layer_index < decoded.project.body.layers.size(); ++layer_index) {
|
||||
const auto& layer = decoded.project.body.layers[layer_index];
|
||||
for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) {
|
||||
const auto& frame = layer.frames[frame_index];
|
||||
for (std::size_t face_index = 0; face_index < frame.faces.size(); ++face_index) {
|
||||
const auto& face = frame.faces[face_index];
|
||||
if (!face.has_data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (face.body_payload_offset > body.size()
|
||||
|| face.payload_bytes > body.size() - face.body_payload_offset) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI face payload range is outside the body"));
|
||||
}
|
||||
|
||||
const auto image = decode_png_rgba8(
|
||||
body.subspan(face.body_payload_offset, face.payload_bytes));
|
||||
if (!image) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::failure(image.status());
|
||||
}
|
||||
|
||||
if (image.value().width != face.png_width || image.value().height != face.png_height) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
|
||||
pp::foundation::Status::invalid_argument("decoded PPI face payload dimensions changed"));
|
||||
}
|
||||
|
||||
decoded.faces.push_back(PpiDecodedFacePayload {
|
||||
.layer_index = static_cast<std::uint32_t>(layer_index),
|
||||
.frame_index = static_cast<std::uint32_t>(frame_index),
|
||||
.face_index = static_cast<std::uint32_t>(face_index),
|
||||
.descriptor = face,
|
||||
.image = image.value(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::success(std::move(decoded));
|
||||
}
|
||||
|
||||
}
|
||||
146
src/assets/ppi_header.h
Normal file
146
src/assets/ppi_header.h
Normal file
@@ -0,0 +1,146 @@
|
||||
#pragma once
|
||||
|
||||
#include "assets/image_pixels.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
constexpr std::size_t ppi_header_size = 40;
|
||||
constexpr std::uint32_t max_ppi_canvas_dimension = 131072;
|
||||
constexpr std::uint32_t max_ppi_layer_count = 1024;
|
||||
constexpr std::uint32_t max_ppi_frame_count = 100000;
|
||||
constexpr std::size_t max_ppi_layer_name_length = 128;
|
||||
constexpr std::uint64_t max_ppi_face_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
|
||||
|
||||
struct PpiVersion {
|
||||
std::uint32_t major = 0;
|
||||
std::uint32_t minor = 0;
|
||||
};
|
||||
|
||||
struct PpiSoftwareVersion {
|
||||
std::uint32_t major = 0;
|
||||
std::uint32_t minor = 0;
|
||||
std::uint32_t fix = 0;
|
||||
std::uint32_t build = 0;
|
||||
};
|
||||
|
||||
struct PpiThumbnailInfo {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t components = 0;
|
||||
};
|
||||
|
||||
struct PpiHeaderInfo {
|
||||
PpiVersion document_version;
|
||||
PpiSoftwareVersion software_version;
|
||||
PpiThumbnailInfo thumbnail;
|
||||
};
|
||||
|
||||
struct PpiProjectLayout {
|
||||
PpiHeaderInfo header;
|
||||
std::size_t thumbnail_offset = 0;
|
||||
std::size_t thumbnail_bytes = 0;
|
||||
std::size_t body_offset = 0;
|
||||
std::size_t body_bytes = 0;
|
||||
};
|
||||
|
||||
struct PpiBodySummary {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t layer_count = 0;
|
||||
std::uint32_t declared_frame_count = 0;
|
||||
std::uint32_t total_layer_frames = 0;
|
||||
std::uint32_t dirty_face_count = 0;
|
||||
std::uint32_t rgba_face_payload_count = 0;
|
||||
std::uint64_t compressed_face_bytes = 0;
|
||||
std::uint32_t info_bytes = 0;
|
||||
};
|
||||
|
||||
struct PpiProjectSummary {
|
||||
PpiProjectLayout layout;
|
||||
PpiBodySummary body;
|
||||
};
|
||||
|
||||
struct PpiFacePayloadSummary {
|
||||
bool has_data = false;
|
||||
std::uint32_t x0 = 0;
|
||||
std::uint32_t y0 = 0;
|
||||
std::uint32_t x1 = 0;
|
||||
std::uint32_t y1 = 0;
|
||||
std::uint32_t body_payload_offset = 0;
|
||||
std::uint32_t payload_bytes = 0;
|
||||
std::uint32_t png_width = 0;
|
||||
std::uint32_t png_height = 0;
|
||||
};
|
||||
|
||||
struct PpiFrameSummary {
|
||||
std::uint32_t duration_ms = 100;
|
||||
std::array<PpiFacePayloadSummary, 6> faces;
|
||||
};
|
||||
|
||||
struct PpiLayerSummary {
|
||||
std::uint32_t stored_order = 0;
|
||||
std::string name;
|
||||
float opacity = 1.0F;
|
||||
std::uint32_t blend_mode = 0;
|
||||
bool alpha_locked = false;
|
||||
bool visible = true;
|
||||
std::vector<PpiFrameSummary> frames;
|
||||
};
|
||||
|
||||
struct PpiBodyIndex {
|
||||
PpiBodySummary summary;
|
||||
std::vector<PpiLayerSummary> layers;
|
||||
};
|
||||
|
||||
struct PpiProjectIndex {
|
||||
PpiProjectLayout layout;
|
||||
PpiBodyIndex body;
|
||||
};
|
||||
|
||||
struct PpiDecodedFacePayload {
|
||||
std::uint32_t layer_index = 0;
|
||||
std::uint32_t frame_index = 0;
|
||||
std::uint32_t face_index = 0;
|
||||
PpiFacePayloadSummary descriptor;
|
||||
Rgba8Image image;
|
||||
};
|
||||
|
||||
struct PpiDecodedProjectImages {
|
||||
PpiProjectIndex project;
|
||||
std::vector<PpiDecodedFacePayload> faces;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiBodyIndex> parse_ppi_body_index(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(
|
||||
std::span<const std::byte> bytes);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(
|
||||
std::span<const std::byte> bytes);
|
||||
|
||||
}
|
||||
183
src/assets/settings_document.cpp
Normal file
183
src/assets/settings_document.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#include "assets/settings_document.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool is_valid_key_char(char value) noexcept
|
||||
{
|
||||
const auto ch = static_cast<unsigned char>(value);
|
||||
return std::isalnum(ch) != 0 || value == '_' || value == '-' || value == '.';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::size_t SettingsDocument::size() const noexcept
|
||||
{
|
||||
return entries_.size();
|
||||
}
|
||||
|
||||
bool SettingsDocument::empty() const noexcept
|
||||
{
|
||||
return entries_.empty();
|
||||
}
|
||||
|
||||
bool SettingsDocument::has(std::string_view key) const noexcept
|
||||
{
|
||||
return find_entry(key) != entries_.end();
|
||||
}
|
||||
|
||||
const std::vector<SettingsEntry>& SettingsDocument::entries() const noexcept
|
||||
{
|
||||
return entries_;
|
||||
}
|
||||
|
||||
pp::foundation::Status SettingsDocument::set(std::string_view key, SettingsValue value)
|
||||
{
|
||||
const auto key_status = validate_settings_key(key);
|
||||
if (!key_status.ok()) {
|
||||
return key_status;
|
||||
}
|
||||
|
||||
const auto value_status = validate_settings_value(value);
|
||||
if (!value_status.ok()) {
|
||||
return value_status;
|
||||
}
|
||||
|
||||
auto found = find_entry(key);
|
||||
if (found != entries_.end()) {
|
||||
found->value = value;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
if (entries_.size() >= max_settings_entries) {
|
||||
return pp::foundation::Status::out_of_range("settings entry count exceeds the configured limit");
|
||||
}
|
||||
|
||||
entries_.push_back(SettingsEntry {
|
||||
.key = std::string(key),
|
||||
.value = value,
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<SettingsValue> SettingsDocument::get(std::string_view key) const
|
||||
{
|
||||
const auto key_status = validate_settings_key(key);
|
||||
if (!key_status.ok()) {
|
||||
return pp::foundation::Result<SettingsValue>::failure(key_status);
|
||||
}
|
||||
|
||||
const auto found = find_entry(key);
|
||||
if (found == entries_.end()) {
|
||||
return pp::foundation::Result<SettingsValue>::failure(
|
||||
pp::foundation::Status::out_of_range("settings key was not found"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<SettingsValue>::success(found->value);
|
||||
}
|
||||
|
||||
pp::foundation::Status SettingsDocument::unset(std::string_view key) noexcept
|
||||
{
|
||||
const auto key_status = validate_settings_key(key);
|
||||
if (!key_status.ok()) {
|
||||
return key_status;
|
||||
}
|
||||
|
||||
const auto found = find_entry(key);
|
||||
if (found == entries_.end()) {
|
||||
return pp::foundation::Status::out_of_range("settings key was not found");
|
||||
}
|
||||
|
||||
entries_.erase(found);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
void SettingsDocument::clear() noexcept
|
||||
{
|
||||
entries_.clear();
|
||||
}
|
||||
|
||||
std::vector<SettingsEntry>::iterator SettingsDocument::find_entry(std::string_view key) noexcept
|
||||
{
|
||||
return std::find_if(
|
||||
entries_.begin(),
|
||||
entries_.end(),
|
||||
[key](const SettingsEntry& entry) {
|
||||
return entry.key == key;
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<SettingsEntry>::const_iterator SettingsDocument::find_entry(std::string_view key) const noexcept
|
||||
{
|
||||
return std::find_if(
|
||||
entries_.begin(),
|
||||
entries_.end(),
|
||||
[key](const SettingsEntry& entry) {
|
||||
return entry.key == key;
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_settings_key(std::string_view key) noexcept
|
||||
{
|
||||
if (key.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("settings key must not be empty");
|
||||
}
|
||||
|
||||
if (key.size() > max_settings_key_length) {
|
||||
return pp::foundation::Status::out_of_range("settings key length exceeds the configured limit");
|
||||
}
|
||||
|
||||
if (key.front() == '.' || key.back() == '.') {
|
||||
return pp::foundation::Status::invalid_argument("settings key must not start or end with a dot");
|
||||
}
|
||||
|
||||
for (const auto ch : key) {
|
||||
if (!is_valid_key_char(ch)) {
|
||||
return pp::foundation::Status::invalid_argument("settings key contains an unsupported character");
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_settings_value(const SettingsValue& value) noexcept
|
||||
{
|
||||
if (const auto* string_value = std::get_if<std::string>(&value)) {
|
||||
if (string_value->size() > max_settings_string_length) {
|
||||
return pp::foundation::Status::out_of_range("settings string length exceeds the configured limit");
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto* double_value = std::get_if<double>(&value)) {
|
||||
if (!std::isfinite(*double_value)) {
|
||||
return pp::foundation::Status::invalid_argument("settings floating point value must be finite");
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
const char* settings_value_type_name(const SettingsValue& value) noexcept
|
||||
{
|
||||
if (std::holds_alternative<bool>(value)) {
|
||||
return "bool";
|
||||
}
|
||||
if (std::holds_alternative<std::int64_t>(value)) {
|
||||
return "int64";
|
||||
}
|
||||
if (std::holds_alternative<double>(value)) {
|
||||
return "double";
|
||||
}
|
||||
if (std::holds_alternative<std::string>(value)) {
|
||||
return "string";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
48
src/assets/settings_document.h
Normal file
48
src/assets/settings_document.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
constexpr std::size_t max_settings_entries = 4096;
|
||||
constexpr std::size_t max_settings_key_length = 128;
|
||||
constexpr std::size_t max_settings_string_length = 4096;
|
||||
|
||||
using SettingsValue = std::variant<bool, std::int64_t, double, std::string>;
|
||||
|
||||
struct SettingsEntry {
|
||||
std::string key;
|
||||
SettingsValue value;
|
||||
};
|
||||
|
||||
class SettingsDocument {
|
||||
public:
|
||||
[[nodiscard]] std::size_t size() const noexcept;
|
||||
[[nodiscard]] bool empty() const noexcept;
|
||||
[[nodiscard]] bool has(std::string_view key) const noexcept;
|
||||
[[nodiscard]] const std::vector<SettingsEntry>& entries() const noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Status set(std::string_view key, SettingsValue value);
|
||||
[[nodiscard]] pp::foundation::Result<SettingsValue> get(std::string_view key) const;
|
||||
[[nodiscard]] pp::foundation::Status unset(std::string_view key) noexcept;
|
||||
void clear() noexcept;
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::vector<SettingsEntry>::iterator find_entry(std::string_view key) noexcept;
|
||||
[[nodiscard]] std::vector<SettingsEntry>::const_iterator find_entry(std::string_view key) const noexcept;
|
||||
|
||||
std::vector<SettingsEntry> entries_;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_settings_key(std::string_view key) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_settings_value(const SettingsValue& value) noexcept;
|
||||
[[nodiscard]] const char* settings_value_type_name(const SettingsValue& value) noexcept;
|
||||
|
||||
}
|
||||
746
src/document/document.cpp
Normal file
746
src/document/document.cpp
Normal file
@@ -0,0 +1,746 @@
|
||||
#include "document/document.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::document {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_config(DocumentConfig config) noexcept
|
||||
{
|
||||
if (config.width == 0 || config.height == 0) {
|
||||
return pp::foundation::Status::invalid_argument("document dimensions must be greater than zero");
|
||||
}
|
||||
|
||||
if (config.width > max_canvas_dimension || config.height > max_canvas_dimension) {
|
||||
return pp::foundation::Status::out_of_range("document dimensions exceed the configured limit");
|
||||
}
|
||||
|
||||
if (config.layer_count == 0) {
|
||||
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
|
||||
}
|
||||
|
||||
if (config.layer_count > max_layer_count) {
|
||||
return pp::foundation::Status::out_of_range("document layer count exceeds the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string default_layer_name(std::size_t index)
|
||||
{
|
||||
return "Layer " + std::to_string(index + 1U);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint64_t frame_duration_sum(std::span<const AnimationFrame> frames) noexcept
|
||||
{
|
||||
std::uint64_t duration = 0;
|
||||
for (const auto& frame : frames) {
|
||||
duration += frame.duration_ms;
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_layer_name(std::string_view name) noexcept
|
||||
{
|
||||
if (name.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("layer name must not be empty");
|
||||
}
|
||||
|
||||
if (name.size() > max_layer_name_length) {
|
||||
return pp::foundation::Status::out_of_range("layer name length exceeds the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_layer_opacity(float opacity) noexcept
|
||||
{
|
||||
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
|
||||
return pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_blend_mode(pp::paint::BlendMode blend_mode) noexcept
|
||||
{
|
||||
switch (blend_mode) {
|
||||
case pp::paint::BlendMode::normal:
|
||||
case pp::paint::BlendMode::multiply:
|
||||
case pp::paint::BlendMode::screen:
|
||||
case pp::paint::BlendMode::color_dodge:
|
||||
case pp::paint::BlendMode::overlay:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("layer blend mode is not supported");
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_frame_duration(std::uint32_t duration_ms) noexcept
|
||||
{
|
||||
if (duration_ms < min_frame_duration_ms) {
|
||||
return pp::foundation::Status::invalid_argument("frame duration must be greater than zero");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_layer_index(std::size_t index, std::size_t layer_count) noexcept
|
||||
{
|
||||
if (index >= layer_count) {
|
||||
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_frame_index(std::size_t index, std::size_t frame_count) noexcept
|
||||
{
|
||||
if (index >= frame_count) {
|
||||
return pp::foundation::Status::out_of_range("frame index is outside the document");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height) 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"));
|
||||
}
|
||||
|
||||
const auto pixels = width64 * height64;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / rgba8_components) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("face pixel byte size overflows"));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * rgba8_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"));
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_face_pixels(
|
||||
LayerFacePixels pixels,
|
||||
std::uint32_t document_width,
|
||||
std::uint32_t document_height) noexcept
|
||||
{
|
||||
if (pixels.face_index >= cube_face_count) {
|
||||
return pp::foundation::Status::out_of_range("cube face index is outside the document");
|
||||
}
|
||||
|
||||
if (pixels.width == 0 || pixels.height == 0) {
|
||||
return pp::foundation::Status::invalid_argument("face pixel dimensions must be greater than zero");
|
||||
}
|
||||
|
||||
if (pixels.x > document_width || pixels.width > document_width - pixels.x
|
||||
|| pixels.y > document_height || pixels.height > document_height - pixels.y) {
|
||||
return pp::foundation::Status::out_of_range("face pixel rectangle is outside the document");
|
||||
}
|
||||
|
||||
const auto expected_bytes = rgba8_byte_size(pixels.width, pixels.height);
|
||||
if (!expected_bytes) {
|
||||
return expected_bytes.status();
|
||||
}
|
||||
|
||||
if (pixels.rgba8.size() != expected_bytes.value()) {
|
||||
return pp::foundation::Status::invalid_argument("face pixel payload byte size does not match dimensions");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
|
||||
{
|
||||
const auto status = validate_config(config);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(status);
|
||||
}
|
||||
|
||||
CanvasDocument document;
|
||||
document.width_ = config.width;
|
||||
document.height_ = config.height;
|
||||
document.frames_.push_back(AnimationFrame {});
|
||||
document.layers_.reserve(config.layer_count);
|
||||
for (std::uint32_t i = 0; i < config.layer_count; ++i) {
|
||||
document.layers_.push_back(Layer {
|
||||
.name = default_layer_name(i),
|
||||
.frames = document.frames_,
|
||||
});
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasDocument>::success(document);
|
||||
}
|
||||
|
||||
pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(DocumentSnapshotConfig config)
|
||||
{
|
||||
const auto status = validate_config(DocumentConfig {
|
||||
.width = config.width,
|
||||
.height = config.height,
|
||||
.layer_count = static_cast<std::uint32_t>(config.layers.size()),
|
||||
});
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(status);
|
||||
}
|
||||
|
||||
if (config.frames.empty()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("document must contain at least one frame"));
|
||||
}
|
||||
|
||||
if (config.frames.size() > max_frame_count) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
CanvasDocument document;
|
||||
document.width_ = config.width;
|
||||
document.height_ = config.height;
|
||||
document.layers_.reserve(config.layers.size());
|
||||
for (const auto& layer_config : config.layers) {
|
||||
const auto name_status = validate_layer_name(layer_config.name);
|
||||
if (!name_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(name_status);
|
||||
}
|
||||
|
||||
const auto opacity_status = validate_layer_opacity(layer_config.opacity);
|
||||
if (!opacity_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(opacity_status);
|
||||
}
|
||||
|
||||
const auto blend_status = validate_blend_mode(layer_config.blend_mode);
|
||||
if (!blend_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(blend_status);
|
||||
}
|
||||
|
||||
const auto layer_frames = layer_config.frames.empty() ? config.frames : layer_config.frames;
|
||||
if (layer_frames.empty()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("document layer must contain at least one frame"));
|
||||
}
|
||||
|
||||
if (layer_frames.size() > max_frame_count) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::out_of_range("document layer frame count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
for (const auto& frame_config : layer_frames) {
|
||||
const auto duration_status = validate_frame_duration(frame_config.duration_ms);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(duration_status);
|
||||
}
|
||||
}
|
||||
|
||||
document.layers_.push_back(Layer {
|
||||
.name = std::string(layer_config.name),
|
||||
.visible = layer_config.visible,
|
||||
.alpha_locked = layer_config.alpha_locked,
|
||||
.opacity = layer_config.opacity,
|
||||
.blend_mode = layer_config.blend_mode,
|
||||
.frames = {},
|
||||
});
|
||||
document.layers_.back().frames.assign(layer_frames.begin(), layer_frames.end());
|
||||
}
|
||||
|
||||
document.frames_.reserve(config.frames.size());
|
||||
for (const auto& frame_config : config.frames) {
|
||||
const auto duration_status = validate_frame_duration(frame_config.duration_ms);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(duration_status);
|
||||
}
|
||||
|
||||
document.frames_.push_back(frame_config);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasDocument>::success(document);
|
||||
}
|
||||
|
||||
std::uint32_t CanvasDocument::width() const noexcept
|
||||
{
|
||||
return width_;
|
||||
}
|
||||
|
||||
std::uint32_t CanvasDocument::height() const noexcept
|
||||
{
|
||||
return height_;
|
||||
}
|
||||
|
||||
std::size_t CanvasDocument::active_layer_index() const noexcept
|
||||
{
|
||||
return active_layer_index_;
|
||||
}
|
||||
|
||||
std::size_t CanvasDocument::active_frame_index() const noexcept
|
||||
{
|
||||
return active_frame_index_;
|
||||
}
|
||||
|
||||
std::uint64_t CanvasDocument::animation_duration_ms() const noexcept
|
||||
{
|
||||
std::uint64_t duration = frame_duration_sum(frames_);
|
||||
for (const auto& layer : layers_) {
|
||||
duration = std::max(duration, frame_duration_sum(layer.frames));
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::uint64_t> CanvasDocument::layer_animation_duration_ms(std::size_t index) const noexcept
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(index_status);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::uint64_t>::success(frame_duration_sum(layers_[index].frames));
|
||||
}
|
||||
|
||||
std::size_t CanvasDocument::face_pixel_payload_count() const noexcept
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (const auto& layer : layers_) {
|
||||
for (const auto& frame : layer.frames) {
|
||||
count += frame.face_pixels.size();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
std::span<const Layer> CanvasDocument::layers() const noexcept
|
||||
{
|
||||
return layers_;
|
||||
}
|
||||
|
||||
std::span<const AnimationFrame> CanvasDocument::frames() const noexcept
|
||||
{
|
||||
return frames_;
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view name)
|
||||
{
|
||||
if (layers_.size() >= max_layer_count) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("document layer count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
Layer layer;
|
||||
if (name.empty()) {
|
||||
layer.name = default_layer_name(layers_.size());
|
||||
} else {
|
||||
const auto name_status = validate_layer_name(name);
|
||||
if (!name_status.ok()) {
|
||||
return pp::foundation::Result<std::size_t>::failure(name_status);
|
||||
}
|
||||
layer.name = std::string(name);
|
||||
}
|
||||
layer.frames = frames_;
|
||||
layers_.push_back(layer);
|
||||
active_layer_index_ = layers_.size() - 1U;
|
||||
return pp::foundation::Result<std::size_t>::success(active_layer_index_);
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::remove_layer(std::size_t index)
|
||||
{
|
||||
if (index >= layers_.size()) {
|
||||
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||
}
|
||||
|
||||
if (layers_.size() == 1U) {
|
||||
return pp::foundation::Status::invalid_argument("document must keep at least one layer");
|
||||
}
|
||||
|
||||
layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
if (active_layer_index_ >= layers_.size()) {
|
||||
active_layer_index_ = layers_.size() - 1U;
|
||||
} else if (active_layer_index_ > index) {
|
||||
--active_layer_index_;
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::move_layer(std::size_t from, std::size_t to)
|
||||
{
|
||||
if (from >= layers_.size() || to >= layers_.size()) {
|
||||
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||
}
|
||||
|
||||
if (from == to) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
auto layer = layers_[from];
|
||||
layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(from));
|
||||
layers_.insert(layers_.begin() + static_cast<std::ptrdiff_t>(to), layer);
|
||||
|
||||
if (active_layer_index_ == from) {
|
||||
active_layer_index_ = to;
|
||||
} else if (from < active_layer_index_ && active_layer_index_ <= to) {
|
||||
--active_layer_index_;
|
||||
} else if (to <= active_layer_index_ && active_layer_index_ < from) {
|
||||
++active_layer_index_;
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_active_layer(std::size_t index) noexcept
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
active_layer_index_ = index;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::rename_layer(std::size_t index, std::string_view name)
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
const auto name_status = validate_layer_name(name);
|
||||
if (!name_status.ok()) {
|
||||
return name_status;
|
||||
}
|
||||
|
||||
layers_[index].name = std::string(name);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_layer_visible(std::size_t index, bool visible) noexcept
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
layers_[index].visible = visible;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_layer_alpha_locked(std::size_t index, bool alpha_locked) noexcept
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
layers_[index].alpha_locked = alpha_locked;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_layer_opacity(std::size_t index, float opacity) noexcept
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
const auto opacity_status = validate_layer_opacity(opacity);
|
||||
if (!opacity_status.ok()) {
|
||||
return opacity_status;
|
||||
}
|
||||
|
||||
layers_[index].opacity = opacity;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
const auto blend_status = validate_blend_mode(blend_mode);
|
||||
if (!blend_status.ok()) {
|
||||
return blend_status;
|
||||
}
|
||||
|
||||
layers_[index].blend_mode = blend_mode;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t duration_ms)
|
||||
{
|
||||
if (frames_.size() >= max_frame_count) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
const auto duration_status = validate_frame_duration(duration_ms);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
duration_status);
|
||||
}
|
||||
|
||||
frames_.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
|
||||
for (auto& layer : layers_) {
|
||||
layer.frames.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
|
||||
}
|
||||
active_frame_index_ = frames_.size() - 1U;
|
||||
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::size_t> CanvasDocument::duplicate_frame(std::size_t index)
|
||||
{
|
||||
const auto index_status = validate_frame_index(index, frames_.size());
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
index_status);
|
||||
}
|
||||
|
||||
if (frames_.size() >= max_frame_count) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
const auto insert_at = index + 1U;
|
||||
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(insert_at), frames_[index]);
|
||||
for (auto& layer : layers_) {
|
||||
if (index < layer.frames.size()) {
|
||||
layer.frames.insert(
|
||||
layer.frames.begin() + static_cast<std::ptrdiff_t>(insert_at),
|
||||
layer.frames[index]);
|
||||
}
|
||||
}
|
||||
active_frame_index_ = insert_at;
|
||||
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::remove_frame(std::size_t index)
|
||||
{
|
||||
const auto index_status = validate_frame_index(index, frames_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
if (frames_.size() == 1U) {
|
||||
return pp::foundation::Status::invalid_argument("document must keep at least one frame");
|
||||
}
|
||||
|
||||
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
for (auto& layer : layers_) {
|
||||
if (index < layer.frames.size() && layer.frames.size() > 1U) {
|
||||
layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
}
|
||||
}
|
||||
if (active_frame_index_ >= frames_.size()) {
|
||||
active_frame_index_ = frames_.size() - 1U;
|
||||
} else if (active_frame_index_ > index) {
|
||||
--active_frame_index_;
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::move_frame(std::size_t from, std::size_t to)
|
||||
{
|
||||
if (from >= frames_.size() || to >= frames_.size()) {
|
||||
return pp::foundation::Status::out_of_range("frame index is outside the document");
|
||||
}
|
||||
|
||||
if (from == to) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
const auto frame = frames_[from];
|
||||
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(from));
|
||||
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(to), frame);
|
||||
for (auto& layer : layers_) {
|
||||
if (from < layer.frames.size() && to < layer.frames.size()) {
|
||||
const auto layer_frame = layer.frames[from];
|
||||
layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(from));
|
||||
layer.frames.insert(layer.frames.begin() + static_cast<std::ptrdiff_t>(to), layer_frame);
|
||||
}
|
||||
}
|
||||
|
||||
if (active_frame_index_ == from) {
|
||||
active_frame_index_ = to;
|
||||
} else if (from < active_frame_index_ && active_frame_index_ <= to) {
|
||||
--active_frame_index_;
|
||||
} else if (to <= active_frame_index_ && active_frame_index_ < from) {
|
||||
++active_frame_index_;
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept
|
||||
{
|
||||
const auto index_status = validate_frame_index(index, frames_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
const auto duration_status = validate_frame_duration(duration_ms);
|
||||
if (!duration_status.ok()) {
|
||||
return duration_status;
|
||||
}
|
||||
|
||||
frames_[index].duration_ms = duration_ms;
|
||||
for (auto& layer : layers_) {
|
||||
if (index < layer.frames.size()) {
|
||||
layer.frames[index].duration_ms = duration_ms;
|
||||
}
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_active_frame(std::size_t index) noexcept
|
||||
{
|
||||
const auto index_status = validate_frame_index(index, frames_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
active_frame_index_ = index;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_layer_frame_face_pixels(
|
||||
std::size_t layer_index,
|
||||
std::size_t frame_index,
|
||||
LayerFacePixels pixels)
|
||||
{
|
||||
const auto layer_status = validate_layer_index(layer_index, layers_.size());
|
||||
if (!layer_status.ok()) {
|
||||
return layer_status;
|
||||
}
|
||||
|
||||
const auto frame_status = validate_frame_index(frame_index, layers_[layer_index].frames.size());
|
||||
if (!frame_status.ok()) {
|
||||
return frame_status;
|
||||
}
|
||||
|
||||
const auto pixels_status = validate_face_pixels(pixels, width_, height_);
|
||||
if (!pixels_status.ok()) {
|
||||
return pixels_status;
|
||||
}
|
||||
|
||||
auto& faces = layers_[layer_index].frames[frame_index].face_pixels;
|
||||
const auto existing = std::find_if(
|
||||
faces.begin(),
|
||||
faces.end(),
|
||||
[face_index = pixels.face_index](const LayerFacePixels& face) {
|
||||
return face.face_index == face_index;
|
||||
});
|
||||
if (existing == faces.end()) {
|
||||
faces.push_back(std::move(pixels));
|
||||
} else {
|
||||
*existing = std::move(pixels);
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<DocumentHistory> DocumentHistory::create(
|
||||
CanvasDocument initial_document,
|
||||
std::size_t max_entries)
|
||||
{
|
||||
if (max_entries < min_document_history_entries) {
|
||||
return pp::foundation::Result<DocumentHistory>::failure(
|
||||
pp::foundation::Status::invalid_argument("document history must keep at least two entries"));
|
||||
}
|
||||
|
||||
if (max_entries > max_document_history_entries) {
|
||||
return pp::foundation::Result<DocumentHistory>::failure(
|
||||
pp::foundation::Status::out_of_range("document history entry limit exceeds the configured limit"));
|
||||
}
|
||||
|
||||
DocumentHistory history;
|
||||
history.max_entries_ = max_entries;
|
||||
history.entries_.reserve(max_entries);
|
||||
history.entries_.push_back(initial_document);
|
||||
return pp::foundation::Result<DocumentHistory>::success(history);
|
||||
}
|
||||
|
||||
const CanvasDocument& DocumentHistory::current() const noexcept
|
||||
{
|
||||
return entries_[current_index_];
|
||||
}
|
||||
|
||||
std::size_t DocumentHistory::size() const noexcept
|
||||
{
|
||||
return entries_.size();
|
||||
}
|
||||
|
||||
std::size_t DocumentHistory::current_index() const noexcept
|
||||
{
|
||||
return current_index_;
|
||||
}
|
||||
|
||||
bool DocumentHistory::can_undo() const noexcept
|
||||
{
|
||||
return current_index_ > 0;
|
||||
}
|
||||
|
||||
bool DocumentHistory::can_redo() const noexcept
|
||||
{
|
||||
return current_index_ + 1U < entries_.size();
|
||||
}
|
||||
|
||||
pp::foundation::Status DocumentHistory::apply(CanvasDocument next_document)
|
||||
{
|
||||
if (entries_.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("document history is not initialized");
|
||||
}
|
||||
|
||||
if (can_redo()) {
|
||||
entries_.erase(entries_.begin() + static_cast<std::ptrdiff_t>(current_index_ + 1U), entries_.end());
|
||||
}
|
||||
|
||||
entries_.push_back(next_document);
|
||||
if (entries_.size() > max_entries_) {
|
||||
entries_.erase(entries_.begin());
|
||||
} else {
|
||||
++current_index_;
|
||||
}
|
||||
|
||||
current_index_ = entries_.size() - 1U;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status DocumentHistory::undo() noexcept
|
||||
{
|
||||
if (!can_undo()) {
|
||||
return pp::foundation::Status::out_of_range("document history has no undo entry");
|
||||
}
|
||||
|
||||
--current_index_;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status DocumentHistory::redo() noexcept
|
||||
{
|
||||
if (!can_redo()) {
|
||||
return pp::foundation::Status::out_of_range("document history has no redo entry");
|
||||
}
|
||||
|
||||
++current_index_;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
137
src/document/document.h
Normal file
137
src/document/document.h
Normal file
@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
#include "paint/blend.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::document {
|
||||
|
||||
constexpr std::uint32_t max_canvas_dimension = 131072;
|
||||
constexpr std::uint32_t max_layer_count = 1024;
|
||||
constexpr std::uint32_t max_frame_count = 100000;
|
||||
constexpr std::uint32_t min_frame_duration_ms = 1;
|
||||
constexpr std::size_t min_document_history_entries = 2;
|
||||
constexpr std::size_t max_document_history_entries = 10000;
|
||||
constexpr std::size_t max_layer_name_length = 128;
|
||||
constexpr std::uint32_t cube_face_count = 6;
|
||||
constexpr std::uint32_t rgba8_components = 4;
|
||||
constexpr std::uint64_t max_face_pixel_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
|
||||
|
||||
struct DocumentConfig {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t layer_count = 1;
|
||||
};
|
||||
|
||||
struct LayerFacePixels {
|
||||
std::uint32_t face_index = 0;
|
||||
std::uint32_t x = 0;
|
||||
std::uint32_t y = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::vector<std::uint8_t> rgba8;
|
||||
};
|
||||
|
||||
struct AnimationFrame {
|
||||
std::uint32_t duration_ms = 100;
|
||||
std::vector<LayerFacePixels> face_pixels;
|
||||
};
|
||||
|
||||
struct Layer {
|
||||
std::string name;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
float opacity = 1.0F;
|
||||
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
||||
std::vector<AnimationFrame> frames;
|
||||
};
|
||||
|
||||
struct DocumentLayerConfig {
|
||||
std::string_view name;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
float opacity = 1.0F;
|
||||
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
||||
std::span<const AnimationFrame> frames;
|
||||
};
|
||||
|
||||
struct DocumentSnapshotConfig {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::span<const DocumentLayerConfig> layers;
|
||||
std::span<const AnimationFrame> frames;
|
||||
};
|
||||
|
||||
class CanvasDocument {
|
||||
public:
|
||||
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create(DocumentConfig config);
|
||||
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create_from_snapshot(DocumentSnapshotConfig config);
|
||||
|
||||
[[nodiscard]] std::uint32_t width() const noexcept;
|
||||
[[nodiscard]] std::uint32_t height() const noexcept;
|
||||
[[nodiscard]] std::size_t active_layer_index() const noexcept;
|
||||
[[nodiscard]] std::size_t active_frame_index() const noexcept;
|
||||
[[nodiscard]] std::uint64_t animation_duration_ms() const noexcept;
|
||||
[[nodiscard]] pp::foundation::Result<std::uint64_t> layer_animation_duration_ms(std::size_t index) const noexcept;
|
||||
[[nodiscard]] std::size_t face_pixel_payload_count() const noexcept;
|
||||
[[nodiscard]] std::span<const Layer> layers() const noexcept;
|
||||
[[nodiscard]] std::span<const AnimationFrame> frames() const noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> add_layer(std::string_view name);
|
||||
[[nodiscard]] pp::foundation::Status remove_layer(std::size_t index);
|
||||
[[nodiscard]] pp::foundation::Status move_layer(std::size_t from, std::size_t to);
|
||||
[[nodiscard]] pp::foundation::Status set_active_layer(std::size_t index) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status rename_layer(std::size_t index, std::string_view name);
|
||||
[[nodiscard]] pp::foundation::Status set_layer_visible(std::size_t index, bool visible) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_layer_alpha_locked(std::size_t index, bool alpha_locked) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_layer_opacity(std::size_t index, float opacity) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> add_frame(std::uint32_t duration_ms);
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> duplicate_frame(std::size_t index);
|
||||
[[nodiscard]] pp::foundation::Status remove_frame(std::size_t index);
|
||||
[[nodiscard]] pp::foundation::Status move_frame(std::size_t from, std::size_t to);
|
||||
[[nodiscard]] pp::foundation::Status set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_active_frame(std::size_t index) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_layer_frame_face_pixels(
|
||||
std::size_t layer_index,
|
||||
std::size_t frame_index,
|
||||
LayerFacePixels pixels);
|
||||
|
||||
private:
|
||||
std::uint32_t width_ = 0;
|
||||
std::uint32_t height_ = 0;
|
||||
std::size_t active_layer_index_ = 0;
|
||||
std::size_t active_frame_index_ = 0;
|
||||
std::vector<Layer> layers_;
|
||||
std::vector<AnimationFrame> frames_;
|
||||
};
|
||||
|
||||
class DocumentHistory {
|
||||
public:
|
||||
[[nodiscard]] static pp::foundation::Result<DocumentHistory> create(
|
||||
CanvasDocument initial_document,
|
||||
std::size_t max_entries = 256);
|
||||
|
||||
[[nodiscard]] const CanvasDocument& current() const noexcept;
|
||||
[[nodiscard]] std::size_t size() const noexcept;
|
||||
[[nodiscard]] std::size_t current_index() const noexcept;
|
||||
[[nodiscard]] bool can_undo() const noexcept;
|
||||
[[nodiscard]] bool can_redo() const noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Status apply(CanvasDocument next_document);
|
||||
[[nodiscard]] pp::foundation::Status undo() noexcept;
|
||||
[[nodiscard]] pp::foundation::Status redo() noexcept;
|
||||
|
||||
private:
|
||||
std::size_t max_entries_ = 0;
|
||||
std::size_t current_index_ = 0;
|
||||
std::vector<CanvasDocument> entries_;
|
||||
};
|
||||
|
||||
}
|
||||
116
src/document/ppi_import.cpp
Normal file
116
src/document/ppi_import.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "document/ppi_import.h"
|
||||
|
||||
#include <utility>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::document {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<pp::paint::BlendMode> ppi_layer_blend_mode(
|
||||
std::uint32_t blend_mode) noexcept
|
||||
{
|
||||
switch (blend_mode) {
|
||||
case 0:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::normal);
|
||||
case 1:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::multiply);
|
||||
case 2:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::screen);
|
||||
case 3:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::color_dodge);
|
||||
case 4:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::overlay);
|
||||
default:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI layer blend mode is not supported by pp_document"));
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<CanvasDocument> document_from_ppi_index(
|
||||
const pp::assets::PpiProjectIndex& project)
|
||||
{
|
||||
if (project.body.layers.empty()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI project has no layers"));
|
||||
}
|
||||
|
||||
const auto& reference_frames = project.body.layers.front().frames;
|
||||
if (reference_frames.empty()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI project has no frames"));
|
||||
}
|
||||
|
||||
std::vector<AnimationFrame> frames;
|
||||
frames.reserve(reference_frames.size());
|
||||
for (const auto& frame : reference_frames) {
|
||||
frames.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
|
||||
}
|
||||
|
||||
std::vector<std::vector<AnimationFrame>> layer_frames;
|
||||
layer_frames.reserve(project.body.layers.size());
|
||||
std::vector<DocumentLayerConfig> layers;
|
||||
layers.reserve(project.body.layers.size());
|
||||
for (const auto& layer : project.body.layers) {
|
||||
const auto blend_mode = ppi_layer_blend_mode(layer.blend_mode);
|
||||
if (!blend_mode) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(blend_mode.status());
|
||||
}
|
||||
|
||||
auto& frame_list = layer_frames.emplace_back();
|
||||
frame_list.reserve(layer.frames.size());
|
||||
for (const auto& frame : layer.frames) {
|
||||
frame_list.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
|
||||
}
|
||||
|
||||
layers.push_back(DocumentLayerConfig {
|
||||
.name = layer.name,
|
||||
.visible = layer.visible,
|
||||
.alpha_locked = layer.alpha_locked,
|
||||
.opacity = layer.opacity,
|
||||
.blend_mode = blend_mode.value(),
|
||||
.frames = std::span<const AnimationFrame>(frame_list.data(), frame_list.size()),
|
||||
});
|
||||
}
|
||||
|
||||
return CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = project.body.summary.width,
|
||||
.height = project.body.summary.height,
|
||||
.layers = layers,
|
||||
.frames = frames,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<CanvasDocument> import_ppi_project_document(
|
||||
const pp::assets::PpiDecodedProjectImages& project)
|
||||
{
|
||||
auto document = document_from_ppi_index(project.project);
|
||||
if (!document) {
|
||||
return document;
|
||||
}
|
||||
|
||||
auto value = document.value();
|
||||
for (const auto& face : project.faces) {
|
||||
const auto status = value.set_layer_frame_face_pixels(
|
||||
face.layer_index,
|
||||
face.frame_index,
|
||||
LayerFacePixels {
|
||||
.face_index = face.face_index,
|
||||
.x = face.descriptor.x0,
|
||||
.y = face.descriptor.y0,
|
||||
.width = face.image.width,
|
||||
.height = face.image.height,
|
||||
.rgba8 = face.image.pixels,
|
||||
});
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(status);
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasDocument>::success(std::move(value));
|
||||
}
|
||||
|
||||
}
|
||||
11
src/document/ppi_import.h
Normal file
11
src/document/ppi_import.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "assets/ppi_header.h"
|
||||
#include "document/document.h"
|
||||
|
||||
namespace pp::document {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<CanvasDocument> import_ppi_project_document(
|
||||
const pp::assets::PpiDecodedProjectImages& project);
|
||||
|
||||
}
|
||||
142
src/foundation/binary_stream.cpp
Normal file
142
src/foundation/binary_stream.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "foundation/binary_stream.h"
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
ByteReader::ByteReader(std::span<const std::byte> bytes) noexcept
|
||||
: bytes_(bytes)
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t ByteReader::position() const noexcept
|
||||
{
|
||||
return position_;
|
||||
}
|
||||
|
||||
std::size_t ByteReader::size() const noexcept
|
||||
{
|
||||
return bytes_.size();
|
||||
}
|
||||
|
||||
std::size_t ByteReader::remaining() const noexcept
|
||||
{
|
||||
return bytes_.size() - position_;
|
||||
}
|
||||
|
||||
bool ByteReader::empty() const noexcept
|
||||
{
|
||||
return remaining() == 0;
|
||||
}
|
||||
|
||||
Status ByteReader::seek(std::size_t position) noexcept
|
||||
{
|
||||
if (position > bytes_.size()) {
|
||||
return Status::out_of_range("seek position is outside the stream");
|
||||
}
|
||||
|
||||
position_ = position;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Result<std::uint8_t> ByteReader::read_u8() noexcept
|
||||
{
|
||||
const auto bytes = read_bytes(1);
|
||||
if (!bytes) {
|
||||
return Result<std::uint8_t>::failure(bytes.status());
|
||||
}
|
||||
|
||||
return Result<std::uint8_t>::success(static_cast<std::uint8_t>(bytes.value()[0]));
|
||||
}
|
||||
|
||||
Result<std::uint16_t> ByteReader::read_u16_le() noexcept
|
||||
{
|
||||
const auto bytes = read_bytes(2);
|
||||
if (!bytes) {
|
||||
return Result<std::uint16_t>::failure(bytes.status());
|
||||
}
|
||||
|
||||
const auto b0 = static_cast<std::uint16_t>(bytes.value()[0]);
|
||||
const auto b1 = static_cast<std::uint16_t>(bytes.value()[1]);
|
||||
return Result<std::uint16_t>::success(static_cast<std::uint16_t>(b0 | (b1 << 8U)));
|
||||
}
|
||||
|
||||
Result<std::uint32_t> ByteReader::read_u32_le() noexcept
|
||||
{
|
||||
const auto bytes = read_bytes(4);
|
||||
if (!bytes) {
|
||||
return Result<std::uint32_t>::failure(bytes.status());
|
||||
}
|
||||
|
||||
const auto b0 = static_cast<std::uint32_t>(bytes.value()[0]);
|
||||
const auto b1 = static_cast<std::uint32_t>(bytes.value()[1]);
|
||||
const auto b2 = static_cast<std::uint32_t>(bytes.value()[2]);
|
||||
const auto b3 = static_cast<std::uint32_t>(bytes.value()[3]);
|
||||
return Result<std::uint32_t>::success(b0 | (b1 << 8U) | (b2 << 16U) | (b3 << 24U));
|
||||
}
|
||||
|
||||
Result<std::span<const std::byte>> ByteReader::read_bytes(std::size_t count) noexcept
|
||||
{
|
||||
if (count > remaining()) {
|
||||
return Result<std::span<const std::byte>>::failure(
|
||||
Status::out_of_range("read would move beyond the end of the stream"));
|
||||
}
|
||||
|
||||
const auto start = position_;
|
||||
position_ += count;
|
||||
return Result<std::span<const std::byte>>::success(bytes_.subspan(start, count));
|
||||
}
|
||||
|
||||
ByteWriter::ByteWriter(std::vector<std::byte>& bytes) noexcept
|
||||
: bytes_(&bytes)
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t ByteWriter::size() const noexcept
|
||||
{
|
||||
return bytes_ == nullptr ? 0 : bytes_->size();
|
||||
}
|
||||
|
||||
Status ByteWriter::write_u8(std::uint8_t value)
|
||||
{
|
||||
if (bytes_ == nullptr) {
|
||||
return Status::invalid_argument("writer has no backing storage");
|
||||
}
|
||||
|
||||
bytes_->push_back(static_cast<std::byte>(value));
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status ByteWriter::write_u16_le(std::uint16_t value)
|
||||
{
|
||||
if (bytes_ == nullptr) {
|
||||
return Status::invalid_argument("writer has no backing storage");
|
||||
}
|
||||
|
||||
bytes_->push_back(static_cast<std::byte>(value & 0xffU));
|
||||
bytes_->push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status ByteWriter::write_u32_le(std::uint32_t value)
|
||||
{
|
||||
if (bytes_ == nullptr) {
|
||||
return Status::invalid_argument("writer has no backing storage");
|
||||
}
|
||||
|
||||
bytes_->push_back(static_cast<std::byte>(value & 0xffU));
|
||||
bytes_->push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
||||
bytes_->push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
|
||||
bytes_->push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status ByteWriter::write_bytes(std::span<const std::byte> bytes)
|
||||
{
|
||||
if (bytes_ == nullptr) {
|
||||
return Status::invalid_argument("writer has no backing storage");
|
||||
}
|
||||
|
||||
bytes_->insert(bytes_->end(), bytes.begin(), bytes.end());
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
46
src/foundation/binary_stream.h
Normal file
46
src/foundation/binary_stream.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
class ByteReader {
|
||||
public:
|
||||
explicit ByteReader(std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] std::size_t position() const noexcept;
|
||||
[[nodiscard]] std::size_t size() const noexcept;
|
||||
[[nodiscard]] std::size_t remaining() const noexcept;
|
||||
[[nodiscard]] bool empty() const noexcept;
|
||||
|
||||
[[nodiscard]] Status seek(std::size_t position) noexcept;
|
||||
[[nodiscard]] Result<std::uint8_t> read_u8() noexcept;
|
||||
[[nodiscard]] Result<std::uint16_t> read_u16_le() noexcept;
|
||||
[[nodiscard]] Result<std::uint32_t> read_u32_le() noexcept;
|
||||
[[nodiscard]] Result<std::span<const std::byte>> read_bytes(std::size_t count) noexcept;
|
||||
|
||||
private:
|
||||
std::span<const std::byte> bytes_;
|
||||
std::size_t position_ = 0;
|
||||
};
|
||||
|
||||
class ByteWriter {
|
||||
public:
|
||||
explicit ByteWriter(std::vector<std::byte>& bytes) noexcept;
|
||||
|
||||
[[nodiscard]] std::size_t size() const noexcept;
|
||||
[[nodiscard]] Status write_u8(std::uint8_t value);
|
||||
[[nodiscard]] Status write_u16_le(std::uint16_t value);
|
||||
[[nodiscard]] Status write_u32_le(std::uint32_t value);
|
||||
[[nodiscard]] Status write_bytes(std::span<const std::byte> bytes);
|
||||
|
||||
private:
|
||||
std::vector<std::byte>* bytes_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
97
src/foundation/event.cpp
Normal file
97
src/foundation/event.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "foundation/event.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
EventDispatcher::EventDispatcher(std::size_t max_subscriptions) noexcept
|
||||
: max_subscriptions_(max_subscriptions)
|
||||
{
|
||||
subscriptions_.reserve(std::min(max_subscriptions_, max_event_subscriptions));
|
||||
}
|
||||
|
||||
std::size_t EventDispatcher::size() const noexcept
|
||||
{
|
||||
return subscriptions_.size();
|
||||
}
|
||||
|
||||
bool EventDispatcher::empty() const noexcept
|
||||
{
|
||||
return subscriptions_.empty();
|
||||
}
|
||||
|
||||
std::size_t EventDispatcher::max_subscriptions() const noexcept
|
||||
{
|
||||
return max_subscriptions_;
|
||||
}
|
||||
|
||||
Result<std::uint64_t> EventDispatcher::subscribe(std::uint32_t type, EventCallback callback, void* user_data)
|
||||
{
|
||||
if (max_subscriptions_ == 0U || max_subscriptions_ > max_event_subscriptions) {
|
||||
return Result<std::uint64_t>::failure(
|
||||
Status::out_of_range("event dispatcher capacity is outside the configured range"));
|
||||
}
|
||||
|
||||
if (type == 0U) {
|
||||
return Result<std::uint64_t>::failure(Status::invalid_argument("event type must not be zero"));
|
||||
}
|
||||
|
||||
if (callback == nullptr) {
|
||||
return Result<std::uint64_t>::failure(Status::invalid_argument("event callback must not be null"));
|
||||
}
|
||||
|
||||
if (subscriptions_.size() >= max_subscriptions_) {
|
||||
return Result<std::uint64_t>::failure(Status::out_of_range("event dispatcher is full"));
|
||||
}
|
||||
|
||||
const auto id = next_subscription_id_++;
|
||||
subscriptions_.push_back(EventSubscription {
|
||||
.id = id,
|
||||
.type = type,
|
||||
.callback = callback,
|
||||
.user_data = user_data,
|
||||
});
|
||||
|
||||
return Result<std::uint64_t>::success(id);
|
||||
}
|
||||
|
||||
Status EventDispatcher::unsubscribe(std::uint64_t subscription_id) noexcept
|
||||
{
|
||||
const auto found = std::find_if(
|
||||
subscriptions_.begin(),
|
||||
subscriptions_.end(),
|
||||
[subscription_id](const EventSubscription& subscription) {
|
||||
return subscription.id == subscription_id;
|
||||
});
|
||||
|
||||
if (found == subscriptions_.end()) {
|
||||
return Status::out_of_range("event subscription id was not found");
|
||||
}
|
||||
|
||||
subscriptions_.erase(found);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
std::size_t EventDispatcher::publish(const Event& event) const noexcept
|
||||
{
|
||||
if (event.type == 0U) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::size_t delivered = 0;
|
||||
for (const auto& subscription : subscriptions_) {
|
||||
if (subscription.type == event.type) {
|
||||
subscription.callback(event, subscription.user_data);
|
||||
++delivered;
|
||||
}
|
||||
}
|
||||
|
||||
return delivered;
|
||||
}
|
||||
|
||||
void EventDispatcher::clear() noexcept
|
||||
{
|
||||
subscriptions_.clear();
|
||||
}
|
||||
|
||||
}
|
||||
48
src/foundation/event.h
Normal file
48
src/foundation/event.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
constexpr std::size_t max_event_subscriptions = 65536;
|
||||
|
||||
struct Event {
|
||||
std::uint32_t type = 0;
|
||||
std::uint64_t source_id = 0;
|
||||
std::uint64_t frame_id = 0;
|
||||
std::uint64_t payload_u64 = 0;
|
||||
};
|
||||
|
||||
using EventCallback = void (*)(const Event& event, void* user_data) noexcept;
|
||||
|
||||
struct EventSubscription {
|
||||
std::uint64_t id = 0;
|
||||
std::uint32_t type = 0;
|
||||
EventCallback callback = nullptr;
|
||||
void* user_data = nullptr;
|
||||
};
|
||||
|
||||
class EventDispatcher {
|
||||
public:
|
||||
explicit EventDispatcher(std::size_t max_subscriptions = max_event_subscriptions) noexcept;
|
||||
|
||||
[[nodiscard]] std::size_t size() const noexcept;
|
||||
[[nodiscard]] bool empty() const noexcept;
|
||||
[[nodiscard]] std::size_t max_subscriptions() const noexcept;
|
||||
|
||||
[[nodiscard]] Result<std::uint64_t> subscribe(std::uint32_t type, EventCallback callback, void* user_data);
|
||||
[[nodiscard]] Status unsubscribe(std::uint64_t subscription_id) noexcept;
|
||||
[[nodiscard]] std::size_t publish(const Event& event) const noexcept;
|
||||
void clear() noexcept;
|
||||
|
||||
private:
|
||||
std::size_t max_subscriptions_ = max_event_subscriptions;
|
||||
std::uint64_t next_subscription_id_ = 1;
|
||||
std::vector<EventSubscription> subscriptions_;
|
||||
};
|
||||
|
||||
}
|
||||
93
src/foundation/log.cpp
Normal file
93
src/foundation/log.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "foundation/log.h"
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool should_write(LogLevel level, LogLevel min_level) noexcept
|
||||
{
|
||||
return static_cast<std::uint8_t>(level) >= static_cast<std::uint8_t>(min_level);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Logger::Logger(ILogSink& sink) noexcept
|
||||
: sink_(&sink)
|
||||
{
|
||||
}
|
||||
|
||||
void Logger::set_min_level(LogLevel level) noexcept
|
||||
{
|
||||
min_level_ = level;
|
||||
}
|
||||
|
||||
LogLevel Logger::min_level() const noexcept
|
||||
{
|
||||
return min_level_;
|
||||
}
|
||||
|
||||
Status Logger::write(
|
||||
LogLevel level,
|
||||
std::string_view component,
|
||||
std::string_view message,
|
||||
std::uint64_t frame_id,
|
||||
std::uint64_t stroke_id,
|
||||
std::uint64_t thread_id) noexcept
|
||||
{
|
||||
if (component.empty()) {
|
||||
return Status::invalid_argument("log component must not be empty");
|
||||
}
|
||||
|
||||
if (message.empty()) {
|
||||
return Status::invalid_argument("log message must not be empty");
|
||||
}
|
||||
|
||||
if (!should_write(level, min_level_)) {
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
sink_->write(LogRecord {
|
||||
.level = level,
|
||||
.component = std::string(component),
|
||||
.message = std::string(message),
|
||||
.frame_id = frame_id,
|
||||
.stroke_id = stroke_id,
|
||||
.thread_id = thread_id,
|
||||
});
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
void MemoryLogSink::write(const LogRecord& record) noexcept
|
||||
{
|
||||
records_.push_back(record);
|
||||
}
|
||||
|
||||
const std::vector<LogRecord>& MemoryLogSink::records() const noexcept
|
||||
{
|
||||
return records_;
|
||||
}
|
||||
|
||||
void MemoryLogSink::clear() noexcept
|
||||
{
|
||||
records_.clear();
|
||||
}
|
||||
|
||||
const char* log_level_name(LogLevel level) noexcept
|
||||
{
|
||||
switch (level) {
|
||||
case LogLevel::trace:
|
||||
return "trace";
|
||||
case LogLevel::debug:
|
||||
return "debug";
|
||||
case LogLevel::info:
|
||||
return "info";
|
||||
case LogLevel::warning:
|
||||
return "warning";
|
||||
case LogLevel::error:
|
||||
return "error";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
67
src/foundation/log.h
Normal file
67
src/foundation/log.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
enum class LogLevel : std::uint8_t {
|
||||
trace,
|
||||
debug,
|
||||
info,
|
||||
warning,
|
||||
error,
|
||||
};
|
||||
|
||||
struct LogRecord {
|
||||
LogLevel level = LogLevel::info;
|
||||
std::string component;
|
||||
std::string message;
|
||||
std::uint64_t frame_id = 0;
|
||||
std::uint64_t stroke_id = 0;
|
||||
std::uint64_t thread_id = 0;
|
||||
};
|
||||
|
||||
class ILogSink {
|
||||
public:
|
||||
virtual ~ILogSink() = default;
|
||||
virtual void write(const LogRecord& record) noexcept = 0;
|
||||
};
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
explicit Logger(ILogSink& sink) noexcept;
|
||||
|
||||
void set_min_level(LogLevel level) noexcept;
|
||||
[[nodiscard]] LogLevel min_level() const noexcept;
|
||||
|
||||
[[nodiscard]] Status write(
|
||||
LogLevel level,
|
||||
std::string_view component,
|
||||
std::string_view message,
|
||||
std::uint64_t frame_id = 0,
|
||||
std::uint64_t stroke_id = 0,
|
||||
std::uint64_t thread_id = 0) noexcept;
|
||||
|
||||
private:
|
||||
ILogSink* sink_ = nullptr;
|
||||
LogLevel min_level_ = LogLevel::trace;
|
||||
};
|
||||
|
||||
class MemoryLogSink final : public ILogSink {
|
||||
public:
|
||||
void write(const LogRecord& record) noexcept override;
|
||||
[[nodiscard]] const std::vector<LogRecord>& records() const noexcept;
|
||||
void clear() noexcept;
|
||||
|
||||
private:
|
||||
std::vector<LogRecord> records_;
|
||||
};
|
||||
|
||||
[[nodiscard]] const char* log_level_name(LogLevel level) noexcept;
|
||||
|
||||
}
|
||||
37
src/foundation/parse.cpp
Normal file
37
src/foundation/parse.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "foundation/parse.h"
|
||||
|
||||
#include <charconv>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
Result<std::uint32_t> parse_u32(std::string_view text) noexcept
|
||||
{
|
||||
if (text.empty()) {
|
||||
return Result<std::uint32_t>::failure(
|
||||
Status::invalid_argument("value must not be empty"));
|
||||
}
|
||||
|
||||
if (text.front() == '-' || text.front() == '+') {
|
||||
return Result<std::uint32_t>::failure(
|
||||
Status::invalid_argument("value must be an unsigned integer without a sign"));
|
||||
}
|
||||
|
||||
std::uint32_t value = 0;
|
||||
const auto* begin = text.data();
|
||||
const auto* end = text.data() + text.size();
|
||||
const auto [ptr, ec] = std::from_chars(begin, end, value);
|
||||
|
||||
if (ec == std::errc::result_out_of_range) {
|
||||
return Result<std::uint32_t>::failure(
|
||||
Status::out_of_range("value is outside the uint32 range"));
|
||||
}
|
||||
|
||||
if (ec != std::errc {} || ptr != end) {
|
||||
return Result<std::uint32_t>::failure(
|
||||
Status::invalid_argument("value must contain only decimal digits"));
|
||||
}
|
||||
|
||||
return Result<std::uint32_t>::success(value);
|
||||
}
|
||||
|
||||
}
|
||||
12
src/foundation/parse.h
Normal file
12
src/foundation/parse.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
[[nodiscard]] Result<std::uint32_t> parse_u32(std::string_view text) noexcept;
|
||||
|
||||
}
|
||||
80
src/foundation/result.h
Normal file
80
src/foundation/result.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
enum class StatusCode {
|
||||
ok,
|
||||
invalid_argument,
|
||||
out_of_range,
|
||||
};
|
||||
|
||||
struct Status {
|
||||
StatusCode code = StatusCode::ok;
|
||||
const char* message = "ok";
|
||||
|
||||
[[nodiscard]] constexpr bool ok() const noexcept
|
||||
{
|
||||
return code == StatusCode::ok;
|
||||
}
|
||||
|
||||
[[nodiscard]] static constexpr Status success() noexcept
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] static constexpr Status invalid_argument(const char* message) noexcept
|
||||
{
|
||||
return { StatusCode::invalid_argument, message };
|
||||
}
|
||||
|
||||
[[nodiscard]] static constexpr Status out_of_range(const char* message) noexcept
|
||||
{
|
||||
return { StatusCode::out_of_range, message };
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class Result {
|
||||
public:
|
||||
[[nodiscard]] static constexpr Result success(T value) noexcept
|
||||
{
|
||||
return Result(value, Status::success());
|
||||
}
|
||||
|
||||
[[nodiscard]] static constexpr Result failure(Status status) noexcept
|
||||
{
|
||||
return Result(T{}, status);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool ok() const noexcept
|
||||
{
|
||||
return status_.ok();
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr explicit operator bool() const noexcept
|
||||
{
|
||||
return ok();
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr const T& value() const noexcept
|
||||
{
|
||||
return value_;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr Status status() const noexcept
|
||||
{
|
||||
return status_;
|
||||
}
|
||||
|
||||
private:
|
||||
constexpr Result(T value, Status status) noexcept
|
||||
: value_(value)
|
||||
, status_(status)
|
||||
{
|
||||
}
|
||||
|
||||
T value_{};
|
||||
Status status_{};
|
||||
};
|
||||
|
||||
}
|
||||
83
src/foundation/task_queue.cpp
Normal file
83
src/foundation/task_queue.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "foundation/task_queue.h"
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
TaskQueue::TaskQueue(std::size_t max_entries) noexcept
|
||||
: max_entries_(max_entries)
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t TaskQueue::size() const noexcept
|
||||
{
|
||||
return tasks_.size();
|
||||
}
|
||||
|
||||
bool TaskQueue::empty() const noexcept
|
||||
{
|
||||
return tasks_.empty();
|
||||
}
|
||||
|
||||
std::size_t TaskQueue::max_entries() const noexcept
|
||||
{
|
||||
return max_entries_;
|
||||
}
|
||||
|
||||
Status TaskQueue::push(TaskItem task)
|
||||
{
|
||||
if (max_entries_ == 0U || max_entries_ > max_task_queue_entries) {
|
||||
return Status::out_of_range("task queue capacity is outside the configured range");
|
||||
}
|
||||
|
||||
if (task.callback == nullptr) {
|
||||
return Status::invalid_argument("task callback must not be null");
|
||||
}
|
||||
|
||||
if (tasks_.size() >= max_entries_) {
|
||||
return Status::out_of_range("task queue is full");
|
||||
}
|
||||
|
||||
tasks_.push_back(task);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Result<TaskItem> TaskQueue::pop() noexcept
|
||||
{
|
||||
if (tasks_.empty()) {
|
||||
return Result<TaskItem>::failure(Status::out_of_range("task queue is empty"));
|
||||
}
|
||||
|
||||
const auto task = tasks_.front();
|
||||
tasks_.pop_front();
|
||||
return Result<TaskItem>::success(task);
|
||||
}
|
||||
|
||||
Status TaskQueue::run_next() noexcept
|
||||
{
|
||||
auto task = pop();
|
||||
if (!task.ok()) {
|
||||
return task.status();
|
||||
}
|
||||
|
||||
task.value().callback(task.value().user_data);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
std::size_t TaskQueue::run_all() noexcept
|
||||
{
|
||||
std::size_t count = 0;
|
||||
while (!tasks_.empty()) {
|
||||
const auto status = run_next();
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
}
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void TaskQueue::clear() noexcept
|
||||
{
|
||||
tasks_.clear();
|
||||
}
|
||||
|
||||
}
|
||||
40
src/foundation/task_queue.h
Normal file
40
src/foundation/task_queue.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
constexpr std::size_t max_task_queue_entries = 65536;
|
||||
|
||||
using TaskCallback = void (*)(void* user_data) noexcept;
|
||||
|
||||
struct TaskItem {
|
||||
TaskCallback callback = nullptr;
|
||||
void* user_data = nullptr;
|
||||
std::uint64_t id = 0;
|
||||
};
|
||||
|
||||
class TaskQueue {
|
||||
public:
|
||||
explicit TaskQueue(std::size_t max_entries = max_task_queue_entries) noexcept;
|
||||
|
||||
[[nodiscard]] std::size_t size() const noexcept;
|
||||
[[nodiscard]] bool empty() const noexcept;
|
||||
[[nodiscard]] std::size_t max_entries() const noexcept;
|
||||
|
||||
[[nodiscard]] Status push(TaskItem task);
|
||||
[[nodiscard]] Result<TaskItem> pop() noexcept;
|
||||
[[nodiscard]] Status run_next() noexcept;
|
||||
[[nodiscard]] std::size_t run_all() noexcept;
|
||||
void clear() noexcept;
|
||||
|
||||
private:
|
||||
std::size_t max_entries_ = max_task_queue_entries;
|
||||
std::deque<TaskItem> tasks_;
|
||||
};
|
||||
|
||||
}
|
||||
98
src/foundation/trace.cpp
Normal file
98
src/foundation/trace.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
#include "foundation/trace.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
Result<TraceSpanId> TraceRecorder::begin_span(TraceSpanDesc desc, std::uint64_t start_us)
|
||||
{
|
||||
if (desc.component.empty()) {
|
||||
return Result<TraceSpanId>::failure(
|
||||
Status::invalid_argument("trace component must not be empty"));
|
||||
}
|
||||
|
||||
if (desc.name.empty()) {
|
||||
return Result<TraceSpanId>::failure(
|
||||
Status::invalid_argument("trace span name must not be empty"));
|
||||
}
|
||||
|
||||
if (next_id_ == std::numeric_limits<TraceSpanId>::max()) {
|
||||
return Result<TraceSpanId>::failure(
|
||||
Status::out_of_range("trace span id space is exhausted"));
|
||||
}
|
||||
|
||||
const auto id = next_id_++;
|
||||
ActiveSpan span;
|
||||
span.id = id;
|
||||
span.component.assign(desc.component);
|
||||
span.name.assign(desc.name);
|
||||
span.desc = desc;
|
||||
span.desc.component = span.component;
|
||||
span.desc.name = span.name;
|
||||
span.start_us = start_us;
|
||||
span.active = true;
|
||||
active_spans_.push_back(span);
|
||||
|
||||
return Result<TraceSpanId>::success(id);
|
||||
}
|
||||
|
||||
Status TraceRecorder::end_span(TraceSpanId id, std::uint64_t end_us)
|
||||
{
|
||||
ActiveSpan* span = find_active_span(id);
|
||||
if (span == nullptr) {
|
||||
return Status::out_of_range("trace span id is not active");
|
||||
}
|
||||
|
||||
if (end_us < span->start_us) {
|
||||
return Status::invalid_argument("trace span cannot end before it starts");
|
||||
}
|
||||
|
||||
TraceEvent event;
|
||||
event.component = span->component;
|
||||
event.name = span->name;
|
||||
event.thread_id = span->desc.thread_id;
|
||||
event.frame_id = span->desc.frame_id;
|
||||
event.stroke_id = span->desc.stroke_id;
|
||||
event.start_us = span->start_us;
|
||||
event.duration_us = end_us - span->start_us;
|
||||
events_.push_back(event);
|
||||
|
||||
span->active = false;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
std::span<const TraceEvent> TraceRecorder::events() const noexcept
|
||||
{
|
||||
return events_;
|
||||
}
|
||||
|
||||
std::size_t TraceRecorder::active_span_count() const noexcept
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (const auto& span : active_spans_) {
|
||||
if (span.active) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void TraceRecorder::clear() noexcept
|
||||
{
|
||||
active_spans_.clear();
|
||||
events_.clear();
|
||||
next_id_ = 1;
|
||||
}
|
||||
|
||||
TraceRecorder::ActiveSpan* TraceRecorder::find_active_span(TraceSpanId id) noexcept
|
||||
{
|
||||
for (auto& span : active_spans_) {
|
||||
if (span.active && span.id == id) {
|
||||
return &span;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
60
src/foundation/trace.h
Normal file
60
src/foundation/trace.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::foundation {
|
||||
|
||||
using TraceSpanId = std::uint64_t;
|
||||
|
||||
struct TraceSpanDesc {
|
||||
std::string_view component;
|
||||
std::string_view name;
|
||||
std::uint64_t thread_id = 0;
|
||||
std::uint64_t frame_id = 0;
|
||||
std::uint64_t stroke_id = 0;
|
||||
};
|
||||
|
||||
struct TraceEvent {
|
||||
std::string component;
|
||||
std::string name;
|
||||
std::uint64_t thread_id = 0;
|
||||
std::uint64_t frame_id = 0;
|
||||
std::uint64_t stroke_id = 0;
|
||||
std::uint64_t start_us = 0;
|
||||
std::uint64_t duration_us = 0;
|
||||
};
|
||||
|
||||
class TraceRecorder {
|
||||
public:
|
||||
[[nodiscard]] Result<TraceSpanId> begin_span(TraceSpanDesc desc, std::uint64_t start_us);
|
||||
[[nodiscard]] Status end_span(TraceSpanId id, std::uint64_t end_us);
|
||||
|
||||
[[nodiscard]] std::span<const TraceEvent> events() const noexcept;
|
||||
[[nodiscard]] std::size_t active_span_count() const noexcept;
|
||||
|
||||
void clear() noexcept;
|
||||
|
||||
private:
|
||||
struct ActiveSpan {
|
||||
TraceSpanId id = 0;
|
||||
TraceSpanDesc desc;
|
||||
std::string component;
|
||||
std::string name;
|
||||
std::uint64_t start_us = 0;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] ActiveSpan* find_active_span(TraceSpanId id) noexcept;
|
||||
|
||||
std::vector<ActiveSpan> active_spans_;
|
||||
std::vector<TraceEvent> events_;
|
||||
TraceSpanId next_id_ = 1;
|
||||
};
|
||||
|
||||
}
|
||||
22
src/main.cpp
22
src/main.cpp
@@ -19,7 +19,10 @@
|
||||
#include "abr.h"
|
||||
#include "settings.h"
|
||||
|
||||
#if __has_include(<renderdoc_app.h>)
|
||||
#include <renderdoc_app.h>
|
||||
#define USE_RENDERDOC
|
||||
#endif
|
||||
|
||||
#include <iomanip>
|
||||
#include <ctime>
|
||||
@@ -33,7 +36,7 @@ HINSTANCE hInst;
|
||||
HWND hWnd;
|
||||
HDC hDC;
|
||||
HGLRC hRC;
|
||||
wchar_t* className;
|
||||
const wchar_t* className;
|
||||
bool keys[256];
|
||||
std::mutex gl_mutex;
|
||||
std::mutex async_mutex;
|
||||
@@ -54,6 +57,7 @@ float timer_ink_touch = 0;
|
||||
float timer_ink_pen = 0;
|
||||
bool sandboxed = false;
|
||||
|
||||
#ifdef USE_RENDERDOC
|
||||
RENDERDOC_API_1_4_0* rdoc_api = NULL;
|
||||
bool win32_renderdoc_init()
|
||||
{
|
||||
@@ -78,6 +82,10 @@ void win32_renderdoc_frame_end()
|
||||
if (rdoc_api)
|
||||
rdoc_api->EndFrameCapture(NULL, NULL);
|
||||
}
|
||||
#else
|
||||
void win32_renderdoc_frame_start() { }
|
||||
void win32_renderdoc_frame_end() { }
|
||||
#endif
|
||||
|
||||
HRESULT(*GetDpiForMonitor_fn)(HMONITOR hmonitor, MONITOR_DPI_TYPE dpiType, UINT* dpiX, UINT* dpiY);
|
||||
HRESULT(*SetProcessDpiAwareness_fn)(PROCESS_DPI_AWARENESS value);
|
||||
@@ -367,7 +375,7 @@ int read_WMI_info()
|
||||
}
|
||||
|
||||
IWbemServices* pService = NULL;
|
||||
if (FAILED(hRes = pLocator->ConnectServer(L"root\\CIMV2", NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
|
||||
if (FAILED(hRes = pLocator->ConnectServer(BSTR(L"root\\CIMV2"), NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
|
||||
{
|
||||
pLocator->Release();
|
||||
LOG("Unable to connect to \"CIMV2\": %x", hRes);
|
||||
@@ -411,7 +419,7 @@ int read_WMI_info()
|
||||
// GET DEVICE INFO
|
||||
{
|
||||
IEnumWbemClassObject* pEnumerator = NULL;
|
||||
if (FAILED(hRes = pService->ExecQuery(L"WQL", L"SELECT * FROM Win32_ComputerSystem", WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
|
||||
if (FAILED(hRes = pService->ExecQuery(BSTR(L"WQL"), BSTR(L"SELECT * FROM Win32_ComputerSystem"), WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
|
||||
{
|
||||
pLocator->Release();
|
||||
pService->Release();
|
||||
@@ -438,7 +446,7 @@ int read_WMI_info()
|
||||
// GET OS INFO
|
||||
{
|
||||
IEnumWbemClassObject* pEnumerator = NULL;
|
||||
if (FAILED(hRes = pService->ExecQuery(L"WQL", L"SELECT * FROM Win32_OperatingSystem", WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
|
||||
if (FAILED(hRes = pService->ExecQuery(BSTR(L"WQL"), BSTR(L"SELECT * FROM Win32_OperatingSystem"), WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
|
||||
{
|
||||
pLocator->Release();
|
||||
pService->Release();
|
||||
@@ -468,7 +476,7 @@ int read_WMI_info()
|
||||
|
||||
pService->Release();
|
||||
pService = NULL;
|
||||
if (FAILED(hRes = pLocator->ConnectServer(L"root\\Microsoft\\Windows\\DeviceGuard", NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
|
||||
if (FAILED(hRes = pLocator->ConnectServer(BSTR(L"root\\Microsoft\\Windows\\DeviceGuard"), NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
|
||||
{
|
||||
pLocator->Release();
|
||||
LOG("Unable to connect to \"DeviceGuard\": %x", hRes);
|
||||
@@ -478,7 +486,7 @@ int read_WMI_info()
|
||||
// GET DEVICE GUARD
|
||||
{
|
||||
IEnumWbemClassObject* pEnumerator = NULL;
|
||||
if (FAILED(hRes = pService->ExecQuery(L"WQL", L"SELECT * FROM Win32_DeviceGuard", WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
|
||||
if (FAILED(hRes = pService->ExecQuery(BSTR(L"WQL"), BSTR(L"SELECT * FROM Win32_DeviceGuard"), WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
|
||||
{
|
||||
pLocator->Release();
|
||||
pService->Release();
|
||||
@@ -954,8 +962,10 @@ int main(int argc, char** argv)
|
||||
LOG("GL vendor: %s", glGetString(GL_VENDOR));
|
||||
LOG("GL renderer: %s", glGetString(GL_RENDERER));
|
||||
|
||||
#ifdef USE_RENDERDOC
|
||||
if (!win32_renderdoc_init())
|
||||
LOG("Renderdoc not started");
|
||||
#endif // USE_RENDERDOC
|
||||
|
||||
swprintf_s(window_title, L"PanoPainter %s (%s)", g_version_number_w,
|
||||
str2wstr((char*)glGetString(GL_RENDERER)).c_str());
|
||||
|
||||
109
src/paint/blend.cpp
Normal file
109
src/paint/blend.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "paint/blend.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] float saturate(float value) noexcept
|
||||
{
|
||||
if (!std::isfinite(value)) {
|
||||
return value < 0.0F ? 0.0F : 1.0F;
|
||||
}
|
||||
|
||||
return std::clamp(value, 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
[[nodiscard]] float mix(float a, float b, float t) noexcept
|
||||
{
|
||||
return a * (1.0F - t) + b * t;
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_channel(float base, float stroke, BlendMode mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
case BlendMode::normal:
|
||||
return stroke;
|
||||
case BlendMode::multiply:
|
||||
return base * stroke;
|
||||
case BlendMode::screen:
|
||||
return 1.0F - (1.0F - base) * (1.0F - stroke);
|
||||
case BlendMode::color_dodge:
|
||||
if (stroke >= 1.0F) {
|
||||
return 1.0F;
|
||||
}
|
||||
return saturate(base / (1.0F - stroke));
|
||||
case BlendMode::overlay:
|
||||
return base < 0.5F
|
||||
? 2.0F * base * stroke
|
||||
: 1.0F - 2.0F * (1.0F - base) * (1.0F - stroke);
|
||||
}
|
||||
|
||||
return stroke;
|
||||
}
|
||||
|
||||
[[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) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
const auto stroke_weight = stroke_alpha / alpha_total;
|
||||
const auto base_weight = base_alpha / alpha_total;
|
||||
if (mode == BlendMode::normal) {
|
||||
return saturate(mix(base, stroke, stroke_weight));
|
||||
}
|
||||
|
||||
const auto mode_value = blend_channel(base, stroke, mode);
|
||||
return saturate(mix(stroke, mix(base, mode_value, stroke_weight), base_weight));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rgba blend_pixels(Rgba base, Rgba stroke, BlendMode mode) noexcept
|
||||
{
|
||||
base.r = saturate(base.r);
|
||||
base.g = saturate(base.g);
|
||||
base.b = saturate(base.b);
|
||||
base.a = saturate(base.a);
|
||||
stroke.r = saturate(stroke.r);
|
||||
stroke.g = saturate(stroke.g);
|
||||
stroke.b = saturate(stroke.b);
|
||||
stroke.a = saturate(stroke.a);
|
||||
|
||||
if (stroke.a == 0.0F) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const auto contribution = (1.0F - base.a) * stroke.a;
|
||||
const auto alpha_total = saturate(base.a + contribution);
|
||||
|
||||
return {
|
||||
blend_rgb(base.r, stroke.r, base.a, stroke.a, alpha_total, mode),
|
||||
blend_rgb(base.g, stroke.g, base.a, stroke.a, alpha_total, mode),
|
||||
blend_rgb(base.b, stroke.b, base.a, stroke.a, alpha_total, mode),
|
||||
alpha_total,
|
||||
};
|
||||
}
|
||||
|
||||
const char* blend_mode_name(BlendMode mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
case BlendMode::normal:
|
||||
return "normal";
|
||||
case BlendMode::multiply:
|
||||
return "multiply";
|
||||
case BlendMode::screen:
|
||||
return "screen";
|
||||
case BlendMode::color_dodge:
|
||||
return "color_dodge";
|
||||
case BlendMode::overlay:
|
||||
return "overlay";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
25
src/paint/blend.h
Normal file
25
src/paint/blend.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
enum class BlendMode : std::uint8_t {
|
||||
normal,
|
||||
multiply,
|
||||
screen,
|
||||
color_dodge,
|
||||
overlay,
|
||||
};
|
||||
|
||||
struct Rgba {
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
float b = 0.0F;
|
||||
float a = 0.0F;
|
||||
};
|
||||
|
||||
[[nodiscard]] Rgba blend_pixels(Rgba base, Rgba stroke, BlendMode mode) noexcept;
|
||||
[[nodiscard]] const char* blend_mode_name(BlendMode mode) noexcept;
|
||||
|
||||
}
|
||||
72
src/paint/brush.cpp
Normal file
72
src/paint/brush.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "paint/brush.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool finite_in_range(float value, float min, float max) noexcept
|
||||
{
|
||||
return std::isfinite(value) && value >= min && value <= max;
|
||||
}
|
||||
|
||||
[[nodiscard]] float clamp01(float value) noexcept
|
||||
{
|
||||
return std::clamp(value, 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_brush_params(const BrushParams& params) noexcept
|
||||
{
|
||||
if (!finite_in_range(params.size, min_brush_size, max_brush_size)) {
|
||||
return pp::foundation::Status::out_of_range("brush size is outside the configured range");
|
||||
}
|
||||
|
||||
if (!finite_in_range(params.spacing, min_brush_spacing, max_brush_spacing)) {
|
||||
return pp::foundation::Status::out_of_range("brush spacing is outside the configured range");
|
||||
}
|
||||
|
||||
if (!finite_in_range(params.opacity, 0.0F, 1.0F)) {
|
||||
return pp::foundation::Status::out_of_range("brush opacity must be finite and within 0..1");
|
||||
}
|
||||
|
||||
if (!finite_in_range(params.flow, 0.0F, 1.0F)) {
|
||||
return pp::foundation::Status::out_of_range("brush flow must be finite and within 0..1");
|
||||
}
|
||||
|
||||
if (!finite_in_range(params.angle_degrees, -max_brush_angle_degrees, max_brush_angle_degrees)) {
|
||||
return pp::foundation::Status::out_of_range("brush angle is outside the configured range");
|
||||
}
|
||||
|
||||
if (!finite_in_range(params.size_jitter, 0.0F, 1.0F)) {
|
||||
return pp::foundation::Status::out_of_range("brush size jitter must be finite and within 0..1");
|
||||
}
|
||||
|
||||
if (!finite_in_range(params.opacity_jitter, 0.0F, 1.0F)) {
|
||||
return pp::foundation::Status::out_of_range("brush opacity jitter must be finite and within 0..1");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
BrushStamp evaluate_brush_stamp(const BrushParams& params, float pressure) noexcept
|
||||
{
|
||||
const auto clamped_pressure = clamp01(std::isfinite(pressure) ? pressure : 0.0F);
|
||||
const auto size_pressure = params.pressure_controls_size ? clamped_pressure : 1.0F;
|
||||
const auto opacity_pressure = params.pressure_controls_opacity ? clamped_pressure : 1.0F;
|
||||
|
||||
const auto jitter_size_scale = 1.0F - (params.size_jitter * 0.5F);
|
||||
const auto jitter_opacity_scale = 1.0F - (params.opacity_jitter * 0.5F);
|
||||
|
||||
return BrushStamp {
|
||||
.size = std::max(min_brush_size, params.size * size_pressure * jitter_size_scale),
|
||||
.opacity = clamp01(params.opacity * opacity_pressure * jitter_opacity_scale),
|
||||
.flow = clamp01(params.flow),
|
||||
.angle_degrees = params.angle_degrees,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
37
src/paint/brush.h
Normal file
37
src/paint/brush.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
constexpr float min_brush_size = 0.1F;
|
||||
constexpr float max_brush_size = 4096.0F;
|
||||
constexpr float min_brush_spacing = 0.01F;
|
||||
constexpr float max_brush_spacing = 16.0F;
|
||||
constexpr float max_brush_angle_degrees = 360.0F;
|
||||
|
||||
struct BrushParams {
|
||||
float size = 32.0F;
|
||||
float spacing = 0.25F;
|
||||
float opacity = 1.0F;
|
||||
float flow = 1.0F;
|
||||
float angle_degrees = 0.0F;
|
||||
float size_jitter = 0.0F;
|
||||
float opacity_jitter = 0.0F;
|
||||
bool pressure_controls_size = true;
|
||||
bool pressure_controls_opacity = false;
|
||||
};
|
||||
|
||||
struct BrushStamp {
|
||||
float size = 0.0F;
|
||||
float opacity = 0.0F;
|
||||
float flow = 0.0F;
|
||||
float angle_degrees = 0.0F;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_brush_params(const BrushParams& params) noexcept;
|
||||
[[nodiscard]] BrushStamp evaluate_brush_stamp(const BrushParams& params, float pressure) noexcept;
|
||||
|
||||
}
|
||||
162
src/paint/stroke.cpp
Normal file
162
src/paint/stroke.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include "paint/stroke.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool is_finite_point(const StrokePoint& point) noexcept
|
||||
{
|
||||
return std::isfinite(point.x) && std::isfinite(point.y) && std::isfinite(point.pressure);
|
||||
}
|
||||
|
||||
[[nodiscard]] float clamp_pressure(float pressure) noexcept
|
||||
{
|
||||
return std::clamp(pressure, 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
[[nodiscard]] float distance_between(const StrokePoint& a, const StrokePoint& b) noexcept
|
||||
{
|
||||
const auto dx = b.x - a.x;
|
||||
const auto dy = b.y - a.y;
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
[[nodiscard]] StrokeSample interpolate_sample(
|
||||
const StrokePoint& a,
|
||||
const StrokePoint& b,
|
||||
float t,
|
||||
float distance) noexcept
|
||||
{
|
||||
return StrokeSample {
|
||||
.x = a.x + ((b.x - a.x) * t),
|
||||
.y = a.y + ((b.y - a.y) * t),
|
||||
.pressure = clamp_pressure(a.pressure + ((b.pressure - a.pressure) * t)),
|
||||
.distance = distance,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_input(
|
||||
std::span<const StrokePoint> points,
|
||||
StrokeSamplingConfig config) noexcept
|
||||
{
|
||||
if (points.size() < 2U) {
|
||||
return pp::foundation::Status::invalid_argument("stroke sampling requires at least two points");
|
||||
}
|
||||
|
||||
if (points.size() > max_stroke_points) {
|
||||
return pp::foundation::Status::out_of_range("stroke point count exceeds the configured limit");
|
||||
}
|
||||
|
||||
if (!std::isfinite(config.spacing) || config.spacing <= 0.0F) {
|
||||
return pp::foundation::Status::invalid_argument("stroke spacing must be finite and greater than zero");
|
||||
}
|
||||
|
||||
if (config.max_samples == 0U || config.max_samples > max_stroke_samples) {
|
||||
return pp::foundation::Status::out_of_range("stroke sample count limit is outside the configured range");
|
||||
}
|
||||
|
||||
for (const auto& point : points) {
|
||||
if (!is_finite_point(point)) {
|
||||
return pp::foundation::Status::invalid_argument("stroke points must contain finite coordinates and pressure");
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status append_sample(
|
||||
std::vector<StrokeSample>& samples,
|
||||
StrokeSample sample,
|
||||
std::size_t max_samples)
|
||||
{
|
||||
if (samples.size() >= max_samples) {
|
||||
return pp::foundation::Status::out_of_range("stroke sampling exceeded the configured sample limit");
|
||||
}
|
||||
|
||||
samples.push_back(sample);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::vector<StrokeSample>> sample_stroke(
|
||||
std::span<const StrokePoint> points,
|
||||
StrokeSamplingConfig config) noexcept
|
||||
{
|
||||
const auto input_status = validate_input(points, config);
|
||||
if (!input_status.ok()) {
|
||||
return pp::foundation::Result<std::vector<StrokeSample>>::failure(input_status);
|
||||
}
|
||||
|
||||
std::vector<StrokeSample> samples;
|
||||
samples.reserve(std::min<std::size_t>(points.size(), config.max_samples));
|
||||
|
||||
auto status = append_sample(
|
||||
samples,
|
||||
StrokeSample {
|
||||
.x = points.front().x,
|
||||
.y = points.front().y,
|
||||
.pressure = clamp_pressure(points.front().pressure),
|
||||
.distance = 0.0F,
|
||||
},
|
||||
config.max_samples);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<std::vector<StrokeSample>>::failure(status);
|
||||
}
|
||||
|
||||
float segment_start_distance = 0.0F;
|
||||
float next_sample_distance = config.spacing;
|
||||
float total_distance = 0.0F;
|
||||
|
||||
for (std::size_t i = 1; i < points.size(); ++i) {
|
||||
const auto& a = points[i - 1U];
|
||||
const auto& b = points[i];
|
||||
const auto segment_length = distance_between(a, b);
|
||||
if (segment_length <= 0.0F) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto segment_end_distance = segment_start_distance + segment_length;
|
||||
while (next_sample_distance <= segment_end_distance) {
|
||||
const auto t = (next_sample_distance - segment_start_distance) / segment_length;
|
||||
status = append_sample(
|
||||
samples,
|
||||
interpolate_sample(a, b, t, next_sample_distance),
|
||||
config.max_samples);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<std::vector<StrokeSample>>::failure(status);
|
||||
}
|
||||
next_sample_distance += config.spacing;
|
||||
}
|
||||
|
||||
segment_start_distance = segment_end_distance;
|
||||
total_distance = segment_end_distance;
|
||||
}
|
||||
|
||||
if (total_distance <= 0.0F) {
|
||||
return pp::foundation::Result<std::vector<StrokeSample>>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke path must have nonzero length"));
|
||||
}
|
||||
|
||||
if (config.include_endpoint && samples.back().distance < total_distance) {
|
||||
status = append_sample(
|
||||
samples,
|
||||
StrokeSample {
|
||||
.x = points.back().x,
|
||||
.y = points.back().y,
|
||||
.pressure = clamp_pressure(points.back().pressure),
|
||||
.distance = total_distance,
|
||||
},
|
||||
config.max_samples);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<std::vector<StrokeSample>>::failure(status);
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::vector<StrokeSample>>::success(samples);
|
||||
}
|
||||
|
||||
}
|
||||
39
src/paint/stroke.h
Normal file
39
src/paint/stroke.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
constexpr std::size_t max_stroke_points = 1000000;
|
||||
constexpr std::size_t max_stroke_samples = 1000000;
|
||||
|
||||
struct StrokePoint {
|
||||
float x = 0.0F;
|
||||
float y = 0.0F;
|
||||
float pressure = 1.0F;
|
||||
std::uint32_t time_ms = 0;
|
||||
};
|
||||
|
||||
struct StrokeSample {
|
||||
float x = 0.0F;
|
||||
float y = 0.0F;
|
||||
float pressure = 1.0F;
|
||||
float distance = 0.0F;
|
||||
};
|
||||
|
||||
struct StrokeSamplingConfig {
|
||||
float spacing = 1.0F;
|
||||
bool include_endpoint = true;
|
||||
std::size_t max_samples = max_stroke_samples;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::vector<StrokeSample>> sample_stroke(
|
||||
std::span<const StrokePoint> points,
|
||||
StrokeSamplingConfig config) noexcept;
|
||||
|
||||
}
|
||||
210
src/paint/stroke_script.cpp
Normal file
210
src/paint/stroke_script.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "paint/stroke_script.h"
|
||||
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] std::string_view trim(std::string_view text) noexcept
|
||||
{
|
||||
while (!text.empty() && (text.front() == ' ' || text.front() == '\t' || text.front() == '\r')) {
|
||||
text.remove_prefix(1);
|
||||
}
|
||||
|
||||
while (!text.empty() && (text.back() == ' ' || text.back() == '\t' || text.back() == '\r')) {
|
||||
text.remove_suffix(1);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view strip_comment(std::string_view line) noexcept
|
||||
{
|
||||
const auto comment = line.find('#');
|
||||
if (comment == std::string_view::npos) {
|
||||
return line;
|
||||
}
|
||||
|
||||
return line.substr(0, comment);
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<float> parse_float_token(std::string_view token) noexcept
|
||||
{
|
||||
token = trim(token);
|
||||
if (token.empty() || token.size() >= 64U) {
|
||||
return pp::foundation::Result<float>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script numeric token is invalid"));
|
||||
}
|
||||
|
||||
std::array<char, 64> buffer {};
|
||||
for (std::size_t i = 0; i < token.size(); ++i) {
|
||||
buffer[i] = token[i];
|
||||
}
|
||||
|
||||
char* end = nullptr;
|
||||
errno = 0;
|
||||
const auto value = std::strtof(buffer.data(), &end);
|
||||
if (errno != 0 || end != buffer.data() + static_cast<std::ptrdiff_t>(token.size()) || !std::isfinite(value)) {
|
||||
return pp::foundation::Result<float>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script numeric token is invalid"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<float>::success(value);
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> split_tokens(
|
||||
std::string_view line,
|
||||
std::array<std::string_view, 8>& tokens) noexcept
|
||||
{
|
||||
std::size_t count = 0;
|
||||
std::size_t offset = 0;
|
||||
while (offset < line.size()) {
|
||||
while (offset < line.size() && (line[offset] == ' ' || line[offset] == '\t')) {
|
||||
++offset;
|
||||
}
|
||||
|
||||
if (offset >= line.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto token_start = offset;
|
||||
while (offset < line.size() && line[offset] != ' ' && line[offset] != '\t') {
|
||||
++offset;
|
||||
}
|
||||
|
||||
if (count >= tokens.size()) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script line has too many tokens"));
|
||||
}
|
||||
|
||||
tokens[count] = line.substr(token_start, offset - token_start);
|
||||
++count;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(count);
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<StrokeScriptStroke> parse_stroke_line(std::string_view line) noexcept
|
||||
{
|
||||
std::array<std::string_view, 8> tokens {};
|
||||
const auto token_count = split_tokens(line, tokens);
|
||||
if (!token_count) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(token_count.status());
|
||||
}
|
||||
|
||||
if (token_count.value() != tokens.size() || tokens[0] != "stroke") {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script line must be 'stroke x1 y1 p1 x2 y2 p2 spacing'"));
|
||||
}
|
||||
|
||||
const auto x1 = parse_float_token(tokens[1]);
|
||||
const auto y1 = parse_float_token(tokens[2]);
|
||||
const auto p1 = parse_float_token(tokens[3]);
|
||||
const auto x2 = parse_float_token(tokens[4]);
|
||||
const auto y2 = parse_float_token(tokens[5]);
|
||||
const auto p2 = parse_float_token(tokens[6]);
|
||||
const auto spacing = parse_float_token(tokens[7]);
|
||||
if (!x1) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(x1.status());
|
||||
}
|
||||
if (!y1) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(y1.status());
|
||||
}
|
||||
if (!p1) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(p1.status());
|
||||
}
|
||||
if (!x2) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(x2.status());
|
||||
}
|
||||
if (!y2) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(y2.status());
|
||||
}
|
||||
if (!p2) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(p2.status());
|
||||
}
|
||||
if (!spacing) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(spacing.status());
|
||||
}
|
||||
|
||||
if (spacing.value() <= 0.0F) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script spacing must be greater than zero"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<StrokeScriptStroke>::success(StrokeScriptStroke {
|
||||
.start = StrokePoint {
|
||||
.x = x1.value(),
|
||||
.y = y1.value(),
|
||||
.pressure = p1.value(),
|
||||
},
|
||||
.end = StrokePoint {
|
||||
.x = x2.value(),
|
||||
.y = y2.value(),
|
||||
.pressure = p2.value(),
|
||||
},
|
||||
.spacing = spacing.value(),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<StrokeScript> parse_stroke_script(std::string_view text)
|
||||
{
|
||||
if (text.empty()) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script must not be empty"));
|
||||
}
|
||||
|
||||
if (text.size() > max_stroke_script_bytes) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::out_of_range("stroke script exceeds the configured size limit"));
|
||||
}
|
||||
|
||||
StrokeScript script;
|
||||
std::size_t offset = 0;
|
||||
while (offset <= text.size()) {
|
||||
const auto line_start = offset;
|
||||
const auto line_end = text.find('\n', line_start);
|
||||
if (line_end == std::string_view::npos) {
|
||||
offset = text.size() + 1U;
|
||||
} else {
|
||||
offset = line_end + 1U;
|
||||
}
|
||||
|
||||
auto line = text.substr(line_start, (line_end == std::string_view::npos) ? std::string_view::npos : line_end - line_start);
|
||||
if (line.size() > max_stroke_script_line_length) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::out_of_range("stroke script line exceeds the configured length limit"));
|
||||
}
|
||||
|
||||
line = trim(strip_comment(line));
|
||||
if (line.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (script.strokes.size() >= max_stroke_script_strokes) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::out_of_range("stroke script stroke count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
const auto stroke = parse_stroke_line(line);
|
||||
if (!stroke) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(stroke.status());
|
||||
}
|
||||
|
||||
script.strokes.push_back(stroke.value());
|
||||
}
|
||||
|
||||
if (script.strokes.empty()) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script must contain at least one stroke"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<StrokeScript>::success(script);
|
||||
}
|
||||
|
||||
}
|
||||
28
src/paint/stroke_script.h
Normal file
28
src/paint/stroke_script.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
#include "paint/stroke.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
constexpr std::size_t max_stroke_script_bytes = 1024 * 1024;
|
||||
constexpr std::size_t max_stroke_script_line_length = 512;
|
||||
constexpr std::size_t max_stroke_script_strokes = 10000;
|
||||
|
||||
struct StrokeScriptStroke {
|
||||
StrokePoint start;
|
||||
StrokePoint end;
|
||||
float spacing = 1.0F;
|
||||
};
|
||||
|
||||
struct StrokeScript {
|
||||
std::vector<StrokeScriptStroke> strokes;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<StrokeScript> parse_stroke_script(std::string_view text);
|
||||
|
||||
}
|
||||
65
src/paint_renderer/compositor.cpp
Normal file
65
src/paint_renderer/compositor.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#include "paint_renderer/compositor.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace pp::paint_renderer {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> expected_pixel_count(pp::renderer::Extent2D extent) noexcept
|
||||
{
|
||||
const auto extent_status = pp::renderer::validate_extent(extent);
|
||||
if (!extent_status.ok()) {
|
||||
return pp::foundation::Result<std::size_t>::failure(extent_status);
|
||||
}
|
||||
|
||||
const auto width = static_cast<std::uint64_t>(extent.width);
|
||||
const auto height = static_cast<std::uint64_t>(extent.height);
|
||||
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("pixel count overflows uint64"));
|
||||
}
|
||||
|
||||
const auto count = width * height;
|
||||
if (count > 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("pixel count exceeds addressable memory"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(count));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Status composite_layer(
|
||||
std::span<pp::paint::Rgba> destination,
|
||||
pp::renderer::Extent2D extent,
|
||||
LayerCompositeView layer) noexcept
|
||||
{
|
||||
const auto pixel_count = expected_pixel_count(extent);
|
||||
if (!pixel_count) {
|
||||
return pixel_count.status();
|
||||
}
|
||||
|
||||
if (destination.size() != pixel_count.value() || layer.pixels.size() != pixel_count.value()) {
|
||||
return pp::foundation::Status::invalid_argument("composite buffers must match the render extent");
|
||||
}
|
||||
|
||||
if (layer.opacity < 0.0F || layer.opacity > 1.0F) {
|
||||
return pp::foundation::Status::out_of_range("layer opacity must be between 0 and 1");
|
||||
}
|
||||
|
||||
if (!layer.visible || layer.opacity == 0.0F) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < destination.size(); ++i) {
|
||||
auto stroke = layer.pixels[i];
|
||||
stroke.a *= layer.opacity;
|
||||
destination[i] = pp::paint::blend_pixels(destination[i], stroke, layer.blend_mode);
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
23
src/paint_renderer/compositor.h
Normal file
23
src/paint_renderer/compositor.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
#include "paint/blend.h"
|
||||
#include "renderer_api/renderer_api.h"
|
||||
|
||||
#include <span>
|
||||
|
||||
namespace pp::paint_renderer {
|
||||
|
||||
struct LayerCompositeView {
|
||||
std::span<const pp::paint::Rgba> pixels;
|
||||
float opacity = 1.0F;
|
||||
bool visible = true;
|
||||
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Status composite_layer(
|
||||
std::span<pp::paint::Rgba> destination,
|
||||
pp::renderer::Extent2D extent,
|
||||
LayerCompositeView layer) noexcept;
|
||||
|
||||
}
|
||||
@@ -64,8 +64,12 @@
|
||||
#elif _WIN32
|
||||
|
||||
#define _USE_MATH_DEFINES
|
||||
#ifndef _CRT_SECURE_NO_WARNINGS
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
#endif
|
||||
#ifndef _SCL_SECURE_NO_WARNINGS
|
||||
#define _SCL_SECURE_NO_WARNINGS
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
#include <tchar.h>
|
||||
@@ -138,6 +142,7 @@
|
||||
#include <regex>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
227
src/renderer_api/renderer_api.cpp
Normal file
227
src/renderer_api/renderer_api.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
#include "renderer_api/renderer_api.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace pp::renderer {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool is_empty_c_string(const char* text) noexcept
|
||||
{
|
||||
return text == nullptr || text[0] == '\0';
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_shader_stage_source(
|
||||
ShaderStageSource source,
|
||||
const char* stage_name) noexcept
|
||||
{
|
||||
if (is_empty_c_string(source.entry_point)) {
|
||||
return pp::foundation::Status::invalid_argument(stage_name);
|
||||
}
|
||||
|
||||
if (source.source == nullptr || source.source_size == 0U) {
|
||||
return pp::foundation::Status::invalid_argument("shader source must not be empty");
|
||||
}
|
||||
|
||||
if (source.source_size > max_shader_source_bytes) {
|
||||
return pp::foundation::Status::out_of_range("shader source exceeds the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::uint32_t bytes_per_pixel(TextureFormat format) noexcept
|
||||
{
|
||||
switch (format) {
|
||||
case TextureFormat::rgba8:
|
||||
return 4;
|
||||
case TextureFormat::r8:
|
||||
return 1;
|
||||
case TextureFormat::depth24_stencil8:
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_extent(Extent2D extent) noexcept
|
||||
{
|
||||
if (extent.width == 0 || extent.height == 0) {
|
||||
return pp::foundation::Status::invalid_argument("texture extent must be greater than zero");
|
||||
}
|
||||
|
||||
if (extent.width > max_texture_dimension || extent.height > max_texture_dimension) {
|
||||
return pp::foundation::Status::out_of_range("texture extent exceeds the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::uint64_t> texture_byte_size(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);
|
||||
}
|
||||
|
||||
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);
|
||||
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"));
|
||||
}
|
||||
|
||||
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("texture 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("texture byte size exceeds the configured limit"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::uint64_t>::success(bytes);
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept
|
||||
{
|
||||
const auto extent_status = validate_extent(target_extent);
|
||||
if (!extent_status.ok()) {
|
||||
return extent_status;
|
||||
}
|
||||
|
||||
if (viewport.x < 0 || viewport.y < 0) {
|
||||
return pp::foundation::Status::invalid_argument("viewport origin must be non-negative");
|
||||
}
|
||||
|
||||
if (viewport.width == 0 || viewport.height == 0) {
|
||||
return pp::foundation::Status::invalid_argument("viewport size must be greater than zero");
|
||||
}
|
||||
|
||||
if (!std::isfinite(viewport.min_depth) || !std::isfinite(viewport.max_depth)) {
|
||||
return pp::foundation::Status::invalid_argument("viewport depth range must be finite");
|
||||
}
|
||||
|
||||
if (viewport.min_depth < 0.0F || viewport.max_depth > 1.0F || viewport.min_depth > viewport.max_depth) {
|
||||
return pp::foundation::Status::out_of_range("viewport depth range must be within 0..1 and ordered");
|
||||
}
|
||||
|
||||
const auto x = static_cast<std::uint32_t>(viewport.x);
|
||||
const auto y = static_cast<std::uint32_t>(viewport.y);
|
||||
if (x > target_extent.width || y > target_extent.height) {
|
||||
return pp::foundation::Status::out_of_range("viewport origin is outside the render target");
|
||||
}
|
||||
|
||||
if (viewport.width > target_extent.width - x || viewport.height > target_extent.height - y) {
|
||||
return pp::foundation::Status::out_of_range("viewport exceeds render target bounds");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept
|
||||
{
|
||||
if (desc.vertex_count == 0) {
|
||||
return pp::foundation::Status::invalid_argument("mesh must contain at least one vertex");
|
||||
}
|
||||
|
||||
if (desc.vertex_count > max_mesh_vertices || desc.index_count > max_mesh_vertices) {
|
||||
return pp::foundation::Status::out_of_range("mesh vertex or index count exceeds the configured limit");
|
||||
}
|
||||
|
||||
switch (desc.topology) {
|
||||
case PrimitiveTopology::triangles:
|
||||
case PrimitiveTopology::triangle_strip:
|
||||
case PrimitiveTopology::lines:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("mesh topology is not supported");
|
||||
}
|
||||
|
||||
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 vertex_status = validate_shader_stage_source(
|
||||
desc.vertex,
|
||||
"vertex shader entry point must not be empty");
|
||||
if (!vertex_status.ok()) {
|
||||
return vertex_status;
|
||||
}
|
||||
|
||||
const auto fragment_status = validate_shader_stage_source(
|
||||
desc.fragment,
|
||||
"fragment shader entry point must not be empty");
|
||||
if (!fragment_status.ok()) {
|
||||
return fragment_status;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (region.width == 0 || region.height == 0) {
|
||||
return pp::foundation::Status::invalid_argument("readback region must be greater than zero");
|
||||
}
|
||||
|
||||
if (region.x > desc.extent.width || region.y > desc.extent.height) {
|
||||
return pp::foundation::Status::out_of_range("readback origin is outside the texture");
|
||||
}
|
||||
|
||||
if (region.width > desc.extent.width - region.x || region.height > desc.extent.height - region.y) {
|
||||
return pp::foundation::Status::out_of_range("readback region exceeds texture bounds");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
const char* texture_format_name(TextureFormat format) noexcept
|
||||
{
|
||||
switch (format) {
|
||||
case TextureFormat::rgba8:
|
||||
return "rgba8";
|
||||
case TextureFormat::r8:
|
||||
return "r8";
|
||||
case TextureFormat::depth24_stencil8:
|
||||
return "depth24_stencil8";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* primitive_topology_name(PrimitiveTopology topology) noexcept
|
||||
{
|
||||
switch (topology) {
|
||||
case PrimitiveTopology::triangles:
|
||||
return "triangles";
|
||||
case PrimitiveTopology::triangle_strip:
|
||||
return "triangle_strip";
|
||||
case PrimitiveTopology::lines:
|
||||
return "lines";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
146
src/renderer_api/renderer_api.h
Normal file
146
src/renderer_api/renderer_api.h
Normal file
@@ -0,0 +1,146 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace pp::renderer {
|
||||
|
||||
constexpr std::uint32_t max_texture_dimension = 32768;
|
||||
constexpr std::uint32_t max_mesh_vertices = 16777216;
|
||||
constexpr std::uint64_t max_texture_bytes = 1024ULL * 1024ULL * 1024ULL;
|
||||
constexpr std::size_t max_shader_source_bytes = 4ULL * 1024ULL * 1024ULL;
|
||||
|
||||
enum class TextureFormat : std::uint8_t {
|
||||
rgba8,
|
||||
r8,
|
||||
depth24_stencil8,
|
||||
};
|
||||
|
||||
struct Extent2D {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
};
|
||||
|
||||
struct TextureDesc {
|
||||
Extent2D extent;
|
||||
TextureFormat format = TextureFormat::rgba8;
|
||||
bool render_target = false;
|
||||
};
|
||||
|
||||
struct ReadbackRegion {
|
||||
std::uint32_t x = 0;
|
||||
std::uint32_t y = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
};
|
||||
|
||||
struct Viewport {
|
||||
std::int32_t x = 0;
|
||||
std::int32_t y = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
float min_depth = 0.0F;
|
||||
float max_depth = 1.0F;
|
||||
};
|
||||
|
||||
struct ClearColor {
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
float b = 0.0F;
|
||||
float a = 0.0F;
|
||||
};
|
||||
|
||||
enum class PrimitiveTopology : std::uint8_t {
|
||||
triangles,
|
||||
triangle_strip,
|
||||
lines,
|
||||
};
|
||||
|
||||
struct MeshDesc {
|
||||
std::uint32_t vertex_count = 0;
|
||||
std::uint32_t index_count = 0;
|
||||
PrimitiveTopology topology = PrimitiveTopology::triangles;
|
||||
};
|
||||
|
||||
struct ShaderStageSource {
|
||||
const char* entry_point = "main";
|
||||
const char* source = nullptr;
|
||||
std::size_t source_size = 0;
|
||||
};
|
||||
|
||||
struct ShaderProgramDesc {
|
||||
const char* debug_name = "";
|
||||
ShaderStageSource vertex;
|
||||
ShaderStageSource fragment;
|
||||
};
|
||||
|
||||
class ITexture2D {
|
||||
public:
|
||||
virtual ~ITexture2D() = default;
|
||||
[[nodiscard]] virtual TextureDesc desc() const noexcept = 0;
|
||||
};
|
||||
|
||||
class IRenderTarget {
|
||||
public:
|
||||
virtual ~IRenderTarget() = default;
|
||||
[[nodiscard]] virtual TextureDesc color_desc() const noexcept = 0;
|
||||
};
|
||||
|
||||
class IShaderProgram {
|
||||
public:
|
||||
virtual ~IShaderProgram() = default;
|
||||
[[nodiscard]] virtual const char* debug_name() const noexcept = 0;
|
||||
};
|
||||
|
||||
class IMesh {
|
||||
public:
|
||||
virtual ~IMesh() = default;
|
||||
[[nodiscard]] virtual MeshDesc desc() const noexcept = 0;
|
||||
};
|
||||
|
||||
class IReadbackBuffer {
|
||||
public:
|
||||
virtual ~IReadbackBuffer() = default;
|
||||
[[nodiscard]] virtual std::uint64_t size_bytes() const noexcept = 0;
|
||||
};
|
||||
|
||||
class IRenderTrace {
|
||||
public:
|
||||
virtual ~IRenderTrace() = default;
|
||||
virtual void marker(const char* component, const char* name) noexcept = 0;
|
||||
};
|
||||
|
||||
class ICommandContext {
|
||||
public:
|
||||
virtual ~ICommandContext() = default;
|
||||
[[nodiscard]] virtual pp::foundation::Status begin_render_pass(
|
||||
IRenderTarget& target,
|
||||
ClearColor clear_color) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status set_viewport(Viewport viewport) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status bind_mesh(IMesh& mesh) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status draw() noexcept = 0;
|
||||
virtual void end_render_pass() noexcept = 0;
|
||||
};
|
||||
|
||||
class IRenderDevice {
|
||||
public:
|
||||
virtual ~IRenderDevice() = default;
|
||||
[[nodiscard]] virtual const char* backend_name() const 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]] pp::foundation::Status validate_extent(Extent2D extent) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Result<std::uint64_t> texture_byte_size(TextureDesc desc) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept;
|
||||
[[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept;
|
||||
[[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept;
|
||||
|
||||
}
|
||||
95
src/ui_core/color.cpp
Normal file
95
src/ui_core/color.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include "ui_core/color.h"
|
||||
|
||||
namespace pp::ui {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int hex_value(char ch) noexcept
|
||||
{
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
return ch - '0';
|
||||
}
|
||||
if (ch >= 'a' && ch <= 'f') {
|
||||
return 10 + (ch - 'a');
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'F') {
|
||||
return 10 + (ch - 'A');
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::uint8_t> parse_hex_byte(std::string_view value) noexcept
|
||||
{
|
||||
const auto high = hex_value(value[0]);
|
||||
const auto low = hex_value(value[1]);
|
||||
if (high < 0 || low < 0) {
|
||||
return pp::foundation::Result<std::uint8_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("color contains a non-hex character"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::uint8_t>::success(
|
||||
static_cast<std::uint8_t>((high << 4) | low));
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::uint8_t> parse_hex_nibble(char value) noexcept
|
||||
{
|
||||
const auto nibble = hex_value(value);
|
||||
if (nibble < 0) {
|
||||
return pp::foundation::Result<std::uint8_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("color contains a non-hex character"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::uint8_t>::success(
|
||||
static_cast<std::uint8_t>((nibble << 4) | nibble));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<ColorRgba8> parse_hex_color(std::string_view value) noexcept
|
||||
{
|
||||
if (value.empty()) {
|
||||
return pp::foundation::Result<ColorRgba8>::failure(
|
||||
pp::foundation::Status::invalid_argument("color must not be empty"));
|
||||
}
|
||||
|
||||
if (value.front() != '#') {
|
||||
return pp::foundation::Result<ColorRgba8>::failure(
|
||||
pp::foundation::Status::invalid_argument("color must start with #"));
|
||||
}
|
||||
|
||||
const auto hex = value.substr(1);
|
||||
if (hex.size() != 3U && hex.size() != 4U && hex.size() != 6U && hex.size() != 8U) {
|
||||
return pp::foundation::Result<ColorRgba8>::failure(
|
||||
pp::foundation::Status::invalid_argument("color must use #rgb, #rgba, #rrggbb, or #rrggbbaa"));
|
||||
}
|
||||
|
||||
ColorRgba8 color;
|
||||
if (hex.size() == 3U || hex.size() == 4U) {
|
||||
const auto r = parse_hex_nibble(hex[0]);
|
||||
const auto g = parse_hex_nibble(hex[1]);
|
||||
const auto b = parse_hex_nibble(hex[2]);
|
||||
const auto a = hex.size() == 4U ? parse_hex_nibble(hex[3])
|
||||
: pp::foundation::Result<std::uint8_t>::success(255);
|
||||
if (!r || !g || !b || !a) {
|
||||
return pp::foundation::Result<ColorRgba8>::failure(
|
||||
pp::foundation::Status::invalid_argument("color contains a non-hex character"));
|
||||
}
|
||||
color = ColorRgba8 { .r = r.value(), .g = g.value(), .b = b.value(), .a = a.value() };
|
||||
return pp::foundation::Result<ColorRgba8>::success(color);
|
||||
}
|
||||
|
||||
const auto r = parse_hex_byte(hex.substr(0, 2));
|
||||
const auto g = parse_hex_byte(hex.substr(2, 2));
|
||||
const auto b = parse_hex_byte(hex.substr(4, 2));
|
||||
const auto a = hex.size() == 8U ? parse_hex_byte(hex.substr(6, 2))
|
||||
: pp::foundation::Result<std::uint8_t>::success(255);
|
||||
if (!r || !g || !b || !a) {
|
||||
return pp::foundation::Result<ColorRgba8>::failure(
|
||||
pp::foundation::Status::invalid_argument("color contains a non-hex character"));
|
||||
}
|
||||
|
||||
color = ColorRgba8 { .r = r.value(), .g = g.value(), .b = b.value(), .a = a.value() };
|
||||
return pp::foundation::Result<ColorRgba8>::success(color);
|
||||
}
|
||||
|
||||
}
|
||||
19
src/ui_core/color.h
Normal file
19
src/ui_core/color.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::ui {
|
||||
|
||||
struct ColorRgba8 {
|
||||
std::uint8_t r = 0;
|
||||
std::uint8_t g = 0;
|
||||
std::uint8_t b = 0;
|
||||
std::uint8_t a = 255;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<ColorRgba8> parse_hex_color(std::string_view value) noexcept;
|
||||
|
||||
}
|
||||
57
src/ui_core/layout_value.cpp
Normal file
57
src/ui_core/layout_value.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "ui_core/layout_value.h"
|
||||
|
||||
#include "foundation/parse.h"
|
||||
|
||||
namespace pp::ui {
|
||||
|
||||
pp::foundation::Result<LayoutLength> parse_layout_length(std::string_view text) noexcept
|
||||
{
|
||||
if (text == "auto") {
|
||||
return pp::foundation::Result<LayoutLength>::success(
|
||||
LayoutLength { .kind = LayoutLengthKind::auto_value, .value = 0 });
|
||||
}
|
||||
|
||||
if (text.empty()) {
|
||||
return pp::foundation::Result<LayoutLength>::failure(
|
||||
pp::foundation::Status::invalid_argument("layout length must not be empty"));
|
||||
}
|
||||
|
||||
if (text.back() == '%') {
|
||||
const auto number = pp::foundation::parse_u32(text.substr(0, text.size() - 1U));
|
||||
if (!number) {
|
||||
return pp::foundation::Result<LayoutLength>::failure(number.status());
|
||||
}
|
||||
|
||||
if (number.value() > 100U) {
|
||||
return pp::foundation::Result<LayoutLength>::failure(
|
||||
pp::foundation::Status::out_of_range("layout percent must be between 0 and 100"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<LayoutLength>::success(
|
||||
LayoutLength { .kind = LayoutLengthKind::percent, .value = number.value() });
|
||||
}
|
||||
|
||||
const auto pixels = pp::foundation::parse_u32(text);
|
||||
if (!pixels) {
|
||||
return pp::foundation::Result<LayoutLength>::failure(pixels.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<LayoutLength>::success(
|
||||
LayoutLength { .kind = LayoutLengthKind::pixels, .value = pixels.value() });
|
||||
}
|
||||
|
||||
const char* layout_length_kind_name(LayoutLengthKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case LayoutLengthKind::auto_value:
|
||||
return "auto";
|
||||
case LayoutLengthKind::pixels:
|
||||
return "pixels";
|
||||
case LayoutLengthKind::percent:
|
||||
return "percent";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
24
src/ui_core/layout_value.h
Normal file
24
src/ui_core/layout_value.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::ui {
|
||||
|
||||
enum class LayoutLengthKind : std::uint8_t {
|
||||
auto_value,
|
||||
pixels,
|
||||
percent,
|
||||
};
|
||||
|
||||
struct LayoutLength {
|
||||
LayoutLengthKind kind = LayoutLengthKind::auto_value;
|
||||
std::uint32_t value = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<LayoutLength> parse_layout_length(std::string_view text) noexcept;
|
||||
[[nodiscard]] const char* layout_length_kind_name(LayoutLengthKind kind) noexcept;
|
||||
|
||||
}
|
||||
71
src/ui_core/layout_xml.cpp
Normal file
71
src/ui_core/layout_xml.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "ui_core/layout_xml.h"
|
||||
|
||||
#include "ui_core/layout_value.h"
|
||||
|
||||
#include <tinyxml2.h>
|
||||
|
||||
namespace pp::ui {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Status visit_element(const tinyxml2::XMLElement& element, LayoutParseSummary& summary)
|
||||
{
|
||||
++summary.node_count;
|
||||
|
||||
for (const char* name : { "width", "height" }) {
|
||||
const char* value = element.Attribute(name);
|
||||
if (value == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto length = parse_layout_length(value);
|
||||
if (!length) {
|
||||
return length.status();
|
||||
}
|
||||
++summary.length_attribute_count;
|
||||
}
|
||||
|
||||
for (const tinyxml2::XMLElement* child = element.FirstChildElement();
|
||||
child != nullptr;
|
||||
child = child->NextSiblingElement()) {
|
||||
const auto status = visit_element(*child, summary);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<LayoutParseSummary> parse_layout_xml(std::string_view xml)
|
||||
{
|
||||
if (xml.empty()) {
|
||||
return pp::foundation::Result<LayoutParseSummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("layout XML must not be empty"));
|
||||
}
|
||||
|
||||
tinyxml2::XMLDocument document;
|
||||
const auto error = document.Parse(xml.data(), xml.size());
|
||||
if (error != tinyxml2::XML_SUCCESS) {
|
||||
return pp::foundation::Result<LayoutParseSummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("layout XML could not be parsed"));
|
||||
}
|
||||
|
||||
const tinyxml2::XMLElement* root = document.RootElement();
|
||||
if (root == nullptr) {
|
||||
return pp::foundation::Result<LayoutParseSummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("layout XML has no root element"));
|
||||
}
|
||||
|
||||
LayoutParseSummary summary;
|
||||
const auto status = visit_element(*root, summary);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<LayoutParseSummary>::failure(status);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<LayoutParseSummary>::success(summary);
|
||||
}
|
||||
|
||||
}
|
||||
17
src/ui_core/layout_xml.h
Normal file
17
src/ui_core/layout_xml.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::ui {
|
||||
|
||||
struct LayoutParseSummary {
|
||||
std::size_t node_count = 0;
|
||||
std::size_t length_attribute_count = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<LayoutParseSummary> parse_layout_xml(std::string_view xml);
|
||||
|
||||
}
|
||||
@@ -11,7 +11,9 @@ const int g_version_build = PP_VERSION_BUILD;
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
const wchar_t* g_version_w = TEXT(PP_VERSION_STRING);
|
||||
const wchar_t* g_version_number_w = TEXT(PP_VERSION_NUMBER_STRING);
|
||||
const wchar_t* g_window_title_w = L"PanoPainter " TEXT(PP_VERSION_NUMBER_STRING);
|
||||
#define PP_WIDEN2(x) L##x
|
||||
#define PP_WIDEN(x) PP_WIDEN2(x)
|
||||
const wchar_t* g_version_w = PP_WIDEN(PP_VERSION_STRING);
|
||||
const wchar_t* g_version_number_w = PP_WIDEN(PP_VERSION_NUMBER_STRING);
|
||||
const wchar_t* g_window_title_w = L"PanoPainter " PP_WIDEN(PP_VERSION_NUMBER_STRING);
|
||||
#endif
|
||||
|
||||
281
tests/CMakeLists.txt
Normal file
281
tests/CMakeLists.txt
Normal file
@@ -0,0 +1,281 @@
|
||||
add_library(pp_test_harness INTERFACE)
|
||||
target_include_directories(pp_test_harness INTERFACE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(pp_test_harness INTERFACE
|
||||
pp_project_options
|
||||
pp_project_warnings)
|
||||
|
||||
add_executable(pp_foundation_binary_stream_tests
|
||||
foundation/binary_stream_tests.cpp
|
||||
)
|
||||
target_link_libraries(pp_foundation_binary_stream_tests PRIVATE
|
||||
pp_foundation
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_foundation_binary_stream_tests COMMAND pp_foundation_binary_stream_tests)
|
||||
set_tests_properties(pp_foundation_binary_stream_tests PROPERTIES
|
||||
LABELS "foundation;desktop-fast")
|
||||
|
||||
add_executable(pp_foundation_event_tests
|
||||
foundation/event_tests.cpp)
|
||||
target_link_libraries(pp_foundation_event_tests PRIVATE
|
||||
pp_foundation
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_foundation_event_tests COMMAND pp_foundation_event_tests)
|
||||
set_tests_properties(pp_foundation_event_tests PROPERTIES
|
||||
LABELS "foundation;desktop-fast")
|
||||
|
||||
add_executable(pp_foundation_log_tests
|
||||
foundation/log_tests.cpp)
|
||||
target_link_libraries(pp_foundation_log_tests PRIVATE
|
||||
pp_foundation
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_foundation_log_tests COMMAND pp_foundation_log_tests)
|
||||
set_tests_properties(pp_foundation_log_tests PROPERTIES
|
||||
LABELS "foundation;desktop-fast")
|
||||
|
||||
add_executable(pp_foundation_parse_tests
|
||||
foundation/parse_tests.cpp)
|
||||
target_link_libraries(pp_foundation_parse_tests PRIVATE
|
||||
pp_foundation
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_foundation_parse_tests COMMAND pp_foundation_parse_tests)
|
||||
set_tests_properties(pp_foundation_parse_tests PROPERTIES
|
||||
LABELS "foundation;desktop-fast")
|
||||
|
||||
add_executable(pp_foundation_task_queue_tests
|
||||
foundation/task_queue_tests.cpp)
|
||||
target_link_libraries(pp_foundation_task_queue_tests PRIVATE
|
||||
pp_foundation
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_foundation_task_queue_tests COMMAND pp_foundation_task_queue_tests)
|
||||
set_tests_properties(pp_foundation_task_queue_tests PROPERTIES
|
||||
LABELS "foundation;desktop-fast")
|
||||
|
||||
add_executable(pp_foundation_trace_tests
|
||||
foundation/trace_tests.cpp)
|
||||
target_link_libraries(pp_foundation_trace_tests PRIVATE
|
||||
pp_foundation
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_foundation_trace_tests COMMAND pp_foundation_trace_tests)
|
||||
set_tests_properties(pp_foundation_trace_tests PROPERTIES
|
||||
LABELS "foundation;desktop-fast")
|
||||
|
||||
add_executable(pp_assets_image_format_tests
|
||||
assets/image_format_tests.cpp)
|
||||
target_link_libraries(pp_assets_image_format_tests PRIVATE
|
||||
pp_assets
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_assets_image_format_tests COMMAND pp_assets_image_format_tests)
|
||||
set_tests_properties(pp_assets_image_format_tests PROPERTIES
|
||||
LABELS "assets;desktop-fast")
|
||||
|
||||
add_executable(pp_assets_image_metadata_tests
|
||||
assets/image_metadata_tests.cpp)
|
||||
target_link_libraries(pp_assets_image_metadata_tests PRIVATE
|
||||
pp_assets
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_assets_image_metadata_tests COMMAND pp_assets_image_metadata_tests)
|
||||
set_tests_properties(pp_assets_image_metadata_tests PROPERTIES
|
||||
LABELS "assets;desktop-fast")
|
||||
|
||||
add_executable(pp_assets_image_pixels_tests
|
||||
assets/image_pixels_tests.cpp)
|
||||
target_link_libraries(pp_assets_image_pixels_tests PRIVATE
|
||||
pp_assets
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_assets_image_pixels_tests COMMAND pp_assets_image_pixels_tests)
|
||||
set_tests_properties(pp_assets_image_pixels_tests PROPERTIES
|
||||
LABELS "assets;desktop-fast")
|
||||
|
||||
add_executable(pp_assets_ppi_header_tests
|
||||
assets/ppi_header_tests.cpp)
|
||||
target_link_libraries(pp_assets_ppi_header_tests PRIVATE
|
||||
pp_assets
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_assets_ppi_header_tests COMMAND pp_assets_ppi_header_tests)
|
||||
set_tests_properties(pp_assets_ppi_header_tests PROPERTIES
|
||||
LABELS "assets;desktop-fast")
|
||||
|
||||
add_executable(pp_assets_settings_document_tests
|
||||
assets/settings_document_tests.cpp)
|
||||
target_link_libraries(pp_assets_settings_document_tests PRIVATE
|
||||
pp_assets
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_assets_settings_document_tests COMMAND pp_assets_settings_document_tests)
|
||||
set_tests_properties(pp_assets_settings_document_tests PROPERTIES
|
||||
LABELS "assets;desktop-fast")
|
||||
|
||||
add_executable(pp_paint_brush_tests
|
||||
paint/brush_tests.cpp)
|
||||
target_link_libraries(pp_paint_brush_tests PRIVATE
|
||||
pp_paint
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_paint_brush_tests COMMAND pp_paint_brush_tests)
|
||||
set_tests_properties(pp_paint_brush_tests PROPERTIES
|
||||
LABELS "paint;desktop-fast")
|
||||
|
||||
add_executable(pp_paint_blend_tests
|
||||
paint/blend_tests.cpp)
|
||||
target_link_libraries(pp_paint_blend_tests PRIVATE
|
||||
pp_paint
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_paint_blend_tests COMMAND pp_paint_blend_tests)
|
||||
set_tests_properties(pp_paint_blend_tests PROPERTIES
|
||||
LABELS "paint;desktop-fast")
|
||||
|
||||
add_executable(pp_paint_stroke_tests
|
||||
paint/stroke_tests.cpp)
|
||||
target_link_libraries(pp_paint_stroke_tests PRIVATE
|
||||
pp_paint
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_paint_stroke_tests COMMAND pp_paint_stroke_tests)
|
||||
set_tests_properties(pp_paint_stroke_tests PROPERTIES
|
||||
LABELS "paint;desktop-fast")
|
||||
|
||||
add_executable(pp_paint_stroke_script_tests
|
||||
paint/stroke_script_tests.cpp)
|
||||
target_link_libraries(pp_paint_stroke_script_tests PRIVATE
|
||||
pp_paint
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_paint_stroke_script_tests COMMAND pp_paint_stroke_script_tests)
|
||||
set_tests_properties(pp_paint_stroke_script_tests PROPERTIES
|
||||
LABELS "paint;desktop-fast")
|
||||
|
||||
add_executable(pp_document_tests
|
||||
document/document_tests.cpp)
|
||||
target_link_libraries(pp_document_tests PRIVATE
|
||||
pp_document
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_document_tests COMMAND pp_document_tests)
|
||||
set_tests_properties(pp_document_tests PROPERTIES
|
||||
LABELS "document;desktop-fast")
|
||||
|
||||
add_executable(pp_document_ppi_import_tests
|
||||
document/ppi_import_tests.cpp)
|
||||
target_link_libraries(pp_document_ppi_import_tests PRIVATE
|
||||
pp_document
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_document_ppi_import_tests COMMAND pp_document_ppi_import_tests)
|
||||
set_tests_properties(pp_document_ppi_import_tests PROPERTIES
|
||||
LABELS "assets;document;integration;desktop-fast")
|
||||
|
||||
add_executable(pp_renderer_api_tests
|
||||
renderer_api/renderer_api_tests.cpp)
|
||||
target_link_libraries(pp_renderer_api_tests PRIVATE
|
||||
pp_renderer_api
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_renderer_api_tests COMMAND pp_renderer_api_tests)
|
||||
set_tests_properties(pp_renderer_api_tests PROPERTIES
|
||||
LABELS "renderer;desktop-fast")
|
||||
|
||||
add_executable(pp_paint_renderer_compositor_tests
|
||||
paint_renderer/compositor_tests.cpp)
|
||||
target_link_libraries(pp_paint_renderer_compositor_tests PRIVATE
|
||||
pp_paint_renderer
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_paint_renderer_compositor_tests COMMAND pp_paint_renderer_compositor_tests)
|
||||
set_tests_properties(pp_paint_renderer_compositor_tests PROPERTIES
|
||||
LABELS "renderer;paint;desktop-fast")
|
||||
|
||||
add_executable(pp_ui_core_color_tests
|
||||
ui_core/color_tests.cpp)
|
||||
target_link_libraries(pp_ui_core_color_tests PRIVATE
|
||||
pp_ui_core
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_ui_core_color_tests COMMAND pp_ui_core_color_tests)
|
||||
set_tests_properties(pp_ui_core_color_tests PROPERTIES
|
||||
LABELS "ui;desktop-fast")
|
||||
|
||||
add_executable(pp_ui_core_layout_value_tests
|
||||
ui_core/layout_value_tests.cpp)
|
||||
target_link_libraries(pp_ui_core_layout_value_tests PRIVATE
|
||||
pp_ui_core
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_ui_core_layout_value_tests COMMAND pp_ui_core_layout_value_tests)
|
||||
set_tests_properties(pp_ui_core_layout_value_tests PROPERTIES
|
||||
LABELS "ui;desktop-fast")
|
||||
|
||||
add_executable(pp_ui_core_layout_xml_tests
|
||||
ui_core/layout_xml_tests.cpp)
|
||||
target_link_libraries(pp_ui_core_layout_xml_tests PRIVATE
|
||||
pp_ui_core
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_ui_core_layout_xml_tests COMMAND pp_ui_core_layout_xml_tests)
|
||||
set_tests_properties(pp_ui_core_layout_xml_tests PROPERTIES
|
||||
LABELS "ui;desktop-fast")
|
||||
|
||||
if(TARGET pano_cli)
|
||||
add_test(NAME pano_cli_create_document_smoke
|
||||
COMMAND pano_cli create-document --width 64 --height 32 --layers 2)
|
||||
set_tests_properties(pano_cli_create_document_smoke PROPERTIES
|
||||
LABELS "integration;desktop-fast")
|
||||
|
||||
add_test(NAME pano_cli_create_animation_document_smoke
|
||||
COMMAND pano_cli create-document --width 64 --height 32 --layers 2 --frames 3 --frame-duration-ms 250)
|
||||
set_tests_properties(pano_cli_create_animation_document_smoke PROPERTIES
|
||||
LABELS "document;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"frames\":3.*\"activeFrame\":2.*\"animationDurationMs\":750")
|
||||
|
||||
add_test(NAME pano_cli_inspect_image_rejects_unsupported
|
||||
COMMAND pano_cli inspect-image --path "${CMAKE_CURRENT_SOURCE_DIR}/data/images/unsupported-image.txt")
|
||||
set_tests_properties(pano_cli_inspect_image_rejects_unsupported PROPERTIES
|
||||
LABELS "assets;integration;desktop-fast"
|
||||
WILL_FAIL TRUE)
|
||||
|
||||
add_test(NAME pano_cli_inspect_png_metadata_smoke
|
||||
COMMAND pano_cli inspect-image --path "${CMAKE_CURRENT_SOURCE_DIR}/data/images/tiny-rgba-header.png")
|
||||
set_tests_properties(pano_cli_inspect_png_metadata_smoke PROPERTIES
|
||||
LABELS "assets;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"format\":\"png\".*\"width\":320.*\"height\":240.*\"components\":4.*\"colorType\":\"rgba\"")
|
||||
|
||||
add_test(NAME pano_cli_inspect_project_layout_smoke
|
||||
COMMAND pano_cli inspect-project --path "${CMAKE_CURRENT_SOURCE_DIR}/data/projects/minimal-project.ppi")
|
||||
set_tests_properties(pano_cli_inspect_project_layout_smoke PROPERTIES
|
||||
LABELS "assets;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"thumbnail\":\\{\"width\":128,\"height\":128,\"components\":4,\"bytes\":65536\\}.*\"body\":\\{\"offset\":65576,\"bytes\":73,\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"dirtyFaces\":0,\"rgbaFacePayloads\":0,\"compressedBytes\":0,\"infoBytes\":0\\}.*\"layers\":\\[\\{\"index\":0,\"storedOrder\":0,\"name\":\"Ink\"")
|
||||
|
||||
add_test(NAME pano_cli_load_project_metadata_smoke
|
||||
COMMAND pano_cli load-project --path "${CMAKE_CURRENT_SOURCE_DIR}/data/projects/minimal-project.ppi")
|
||||
set_tests_properties(pano_cli_load_project_metadata_smoke PROPERTIES
|
||||
LABELS "assets;document;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"load-project\".*\"pixelDataLoaded\":false.*\"facePayloads\":0.*\"document\":\\{\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"animationDurationMs\":100,\"layerNames\":\\[\"Ink\"\\],\"layerFrameCounts\":\\[1\\],\"layerDurationsMs\":\\[100\\]")
|
||||
|
||||
add_test(NAME pano_cli_parse_layout_smoke
|
||||
COMMAND pano_cli parse-layout --path "${CMAKE_CURRENT_SOURCE_DIR}/data/layouts/simple-layout.xml")
|
||||
set_tests_properties(pano_cli_parse_layout_smoke PROPERTIES
|
||||
LABELS "ui;integration;desktop-fast")
|
||||
|
||||
add_test(NAME pano_cli_simulate_stroke_smoke
|
||||
COMMAND pano_cli simulate-stroke --x1 0 --y1 0 --x2 10 --y2 0 --spacing 2)
|
||||
set_tests_properties(pano_cli_simulate_stroke_smoke PROPERTIES
|
||||
LABELS "paint;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"samples\":6.*\"distance\":10")
|
||||
|
||||
add_test(NAME pano_cli_simulate_stroke_script_smoke
|
||||
COMMAND pano_cli simulate-stroke-script --path "${CMAKE_CURRENT_SOURCE_DIR}/data/strokes/two-strokes.ppstroke")
|
||||
set_tests_properties(pano_cli_simulate_stroke_script_smoke PROPERTIES
|
||||
LABELS "paint;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"strokes\":2.*\"samples\":9.*\"distance\":20")
|
||||
endif()
|
||||
88
tests/assets/image_format_tests.cpp
Normal file
88
tests/assets/image_format_tests.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "assets/image_format.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
using pp::assets::ImageFormat;
|
||||
using pp::assets::detect_image_format;
|
||||
using pp::assets::image_format_name;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
void detects_png_and_jpeg_signatures(pp::tests::Harness& h)
|
||||
{
|
||||
constexpr std::array png {
|
||||
std::byte { 0x89 },
|
||||
std::byte { 0x50 },
|
||||
std::byte { 0x4e },
|
||||
std::byte { 0x47 },
|
||||
std::byte { 0x0d },
|
||||
std::byte { 0x0a },
|
||||
std::byte { 0x1a },
|
||||
std::byte { 0x0a },
|
||||
std::byte { 0x00 },
|
||||
};
|
||||
constexpr std::array jpeg {
|
||||
std::byte { 0xff },
|
||||
std::byte { 0xd8 },
|
||||
std::byte { 0xff },
|
||||
std::byte { 0xe0 },
|
||||
};
|
||||
|
||||
const auto png_format = detect_image_format(png);
|
||||
const auto jpeg_format = detect_image_format(jpeg);
|
||||
|
||||
PP_EXPECT(h, png_format.ok());
|
||||
PP_EXPECT(h, png_format.value() == ImageFormat::png);
|
||||
PP_EXPECT(h, image_format_name(png_format.value()) == std::string_view("png"));
|
||||
PP_EXPECT(h, jpeg_format.ok());
|
||||
PP_EXPECT(h, jpeg_format.value() == ImageFormat::jpeg);
|
||||
PP_EXPECT(h, image_format_name(jpeg_format.value()) == std::string_view("jpeg"));
|
||||
}
|
||||
|
||||
void rejects_empty_truncated_and_unsupported_inputs(pp::tests::Harness& h)
|
||||
{
|
||||
constexpr std::array<std::byte, 0> empty {};
|
||||
constexpr std::array partial_png {
|
||||
std::byte { 0x89 },
|
||||
std::byte { 0x50 },
|
||||
std::byte { 0x4e },
|
||||
};
|
||||
constexpr std::array short_unknown {
|
||||
std::byte { 0x12 },
|
||||
std::byte { 0x34 },
|
||||
};
|
||||
constexpr std::array unsupported {
|
||||
std::byte { 0x47 },
|
||||
std::byte { 0x49 },
|
||||
std::byte { 0x46 },
|
||||
std::byte { 0x38 },
|
||||
};
|
||||
|
||||
const auto empty_result = detect_image_format(empty);
|
||||
const auto partial_png_result = detect_image_format(partial_png);
|
||||
const auto short_result = detect_image_format(short_unknown);
|
||||
const auto unsupported_result = detect_image_format(unsupported);
|
||||
|
||||
PP_EXPECT(h, !empty_result.ok());
|
||||
PP_EXPECT(h, empty_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !partial_png_result.ok());
|
||||
PP_EXPECT(h, partial_png_result.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !short_result.ok());
|
||||
PP_EXPECT(h, short_result.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !unsupported_result.ok());
|
||||
PP_EXPECT(h, unsupported_result.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("detects_png_and_jpeg_signatures", detects_png_and_jpeg_signatures);
|
||||
harness.run("rejects_empty_truncated_and_unsupported_inputs", rejects_empty_truncated_and_unsupported_inputs);
|
||||
return harness.finish();
|
||||
}
|
||||
113
tests/assets/image_metadata_tests.cpp
Normal file
113
tests/assets/image_metadata_tests.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include "assets/image_metadata.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
using pp::assets::ImageColorType;
|
||||
using pp::assets::image_color_type_name;
|
||||
using pp::assets::max_image_dimension;
|
||||
using pp::assets::parse_png_metadata;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
using PngHeader = std::array<std::byte, 33>;
|
||||
|
||||
PngHeader make_png_header(std::uint32_t width, std::uint32_t height, std::byte bit_depth, std::byte color_type)
|
||||
{
|
||||
PngHeader bytes {
|
||||
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
|
||||
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
|
||||
std::byte { 'I' }, std::byte { 'H' }, std::byte { 'D' }, std::byte { 'R' },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
bit_depth, color_type, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
};
|
||||
|
||||
bytes[16] = static_cast<std::byte>((width >> 24U) & 0xffU);
|
||||
bytes[17] = static_cast<std::byte>((width >> 16U) & 0xffU);
|
||||
bytes[18] = static_cast<std::byte>((width >> 8U) & 0xffU);
|
||||
bytes[19] = static_cast<std::byte>(width & 0xffU);
|
||||
bytes[20] = static_cast<std::byte>((height >> 24U) & 0xffU);
|
||||
bytes[21] = static_cast<std::byte>((height >> 16U) & 0xffU);
|
||||
bytes[22] = static_cast<std::byte>((height >> 8U) & 0xffU);
|
||||
bytes[23] = static_cast<std::byte>(height & 0xffU);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void parses_png_ihdr_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
const auto rgba = make_png_header(320, 240, std::byte { 8 }, std::byte { 6 });
|
||||
const auto rgb = make_png_header(17, 9, std::byte { 8 }, std::byte { 2 });
|
||||
|
||||
const auto rgba_result = parse_png_metadata(rgba);
|
||||
const auto rgb_result = parse_png_metadata(rgb);
|
||||
|
||||
PP_EXPECT(h, rgba_result.ok());
|
||||
PP_EXPECT(h, rgba_result.value().width == 320U);
|
||||
PP_EXPECT(h, rgba_result.value().height == 240U);
|
||||
PP_EXPECT(h, rgba_result.value().bit_depth == 8U);
|
||||
PP_EXPECT(h, rgba_result.value().components == 4U);
|
||||
PP_EXPECT(h, rgba_result.value().color_type == ImageColorType::rgba);
|
||||
PP_EXPECT(h, image_color_type_name(rgba_result.value().color_type) == std::string_view("rgba"));
|
||||
|
||||
PP_EXPECT(h, rgb_result.ok());
|
||||
PP_EXPECT(h, rgb_result.value().components == 3U);
|
||||
PP_EXPECT(h, rgb_result.value().color_type == ImageColorType::rgb);
|
||||
}
|
||||
|
||||
void maps_png_color_type_components(pp::tests::Harness& h)
|
||||
{
|
||||
const auto grayscale = parse_png_metadata(make_png_header(1, 1, std::byte { 8 }, std::byte { 0 }));
|
||||
const auto indexed = parse_png_metadata(make_png_header(1, 1, std::byte { 8 }, std::byte { 3 }));
|
||||
const auto gray_alpha = parse_png_metadata(make_png_header(1, 1, std::byte { 8 }, std::byte { 4 }));
|
||||
|
||||
PP_EXPECT(h, grayscale.ok());
|
||||
PP_EXPECT(h, grayscale.value().components == 1U);
|
||||
PP_EXPECT(h, grayscale.value().color_type == ImageColorType::grayscale);
|
||||
PP_EXPECT(h, indexed.ok());
|
||||
PP_EXPECT(h, indexed.value().components == 1U);
|
||||
PP_EXPECT(h, indexed.value().color_type == ImageColorType::indexed);
|
||||
PP_EXPECT(h, gray_alpha.ok());
|
||||
PP_EXPECT(h, gray_alpha.value().components == 2U);
|
||||
PP_EXPECT(h, gray_alpha.value().color_type == ImageColorType::grayscale_alpha);
|
||||
}
|
||||
|
||||
void rejects_corrupt_or_extreme_png_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
const std::array<std::byte, 8> truncated {
|
||||
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
|
||||
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
|
||||
};
|
||||
auto bad_signature = make_png_header(1, 1, std::byte { 8 }, std::byte { 6 });
|
||||
bad_signature[0] = std::byte { 0x00 };
|
||||
auto bad_ihdr = make_png_header(1, 1, std::byte { 8 }, std::byte { 6 });
|
||||
bad_ihdr[15] = std::byte { 'X' };
|
||||
const auto zero_width = make_png_header(0, 1, std::byte { 8 }, std::byte { 6 });
|
||||
const auto too_large = make_png_header(max_image_dimension + 1U, 1, std::byte { 8 }, std::byte { 6 });
|
||||
const auto bad_depth = make_png_header(1, 1, std::byte { 0 }, std::byte { 6 });
|
||||
const auto bad_color = make_png_header(1, 1, std::byte { 8 }, std::byte { 5 });
|
||||
|
||||
PP_EXPECT(h, parse_png_metadata(truncated).status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, parse_png_metadata(bad_signature).status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, parse_png_metadata(bad_ihdr).status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, parse_png_metadata(zero_width).status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, parse_png_metadata(too_large).status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, parse_png_metadata(bad_depth).status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, parse_png_metadata(bad_color).status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("parses_png_ihdr_metadata", parses_png_ihdr_metadata);
|
||||
harness.run("maps_png_color_type_components", maps_png_color_type_components);
|
||||
harness.run("rejects_corrupt_or_extreme_png_metadata", rejects_corrupt_or_extreme_png_metadata);
|
||||
return harness.finish();
|
||||
}
|
||||
67
tests/assets/image_pixels_tests.cpp
Normal file
67
tests/assets/image_pixels_tests.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "assets/image_pixels.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
using pp::assets::decode_png_rgba8;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::byte, 68> transparent_png_1x1 {
|
||||
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
|
||||
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
|
||||
std::byte { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 },
|
||||
std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 },
|
||||
std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 },
|
||||
std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 },
|
||||
std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 },
|
||||
};
|
||||
|
||||
void decodes_png_to_rgba8_pixels(pp::tests::Harness& h)
|
||||
{
|
||||
const auto image = decode_png_rgba8(transparent_png_1x1);
|
||||
|
||||
PP_EXPECT(h, image.ok());
|
||||
PP_EXPECT(h, image.value().width == 1U);
|
||||
PP_EXPECT(h, image.value().height == 1U);
|
||||
PP_EXPECT(h, image.value().pixels.size() == 4U);
|
||||
PP_EXPECT(h, image.value().pixels[0] == 0U);
|
||||
PP_EXPECT(h, image.value().pixels[1] == 0U);
|
||||
PP_EXPECT(h, image.value().pixels[2] == 0U);
|
||||
PP_EXPECT(h, image.value().pixels[3] == 0U);
|
||||
}
|
||||
|
||||
void rejects_corrupt_png_payload(pp::tests::Harness& h)
|
||||
{
|
||||
auto corrupt = transparent_png_1x1;
|
||||
corrupt[0] = std::byte { 0x00 };
|
||||
|
||||
const auto image = decode_png_rgba8(corrupt);
|
||||
|
||||
PP_EXPECT(h, !image.ok());
|
||||
PP_EXPECT(h, image.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("decodes_png_to_rgba8_pixels", decodes_png_to_rgba8_pixels);
|
||||
harness.run("rejects_corrupt_png_payload", rejects_corrupt_png_payload);
|
||||
return harness.finish();
|
||||
}
|
||||
405
tests/assets/ppi_header_tests.cpp
Normal file
405
tests/assets/ppi_header_tests.cpp
Normal file
@@ -0,0 +1,405 @@
|
||||
#include "assets/ppi_header.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using pp::assets::parse_ppi_header;
|
||||
using pp::assets::decode_ppi_project_images;
|
||||
using pp::assets::parse_ppi_project_index;
|
||||
using pp::assets::parse_ppi_project_summary;
|
||||
using pp::assets::parse_ppi_project_layout;
|
||||
using pp::assets::ppi_header_size;
|
||||
using pp::assets::ppi_thumbnail_byte_size;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
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_u32_be(std::vector<std::byte>& bytes, std::uint32_t value)
|
||||
{
|
||||
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>(value & 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));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::byte> valid_header()
|
||||
{
|
||||
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, 2);
|
||||
append_u32(bytes, 3);
|
||||
append_u32(bytes, 1024);
|
||||
append_u32(bytes, 128);
|
||||
append_u32(bytes, 128);
|
||||
append_u32(bytes, 4);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void append_minimal_body(std::vector<std::byte>& bytes)
|
||||
{
|
||||
append_u32(bytes, 64);
|
||||
append_u32(bytes, 32);
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 0);
|
||||
append_f32(bytes, 1.0F);
|
||||
append_u32(bytes, 3);
|
||||
append_ascii(bytes, "Ink");
|
||||
append_u32(bytes, 0);
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 1 });
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 100);
|
||||
for (std::uint32_t i = 0; i < 6U; ++i) {
|
||||
append_u32(bytes, 0);
|
||||
}
|
||||
append_u32(bytes, 0);
|
||||
}
|
||||
|
||||
std::vector<std::byte> png_ihdr_payload(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::uint8_t bit_depth = 8U,
|
||||
std::uint8_t color_type = 6U)
|
||||
{
|
||||
std::vector<std::byte> bytes {
|
||||
std::byte { 0x89 },
|
||||
std::byte { 0x50 },
|
||||
std::byte { 0x4e },
|
||||
std::byte { 0x47 },
|
||||
std::byte { 0x0d },
|
||||
std::byte { 0x0a },
|
||||
std::byte { 0x1a },
|
||||
std::byte { 0x0a },
|
||||
};
|
||||
append_u32_be(bytes, 13);
|
||||
bytes.push_back(std::byte { 'I' });
|
||||
bytes.push_back(std::byte { 'H' });
|
||||
bytes.push_back(std::byte { 'D' });
|
||||
bytes.push_back(std::byte { 'R' });
|
||||
append_u32_be(bytes, width);
|
||||
append_u32_be(bytes, height);
|
||||
bytes.push_back(static_cast<std::byte>(bit_depth));
|
||||
bytes.push_back(static_cast<std::byte>(color_type));
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 0 });
|
||||
append_u32_be(bytes, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::byte> transparent_png_1x1()
|
||||
{
|
||||
return {
|
||||
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
|
||||
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
|
||||
std::byte { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 },
|
||||
std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 },
|
||||
std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 },
|
||||
std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 },
|
||||
std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 },
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<std::byte> minimal_project()
|
||||
{
|
||||
auto bytes = valid_header();
|
||||
bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
|
||||
append_minimal_body(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::byte> project_with_single_face_payload(
|
||||
std::vector<std::byte> payload,
|
||||
std::uint32_t dirty_width = 8,
|
||||
std::uint32_t dirty_height = 4)
|
||||
{
|
||||
auto bytes = valid_header();
|
||||
bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
|
||||
|
||||
append_u32(bytes, 64);
|
||||
append_u32(bytes, 32);
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 0);
|
||||
append_f32(bytes, 1.0F);
|
||||
append_u32(bytes, 3);
|
||||
append_ascii(bytes, "Ink");
|
||||
append_u32(bytes, 0);
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 1 });
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 100);
|
||||
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 2);
|
||||
append_u32(bytes, 3);
|
||||
append_u32(bytes, 2 + dirty_width);
|
||||
append_u32(bytes, 3 + dirty_height);
|
||||
append_u32(bytes, static_cast<std::uint32_t>(payload.size()));
|
||||
bytes.insert(bytes.end(), payload.begin(), payload.end());
|
||||
|
||||
for (std::uint32_t i = 1; i < 6U; ++i) {
|
||||
append_u32(bytes, 0);
|
||||
}
|
||||
append_u32(bytes, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void parses_legacy_ppi_header(pp::tests::Harness& h)
|
||||
{
|
||||
const auto bytes = valid_header();
|
||||
const auto header = parse_ppi_header(bytes);
|
||||
|
||||
PP_EXPECT(h, bytes.size() == ppi_header_size);
|
||||
PP_EXPECT(h, header.ok());
|
||||
PP_EXPECT(h, header.value().document_version.major == 0U);
|
||||
PP_EXPECT(h, header.value().document_version.minor == 4U);
|
||||
PP_EXPECT(h, header.value().software_version.fix == 3U);
|
||||
PP_EXPECT(h, header.value().software_version.build == 1024U);
|
||||
PP_EXPECT(h, header.value().thumbnail.width == 128U);
|
||||
PP_EXPECT(h, header.value().thumbnail.height == 128U);
|
||||
PP_EXPECT(h, header.value().thumbnail.components == 4U);
|
||||
}
|
||||
|
||||
void rejects_truncated_invalid_magic_and_bad_thumbnail(pp::tests::Harness& h)
|
||||
{
|
||||
auto truncated = valid_header();
|
||||
truncated.pop_back();
|
||||
|
||||
auto bad_magic = valid_header();
|
||||
bad_magic[0] = std::byte { 'X' };
|
||||
|
||||
auto bad_thumb = valid_header();
|
||||
bad_thumb[32] = std::byte { 64 };
|
||||
|
||||
const auto truncated_result = parse_ppi_header(truncated);
|
||||
const auto magic_result = parse_ppi_header(bad_magic);
|
||||
const auto thumb_result = parse_ppi_header(bad_thumb);
|
||||
|
||||
PP_EXPECT(h, !truncated_result.ok());
|
||||
PP_EXPECT(h, truncated_result.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !magic_result.ok());
|
||||
PP_EXPECT(h, magic_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !thumb_result.ok());
|
||||
PP_EXPECT(h, thumb_result.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void rejects_unsupported_document_versions(pp::tests::Harness& h)
|
||||
{
|
||||
auto bad_major = valid_header();
|
||||
bad_major[4] = std::byte { 1 };
|
||||
|
||||
auto bad_minor = valid_header();
|
||||
bad_minor[8] = std::byte { 0 };
|
||||
|
||||
const auto major_result = parse_ppi_header(bad_major);
|
||||
const auto minor_result = parse_ppi_header(bad_minor);
|
||||
|
||||
PP_EXPECT(h, !major_result.ok());
|
||||
PP_EXPECT(h, major_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !minor_result.ok());
|
||||
PP_EXPECT(h, minor_result.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void parses_project_layout_with_thumbnail_and_body(pp::tests::Harness& h)
|
||||
{
|
||||
const auto bytes = minimal_project();
|
||||
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
|
||||
PP_EXPECT(h, layout.ok());
|
||||
PP_EXPECT(h, layout.value().thumbnail_offset == ppi_header_size);
|
||||
PP_EXPECT(h, layout.value().thumbnail_bytes == 128U * 128U * 4U);
|
||||
PP_EXPECT(h, layout.value().body_offset == ppi_header_size + (128U * 128U * 4U));
|
||||
PP_EXPECT(h, layout.value().body_bytes == 73U);
|
||||
}
|
||||
|
||||
void rejects_project_layout_with_truncated_thumbnail(pp::tests::Harness& h)
|
||||
{
|
||||
auto bytes = valid_header();
|
||||
bytes.resize(ppi_header_size + (128U * 128U * 4U) - 1U, std::byte { 0 });
|
||||
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
|
||||
PP_EXPECT(h, !layout.ok());
|
||||
PP_EXPECT(h, layout.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void parses_minimal_project_body_summary(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = minimal_project();
|
||||
const auto summary = parse_ppi_project_summary(project);
|
||||
|
||||
PP_EXPECT(h, summary.ok());
|
||||
PP_EXPECT(h, summary.value().body.width == 64U);
|
||||
PP_EXPECT(h, summary.value().body.height == 32U);
|
||||
PP_EXPECT(h, summary.value().body.layer_count == 1U);
|
||||
PP_EXPECT(h, summary.value().body.declared_frame_count == 1U);
|
||||
PP_EXPECT(h, summary.value().body.total_layer_frames == 1U);
|
||||
PP_EXPECT(h, summary.value().body.dirty_face_count == 0U);
|
||||
PP_EXPECT(h, summary.value().body.rgba_face_payload_count == 0U);
|
||||
PP_EXPECT(h, summary.value().body.compressed_face_bytes == 0U);
|
||||
PP_EXPECT(h, summary.value().body.info_bytes == 0U);
|
||||
}
|
||||
|
||||
void indexes_project_layers_frames_and_faces(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4));
|
||||
const auto index = parse_ppi_project_index(project);
|
||||
|
||||
PP_EXPECT(h, index.ok());
|
||||
PP_EXPECT(h, index.value().body.layers.size() == 1U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].stored_order == 0U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].name == "Ink");
|
||||
PP_EXPECT(h, index.value().body.layers[0].visible);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames.size() == 1U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].duration_ms == 100U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].has_data);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].x0 == 2U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].x1 == 10U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].payload_bytes == 33U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].png_width == 8U);
|
||||
PP_EXPECT(h, !index.value().body.layers[0].frames[0].faces[1].has_data);
|
||||
}
|
||||
|
||||
void validates_dirty_face_png_payload_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4));
|
||||
const auto summary = parse_ppi_project_summary(project);
|
||||
|
||||
PP_EXPECT(h, summary.ok());
|
||||
PP_EXPECT(h, summary.value().body.dirty_face_count == 1U);
|
||||
PP_EXPECT(h, summary.value().body.rgba_face_payload_count == 1U);
|
||||
PP_EXPECT(h, summary.value().body.compressed_face_bytes == 33U);
|
||||
}
|
||||
|
||||
void decodes_dirty_face_png_payloads(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = project_with_single_face_payload(transparent_png_1x1(), 1, 1);
|
||||
const auto decoded = decode_ppi_project_images(project);
|
||||
|
||||
PP_EXPECT(h, decoded.ok());
|
||||
PP_EXPECT(h, decoded.value().faces.size() == 1U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].layer_index == 0U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].frame_index == 0U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].face_index == 0U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].image.width == 1U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].image.height == 1U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].image.pixels.size() == 4U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].image.pixels[3] == 0U);
|
||||
}
|
||||
|
||||
void rejects_metadata_only_payload_when_decoding_pixels(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4));
|
||||
const auto decoded = decode_ppi_project_images(project);
|
||||
|
||||
PP_EXPECT(h, !decoded.ok());
|
||||
PP_EXPECT(h, decoded.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void rejects_invalid_dirty_face_png_payloads(pp::tests::Harness& h)
|
||||
{
|
||||
auto mismatched_dimensions = project_with_single_face_payload(png_ihdr_payload(7, 4));
|
||||
auto non_rgba = project_with_single_face_payload(png_ihdr_payload(8, 4, 8, 2));
|
||||
auto bad_signature_payload = png_ihdr_payload(8, 4);
|
||||
bad_signature_payload[0] = std::byte { 0 };
|
||||
auto bad_signature = project_with_single_face_payload(bad_signature_payload);
|
||||
|
||||
const auto mismatched_result = parse_ppi_project_summary(mismatched_dimensions);
|
||||
const auto non_rgba_result = parse_ppi_project_summary(non_rgba);
|
||||
const auto bad_signature_result = parse_ppi_project_summary(bad_signature);
|
||||
|
||||
PP_EXPECT(h, !mismatched_result.ok());
|
||||
PP_EXPECT(h, mismatched_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !non_rgba_result.ok());
|
||||
PP_EXPECT(h, non_rgba_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_signature_result.ok());
|
||||
PP_EXPECT(h, bad_signature_result.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void rejects_invalid_project_body_summaries(pp::tests::Harness& h)
|
||||
{
|
||||
auto truncated = minimal_project();
|
||||
truncated.pop_back();
|
||||
|
||||
auto mismatched_frames = minimal_project();
|
||||
mismatched_frames[ppi_header_size + (128U * 128U * 4U) + 12U] = std::byte { 2 };
|
||||
|
||||
auto bad_layer_name = minimal_project();
|
||||
bad_layer_name[ppi_header_size + (128U * 128U * 4U) + 24U] = std::byte { 255 };
|
||||
|
||||
const auto truncated_result = parse_ppi_project_summary(truncated);
|
||||
const auto mismatched_frames_result = parse_ppi_project_summary(mismatched_frames);
|
||||
const auto bad_layer_name_result = parse_ppi_project_summary(bad_layer_name);
|
||||
|
||||
PP_EXPECT(h, !truncated_result.ok());
|
||||
PP_EXPECT(h, truncated_result.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !mismatched_frames_result.ok());
|
||||
PP_EXPECT(h, mismatched_frames_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_layer_name_result.ok());
|
||||
PP_EXPECT(h, bad_layer_name_result.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("parses_legacy_ppi_header", parses_legacy_ppi_header);
|
||||
harness.run("rejects_truncated_invalid_magic_and_bad_thumbnail", rejects_truncated_invalid_magic_and_bad_thumbnail);
|
||||
harness.run("rejects_unsupported_document_versions", rejects_unsupported_document_versions);
|
||||
harness.run("parses_project_layout_with_thumbnail_and_body", parses_project_layout_with_thumbnail_and_body);
|
||||
harness.run("rejects_project_layout_with_truncated_thumbnail", rejects_project_layout_with_truncated_thumbnail);
|
||||
harness.run("parses_minimal_project_body_summary", parses_minimal_project_body_summary);
|
||||
harness.run("indexes_project_layers_frames_and_faces", indexes_project_layers_frames_and_faces);
|
||||
harness.run("validates_dirty_face_png_payload_metadata", validates_dirty_face_png_payload_metadata);
|
||||
harness.run("decodes_dirty_face_png_payloads", decodes_dirty_face_png_payloads);
|
||||
harness.run("rejects_metadata_only_payload_when_decoding_pixels", rejects_metadata_only_payload_when_decoding_pixels);
|
||||
harness.run("rejects_invalid_dirty_face_png_payloads", rejects_invalid_dirty_face_png_payloads);
|
||||
harness.run("rejects_invalid_project_body_summaries", rejects_invalid_project_body_summaries);
|
||||
return harness.finish();
|
||||
}
|
||||
122
tests/assets/settings_document_tests.cpp
Normal file
122
tests/assets/settings_document_tests.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "assets/settings_document.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
using pp::assets::SettingsDocument;
|
||||
using pp::assets::SettingsValue;
|
||||
using pp::assets::max_settings_entries;
|
||||
using pp::assets::settings_value_type_name;
|
||||
using pp::assets::validate_settings_key;
|
||||
using pp::assets::validate_settings_value;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
void stores_updates_and_reads_typed_values(pp::tests::Harness& h)
|
||||
{
|
||||
SettingsDocument document;
|
||||
|
||||
PP_EXPECT(h, document.empty());
|
||||
PP_EXPECT(h, document.set("ui.theme", std::string("dark")).ok());
|
||||
PP_EXPECT(h, document.set("brush.size", std::int64_t { 42 }).ok());
|
||||
PP_EXPECT(h, document.set("brush.opacity", 0.75).ok());
|
||||
PP_EXPECT(h, document.set("tablet.enabled", true).ok());
|
||||
PP_EXPECT(h, document.size() == 4U);
|
||||
PP_EXPECT(h, document.has("brush.size"));
|
||||
|
||||
const auto theme = document.get("ui.theme");
|
||||
const auto size = document.get("brush.size");
|
||||
const auto opacity = document.get("brush.opacity");
|
||||
const auto tablet = document.get("tablet.enabled");
|
||||
|
||||
PP_EXPECT(h, theme.ok());
|
||||
PP_EXPECT(h, std::get<std::string>(theme.value()) == std::string_view("dark"));
|
||||
PP_EXPECT(h, settings_value_type_name(theme.value()) == std::string_view("string"));
|
||||
PP_EXPECT(h, size.ok());
|
||||
PP_EXPECT(h, std::get<std::int64_t>(size.value()) == 42);
|
||||
PP_EXPECT(h, opacity.ok());
|
||||
PP_EXPECT(h, std::fabs(std::get<double>(opacity.value()) - 0.75) < 0.0001);
|
||||
PP_EXPECT(h, tablet.ok());
|
||||
PP_EXPECT(h, std::get<bool>(tablet.value()));
|
||||
|
||||
PP_EXPECT(h, document.set("brush.size", std::int64_t { 64 }).ok());
|
||||
PP_EXPECT(h, document.size() == 4U);
|
||||
PP_EXPECT(h, std::get<std::int64_t>(document.get("brush.size").value()) == 64);
|
||||
}
|
||||
|
||||
void unsets_and_clears_entries(pp::tests::Harness& h)
|
||||
{
|
||||
SettingsDocument document;
|
||||
|
||||
PP_EXPECT(h, document.set("a", true).ok());
|
||||
PP_EXPECT(h, document.set("b", std::int64_t { 2 }).ok());
|
||||
PP_EXPECT(h, document.unset("a").ok());
|
||||
PP_EXPECT(h, !document.has("a"));
|
||||
PP_EXPECT(h, document.size() == 1U);
|
||||
|
||||
const auto missing = document.unset("a");
|
||||
PP_EXPECT(h, !missing.ok());
|
||||
PP_EXPECT(h, missing.code == StatusCode::out_of_range);
|
||||
|
||||
document.clear();
|
||||
PP_EXPECT(h, document.empty());
|
||||
}
|
||||
|
||||
void rejects_bad_keys_and_values(pp::tests::Harness& h)
|
||||
{
|
||||
const auto empty = validate_settings_key("");
|
||||
const auto dotted_start = validate_settings_key(".bad");
|
||||
const auto dotted_end = validate_settings_key("bad.");
|
||||
const auto invalid_char = validate_settings_key("bad/key");
|
||||
const auto long_key = validate_settings_key(std::string(129, 'a'));
|
||||
const auto non_finite = validate_settings_value(SettingsValue { std::nan("") });
|
||||
const auto huge_string = validate_settings_value(SettingsValue { std::string(4097, 'x') });
|
||||
|
||||
PP_EXPECT(h, !empty.ok());
|
||||
PP_EXPECT(h, empty.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !dotted_start.ok());
|
||||
PP_EXPECT(h, dotted_start.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !dotted_end.ok());
|
||||
PP_EXPECT(h, dotted_end.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !invalid_char.ok());
|
||||
PP_EXPECT(h, invalid_char.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !long_key.ok());
|
||||
PP_EXPECT(h, long_key.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !non_finite.ok());
|
||||
PP_EXPECT(h, non_finite.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !huge_string.ok());
|
||||
PP_EXPECT(h, huge_string.code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void rejects_missing_and_excessive_entries(pp::tests::Harness& h)
|
||||
{
|
||||
SettingsDocument document;
|
||||
const auto missing = document.get("missing");
|
||||
PP_EXPECT(h, !missing.ok());
|
||||
PP_EXPECT(h, missing.status().code == StatusCode::out_of_range);
|
||||
|
||||
for (std::size_t i = 0; i < max_settings_entries; ++i) {
|
||||
const auto key = std::string("k") + std::to_string(i);
|
||||
PP_EXPECT(h, document.set(key, std::int64_t { 1 }).ok());
|
||||
}
|
||||
|
||||
const auto excessive = document.set("one-more", std::int64_t { 1 });
|
||||
PP_EXPECT(h, !excessive.ok());
|
||||
PP_EXPECT(h, excessive.code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("stores_updates_and_reads_typed_values", stores_updates_and_reads_typed_values);
|
||||
harness.run("unsets_and_clears_entries", unsets_and_clears_entries);
|
||||
harness.run("rejects_bad_keys_and_values", rejects_bad_keys_and_values);
|
||||
harness.run("rejects_missing_and_excessive_entries", rejects_missing_and_excessive_entries);
|
||||
return harness.finish();
|
||||
}
|
||||
BIN
tests/data/images/tiny-rgba-header.png
Normal file
BIN
tests/data/images/tiny-rgba-header.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 B |
1
tests/data/images/unsupported-image.txt
Normal file
1
tests/data/images/unsupported-image.txt
Normal file
@@ -0,0 +1 @@
|
||||
GIF8
|
||||
5
tests/data/layouts/simple-layout.xml
Normal file
5
tests/data/layouts/simple-layout.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<layout width="100%" height="auto">
|
||||
<panel width="320" height="200">
|
||||
<button width="64" height="28"/>
|
||||
</panel>
|
||||
</layout>
|
||||
BIN
tests/data/projects/minimal-project.ppi
Normal file
BIN
tests/data/projects/minimal-project.ppi
Normal file
Binary file not shown.
3
tests/data/strokes/two-strokes.ppstroke
Normal file
3
tests/data/strokes/two-strokes.ppstroke
Normal file
@@ -0,0 +1,3 @@
|
||||
# PanoPainter automation stroke script
|
||||
stroke 0 0 0.25 10 0 0.75 2
|
||||
stroke 10 0 1 10 10 0.5 5
|
||||
641
tests/document/document_tests.cpp
Normal file
641
tests/document/document_tests.cpp
Normal file
@@ -0,0 +1,641 @@
|
||||
#include "document/document.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <string_view>
|
||||
|
||||
using pp::paint::BlendMode;
|
||||
using pp::document::CanvasDocument;
|
||||
using pp::document::DocumentHistory;
|
||||
using pp::document::DocumentConfig;
|
||||
using pp::document::DocumentLayerConfig;
|
||||
using pp::document::DocumentSnapshotConfig;
|
||||
using pp::document::AnimationFrame;
|
||||
using pp::document::LayerFacePixels;
|
||||
using pp::document::max_document_history_entries;
|
||||
using pp::document::max_canvas_dimension;
|
||||
using pp::document::max_frame_count;
|
||||
using pp::document::max_layer_count;
|
||||
using pp::document::max_layer_name_length;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
void creates_document_with_default_layers(pp::tests::Harness& h)
|
||||
{
|
||||
const auto document = CanvasDocument::create(
|
||||
DocumentConfig { .width = 128, .height = 64, .layer_count = 2 });
|
||||
|
||||
PP_EXPECT(h, document.ok());
|
||||
PP_EXPECT(h, document.value().width() == 128U);
|
||||
PP_EXPECT(h, document.value().height() == 64U);
|
||||
PP_EXPECT(h, document.value().layers().size() == 2U);
|
||||
PP_EXPECT(h, document.value().layers()[0].name == std::string_view("Layer 1"));
|
||||
PP_EXPECT(h, document.value().layers()[1].name == std::string_view("Layer 2"));
|
||||
PP_EXPECT(h, document.value().active_layer_index() == 0U);
|
||||
PP_EXPECT(h, document.value().frames().size() == 1U);
|
||||
PP_EXPECT(h, document.value().frames()[0].duration_ms == 100U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames.size() == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].duration_ms == 100U);
|
||||
PP_EXPECT(h, document.value().animation_duration_ms() == 100U);
|
||||
PP_EXPECT(h, document.value().active_frame_index() == 0U);
|
||||
}
|
||||
|
||||
void rejects_invalid_document_configs(pp::tests::Harness& h)
|
||||
{
|
||||
const auto zero_width = CanvasDocument::create(
|
||||
DocumentConfig { .width = 0, .height = 64, .layer_count = 1 });
|
||||
const auto huge_width = CanvasDocument::create(
|
||||
DocumentConfig { .width = max_canvas_dimension + 1U, .height = 64, .layer_count = 1 });
|
||||
const auto no_layers = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 0 });
|
||||
const auto too_many_layers = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = max_layer_count + 1U });
|
||||
|
||||
PP_EXPECT(h, !zero_width.ok());
|
||||
PP_EXPECT(h, zero_width.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !huge_width.ok());
|
||||
PP_EXPECT(h, huge_width.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !no_layers.ok());
|
||||
PP_EXPECT(h, no_layers.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !too_many_layers.ok());
|
||||
PP_EXPECT(h, too_many_layers.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void manages_layer_add_remove_and_active_index(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
const auto added = document.add_layer("Paint");
|
||||
PP_EXPECT(h, added.ok());
|
||||
PP_EXPECT(h, added.value() == 1U);
|
||||
PP_EXPECT(h, document.active_layer_index() == 1U);
|
||||
PP_EXPECT(h, document.layers()[1].name == std::string_view("Paint"));
|
||||
|
||||
PP_EXPECT(h, document.remove_layer(0).ok());
|
||||
PP_EXPECT(h, document.layers().size() == 1U);
|
||||
PP_EXPECT(h, document.active_layer_index() == 0U);
|
||||
|
||||
const auto remove_last = document.remove_layer(0);
|
||||
PP_EXPECT(h, !remove_last.ok());
|
||||
PP_EXPECT(h, remove_last.code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void moves_layers_and_preserves_active_layer_identity(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 3 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
PP_EXPECT(h, document.set_active_layer(2).ok());
|
||||
PP_EXPECT(h, document.move_layer(2, 0).ok());
|
||||
PP_EXPECT(h, document.active_layer_index() == 0U);
|
||||
PP_EXPECT(h, document.layers()[0].name == std::string_view("Layer 3"));
|
||||
PP_EXPECT(h, document.layers()[1].name == std::string_view("Layer 1"));
|
||||
PP_EXPECT(h, document.layers()[2].name == std::string_view("Layer 2"));
|
||||
|
||||
const auto bad_move = document.move_layer(4, 0);
|
||||
PP_EXPECT(h, !bad_move.ok());
|
||||
PP_EXPECT(h, bad_move.code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void updates_layer_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 2 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
PP_EXPECT(h, document.rename_layer(1, "Ink").ok());
|
||||
PP_EXPECT(h, document.set_layer_visible(1, false).ok());
|
||||
PP_EXPECT(h, document.set_layer_alpha_locked(1, true).ok());
|
||||
PP_EXPECT(h, document.set_layer_opacity(1, 0.25F).ok());
|
||||
PP_EXPECT(h, document.set_layer_blend_mode(1, BlendMode::multiply).ok());
|
||||
|
||||
PP_EXPECT(h, document.layers()[1].name == std::string_view("Ink"));
|
||||
PP_EXPECT(h, !document.layers()[1].visible);
|
||||
PP_EXPECT(h, document.layers()[1].alpha_locked);
|
||||
PP_EXPECT(h, std::fabs(document.layers()[1].opacity - 0.25F) < 0.0001F);
|
||||
PP_EXPECT(h, document.layers()[1].blend_mode == BlendMode::multiply);
|
||||
}
|
||||
|
||||
void rejects_invalid_layer_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
const auto empty_name = document.rename_layer(0, "");
|
||||
const auto long_name = document.rename_layer(0, std::string(max_layer_name_length + 1U, 'x'));
|
||||
const auto missing_name = document.rename_layer(4, "Missing");
|
||||
const auto bad_opacity_low = document.set_layer_opacity(0, -0.1F);
|
||||
const auto bad_opacity_high = document.set_layer_opacity(0, 1.1F);
|
||||
const auto bad_opacity_nan = document.set_layer_opacity(0, std::nanf(""));
|
||||
const auto missing_visible = document.set_layer_visible(2, true);
|
||||
const auto missing_alpha_lock = document.set_layer_alpha_locked(2, true);
|
||||
const auto missing_blend = document.set_layer_blend_mode(2, BlendMode::normal);
|
||||
const auto bad_blend = document.set_layer_blend_mode(0, static_cast<BlendMode>(255));
|
||||
const auto bad_add_layer = document.add_layer(std::string(max_layer_name_length + 1U, 'x'));
|
||||
|
||||
PP_EXPECT(h, !empty_name.ok());
|
||||
PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !long_name.ok());
|
||||
PP_EXPECT(h, long_name.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_name.ok());
|
||||
PP_EXPECT(h, missing_name.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !bad_opacity_low.ok());
|
||||
PP_EXPECT(h, bad_opacity_low.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !bad_opacity_high.ok());
|
||||
PP_EXPECT(h, bad_opacity_high.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !bad_opacity_nan.ok());
|
||||
PP_EXPECT(h, bad_opacity_nan.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_visible.ok());
|
||||
PP_EXPECT(h, missing_visible.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_alpha_lock.ok());
|
||||
PP_EXPECT(h, missing_alpha_lock.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_blend.ok());
|
||||
PP_EXPECT(h, missing_blend.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !bad_blend.ok());
|
||||
PP_EXPECT(h, bad_blend.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_add_layer.ok());
|
||||
PP_EXPECT(h, bad_add_layer.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void creates_document_from_snapshot_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
const DocumentLayerConfig layers[] {
|
||||
{
|
||||
.name = "Ink",
|
||||
.visible = false,
|
||||
.alpha_locked = true,
|
||||
.opacity = 0.5F,
|
||||
.blend_mode = BlendMode::screen,
|
||||
.frames = {},
|
||||
},
|
||||
{
|
||||
.name = "Glaze",
|
||||
.visible = true,
|
||||
.alpha_locked = false,
|
||||
.opacity = 0.75F,
|
||||
.blend_mode = BlendMode::overlay,
|
||||
.frames = {},
|
||||
},
|
||||
};
|
||||
const AnimationFrame frames[] {
|
||||
{ .duration_ms = 100, .face_pixels = {} },
|
||||
{ .duration_ms = 250, .face_pixels = {} },
|
||||
};
|
||||
|
||||
const auto document_result = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 128,
|
||||
.height = 64,
|
||||
.layers = layers,
|
||||
.frames = frames,
|
||||
});
|
||||
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
PP_EXPECT(h, document_result.value().layers().size() == 2U);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].name == std::string_view("Ink"));
|
||||
PP_EXPECT(h, !document_result.value().layers()[0].visible);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].alpha_locked);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].blend_mode == BlendMode::screen);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].frames.size() == 2U);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].frames[1].duration_ms == 250U);
|
||||
PP_EXPECT(h, document_result.value().frames().size() == 2U);
|
||||
PP_EXPECT(h, document_result.value().animation_duration_ms() == 350U);
|
||||
}
|
||||
|
||||
void preserves_per_layer_snapshot_timelines(pp::tests::Harness& h)
|
||||
{
|
||||
const AnimationFrame project_frames[] {
|
||||
{ .duration_ms = 100, .face_pixels = {} },
|
||||
};
|
||||
const AnimationFrame short_layer_frames[] {
|
||||
{ .duration_ms = 100, .face_pixels = {} },
|
||||
{ .duration_ms = 150, .face_pixels = {} },
|
||||
};
|
||||
const AnimationFrame long_layer_frames[] {
|
||||
{ .duration_ms = 500, .face_pixels = {} },
|
||||
};
|
||||
const DocumentLayerConfig layers[] {
|
||||
{
|
||||
.name = "Short",
|
||||
.frames = short_layer_frames,
|
||||
},
|
||||
{
|
||||
.name = "Long",
|
||||
.frames = long_layer_frames,
|
||||
},
|
||||
};
|
||||
|
||||
const auto document_result = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 128,
|
||||
.height = 64,
|
||||
.layers = layers,
|
||||
.frames = project_frames,
|
||||
});
|
||||
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
PP_EXPECT(h, document_result.value().frames().size() == 1U);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].frames.size() == 2U);
|
||||
PP_EXPECT(h, document_result.value().layers()[1].frames.size() == 1U);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].frames[1].duration_ms == 150U);
|
||||
PP_EXPECT(h, document_result.value().layers()[1].frames[0].duration_ms == 500U);
|
||||
PP_EXPECT(h, document_result.value().animation_duration_ms() == 500U);
|
||||
const auto layer_duration = document_result.value().layer_animation_duration_ms(0);
|
||||
PP_EXPECT(h, layer_duration.ok());
|
||||
PP_EXPECT(h, layer_duration.value() == 250U);
|
||||
}
|
||||
|
||||
void rejects_invalid_snapshot_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
const DocumentLayerConfig layers[] { { .name = "Ink", .frames = {} } };
|
||||
const AnimationFrame frames[] { { .duration_ms = 100, .face_pixels = {} } };
|
||||
const AnimationFrame bad_frames[] { { .duration_ms = 0, .face_pixels = {} } };
|
||||
const DocumentLayerConfig bad_layers[] { { .name = "", .frames = {} } };
|
||||
const DocumentLayerConfig bad_layer_frames[] { { .name = "Ink", .frames = bad_frames } };
|
||||
|
||||
const auto no_layers = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = {},
|
||||
.frames = frames,
|
||||
});
|
||||
const auto no_frames = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = layers,
|
||||
.frames = {},
|
||||
});
|
||||
const auto bad_frame = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = layers,
|
||||
.frames = bad_frames,
|
||||
});
|
||||
const auto bad_layer = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = bad_layers,
|
||||
.frames = frames,
|
||||
});
|
||||
const auto bad_layer_frame = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = bad_layer_frames,
|
||||
.frames = frames,
|
||||
});
|
||||
|
||||
PP_EXPECT(h, !no_layers.ok());
|
||||
PP_EXPECT(h, no_layers.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !no_frames.ok());
|
||||
PP_EXPECT(h, no_frames.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_frame.ok());
|
||||
PP_EXPECT(h, bad_frame.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_layer.ok());
|
||||
PP_EXPECT(h, bad_layer.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_layer_frame.ok());
|
||||
PP_EXPECT(h, bad_layer_frame.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void manages_animation_frames_and_duration(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
const auto added = document.add_frame(250);
|
||||
PP_EXPECT(h, added.ok());
|
||||
PP_EXPECT(h, added.value() == 1U);
|
||||
PP_EXPECT(h, document.active_frame_index() == 1U);
|
||||
PP_EXPECT(h, document.frames()[1].duration_ms == 250U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[1].duration_ms == 250U);
|
||||
|
||||
const auto duplicated = document.duplicate_frame(1);
|
||||
PP_EXPECT(h, duplicated.ok());
|
||||
PP_EXPECT(h, duplicated.value() == 2U);
|
||||
PP_EXPECT(h, document.frames()[2].duration_ms == 250U);
|
||||
PP_EXPECT(h, document.set_frame_duration(2, 333).ok());
|
||||
PP_EXPECT(h, document.frames()[2].duration_ms == 333U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[2].duration_ms == 333U);
|
||||
PP_EXPECT(h, document.animation_duration_ms() == 683U);
|
||||
|
||||
PP_EXPECT(h, document.remove_frame(1).ok());
|
||||
PP_EXPECT(h, document.frames().size() == 2U);
|
||||
PP_EXPECT(h, document.active_frame_index() == 1U);
|
||||
PP_EXPECT(h, document.animation_duration_ms() == 433U);
|
||||
}
|
||||
|
||||
void moves_frames_and_preserves_active_frame_identity(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
PP_EXPECT(h, document.set_frame_duration(0, 100).ok());
|
||||
PP_EXPECT(h, document.add_frame(200).ok());
|
||||
PP_EXPECT(h, document.add_frame(300).ok());
|
||||
PP_EXPECT(h, document.add_frame(400).ok());
|
||||
|
||||
PP_EXPECT(h, document.set_active_frame(2).ok());
|
||||
PP_EXPECT(h, document.move_frame(2, 0).ok());
|
||||
PP_EXPECT(h, document.active_frame_index() == 0U);
|
||||
PP_EXPECT(h, document.frames()[0].duration_ms == 300U);
|
||||
PP_EXPECT(h, document.frames()[1].duration_ms == 100U);
|
||||
PP_EXPECT(h, document.frames()[2].duration_ms == 200U);
|
||||
PP_EXPECT(h, document.frames()[3].duration_ms == 400U);
|
||||
|
||||
PP_EXPECT(h, document.move_frame(3, 1).ok());
|
||||
PP_EXPECT(h, document.active_frame_index() == 0U);
|
||||
PP_EXPECT(h, document.frames()[1].duration_ms == 400U);
|
||||
PP_EXPECT(h, document.animation_duration_ms() == 1000U);
|
||||
|
||||
const auto missing_from = document.move_frame(9, 0);
|
||||
const auto missing_to = document.move_frame(0, 9);
|
||||
PP_EXPECT(h, !missing_from.ok());
|
||||
PP_EXPECT(h, missing_from.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_to.ok());
|
||||
PP_EXPECT(h, missing_to.code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void rejects_invalid_animation_frame_operations(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
const auto zero_duration = document.add_frame(0);
|
||||
const auto duplicate_missing = document.duplicate_frame(9);
|
||||
const auto remove_missing = document.remove_frame(9);
|
||||
const auto remove_only = document.remove_frame(0);
|
||||
const auto set_bad_duration = document.set_frame_duration(0, 0);
|
||||
const auto set_missing_active = document.set_active_frame(2);
|
||||
|
||||
PP_EXPECT(h, !zero_duration.ok());
|
||||
PP_EXPECT(h, zero_duration.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !duplicate_missing.ok());
|
||||
PP_EXPECT(h, duplicate_missing.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !remove_missing.ok());
|
||||
PP_EXPECT(h, remove_missing.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !remove_only.ok());
|
||||
PP_EXPECT(h, remove_only.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !set_bad_duration.ok());
|
||||
PP_EXPECT(h, set_bad_duration.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !set_missing_active.ok());
|
||||
PP_EXPECT(h, set_missing_active.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, max_frame_count > document.frames().size());
|
||||
}
|
||||
|
||||
void attaches_layer_frame_face_pixels(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 32, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
const auto status = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels {
|
||||
.face_index = 2,
|
||||
.x = 3,
|
||||
.y = 4,
|
||||
.width = 1,
|
||||
.height = 1,
|
||||
.rgba8 = { 10, 20, 30, 40 },
|
||||
});
|
||||
|
||||
PP_EXPECT(h, status.ok());
|
||||
PP_EXPECT(h, document.face_pixel_payload_count() == 1U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels.size() == 1U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].face_index == 2U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].x == 3U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].rgba8[3] == 40U);
|
||||
}
|
||||
|
||||
void replaces_existing_face_pixel_payload(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 32, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
PP_EXPECT(h, document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels {
|
||||
.face_index = 1,
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = 1,
|
||||
.height = 1,
|
||||
.rgba8 = { 1, 2, 3, 4 },
|
||||
}).ok());
|
||||
PP_EXPECT(h, document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels {
|
||||
.face_index = 1,
|
||||
.x = 2,
|
||||
.y = 3,
|
||||
.width = 1,
|
||||
.height = 1,
|
||||
.rgba8 = { 5, 6, 7, 8 },
|
||||
}).ok());
|
||||
|
||||
PP_EXPECT(h, document.face_pixel_payload_count() == 1U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].x == 2U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].rgba8[0] == 5U);
|
||||
}
|
||||
|
||||
void rejects_invalid_face_pixel_payloads(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 32, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
const auto missing_layer = document.set_layer_frame_face_pixels(
|
||||
9,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3, 4 } });
|
||||
const auto missing_frame = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
9,
|
||||
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3, 4 } });
|
||||
const auto bad_face = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 6, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3, 4 } });
|
||||
const auto zero_width = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 0, .height = 1, .rgba8 = {} });
|
||||
const auto outside_bounds = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 0, .x = 63, .y = 0, .width = 2, .height = 1, .rgba8 = { 1, 2, 3, 4, 5, 6, 7, 8 } });
|
||||
const auto bad_byte_count = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3 } });
|
||||
|
||||
PP_EXPECT(h, !missing_layer.ok());
|
||||
PP_EXPECT(h, missing_layer.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_frame.ok());
|
||||
PP_EXPECT(h, missing_frame.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !bad_face.ok());
|
||||
PP_EXPECT(h, bad_face.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !zero_width.ok());
|
||||
PP_EXPECT(h, zero_width.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !outside_bounds.ok());
|
||||
PP_EXPECT(h, outside_bounds.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !bad_byte_count.ok());
|
||||
PP_EXPECT(h, bad_byte_count.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, document.face_pixel_payload_count() == 0U);
|
||||
}
|
||||
|
||||
void records_document_history_and_restores_snapshots(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
|
||||
auto history_result = DocumentHistory::create(document_result.value(), 4);
|
||||
PP_EXPECT(h, history_result.ok());
|
||||
auto history = history_result.value();
|
||||
|
||||
auto with_layer = history.current();
|
||||
const auto added_layer = with_layer.add_layer("Paint");
|
||||
PP_EXPECT(h, added_layer.ok());
|
||||
PP_EXPECT(h, history.apply(with_layer).ok());
|
||||
|
||||
auto with_frame = history.current();
|
||||
const auto added_frame = with_frame.add_frame(250);
|
||||
PP_EXPECT(h, added_frame.ok());
|
||||
PP_EXPECT(h, history.apply(with_frame).ok());
|
||||
|
||||
PP_EXPECT(h, history.size() == 3U);
|
||||
PP_EXPECT(h, history.current_index() == 2U);
|
||||
PP_EXPECT(h, history.current().layers().size() == 2U);
|
||||
PP_EXPECT(h, history.current().frames().size() == 2U);
|
||||
PP_EXPECT(h, history.can_undo());
|
||||
PP_EXPECT(h, !history.can_redo());
|
||||
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.current().layers().size() == 2U);
|
||||
PP_EXPECT(h, history.current().frames().size() == 1U);
|
||||
PP_EXPECT(h, history.can_redo());
|
||||
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.current().layers().size() == 1U);
|
||||
PP_EXPECT(h, history.current().frames().size() == 1U);
|
||||
const auto undo_past_start = history.undo();
|
||||
PP_EXPECT(h, !undo_past_start.ok());
|
||||
PP_EXPECT(h, undo_past_start.code == StatusCode::out_of_range);
|
||||
|
||||
PP_EXPECT(h, history.redo().ok());
|
||||
PP_EXPECT(h, history.current().layers().size() == 2U);
|
||||
}
|
||||
|
||||
void applying_after_undo_discards_redo_branch(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
|
||||
auto history_result = DocumentHistory::create(document_result.value(), 5);
|
||||
PP_EXPECT(h, history_result.ok());
|
||||
auto history = history_result.value();
|
||||
|
||||
auto first_branch = history.current();
|
||||
PP_EXPECT(h, first_branch.add_layer("Branch A").ok());
|
||||
PP_EXPECT(h, history.apply(first_branch).ok());
|
||||
|
||||
auto second_branch = history.current();
|
||||
PP_EXPECT(h, second_branch.add_layer("Branch B").ok());
|
||||
PP_EXPECT(h, history.apply(second_branch).ok());
|
||||
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.can_redo());
|
||||
|
||||
auto replacement_branch = history.current();
|
||||
PP_EXPECT(h, replacement_branch.add_layer("Replacement").ok());
|
||||
PP_EXPECT(h, history.apply(replacement_branch).ok());
|
||||
|
||||
PP_EXPECT(h, !history.can_redo());
|
||||
PP_EXPECT(h, history.current().layers().size() == 3U);
|
||||
PP_EXPECT(h, history.current().layers()[2].name == std::string_view("Replacement"));
|
||||
}
|
||||
|
||||
void bounds_document_history_capacity(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
|
||||
auto too_small = DocumentHistory::create(document_result.value(), 1);
|
||||
auto too_large = DocumentHistory::create(document_result.value(), max_document_history_entries + 1U);
|
||||
PP_EXPECT(h, !too_small.ok());
|
||||
PP_EXPECT(h, too_small.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !too_large.ok());
|
||||
PP_EXPECT(h, too_large.status().code == StatusCode::out_of_range);
|
||||
|
||||
auto history_result = DocumentHistory::create(document_result.value(), 3);
|
||||
PP_EXPECT(h, history_result.ok());
|
||||
auto history = history_result.value();
|
||||
|
||||
for (std::uint32_t i = 0; i < 5U; ++i) {
|
||||
auto next = history.current();
|
||||
const auto added = next.add_frame(100U + i);
|
||||
PP_EXPECT(h, added.ok());
|
||||
PP_EXPECT(h, history.apply(next).ok());
|
||||
PP_EXPECT(h, history.size() <= 3U);
|
||||
}
|
||||
|
||||
PP_EXPECT(h, history.size() == 3U);
|
||||
PP_EXPECT(h, history.current_index() == 2U);
|
||||
PP_EXPECT(h, history.current().frames().size() == 6U);
|
||||
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.current().frames().size() == 5U);
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.current().frames().size() == 4U);
|
||||
const auto undo_evicted_entry = history.undo();
|
||||
PP_EXPECT(h, !undo_evicted_entry.ok());
|
||||
PP_EXPECT(h, undo_evicted_entry.code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("creates_document_with_default_layers", creates_document_with_default_layers);
|
||||
harness.run("rejects_invalid_document_configs", rejects_invalid_document_configs);
|
||||
harness.run("manages_layer_add_remove_and_active_index", manages_layer_add_remove_and_active_index);
|
||||
harness.run("moves_layers_and_preserves_active_layer_identity", moves_layers_and_preserves_active_layer_identity);
|
||||
harness.run("updates_layer_metadata", updates_layer_metadata);
|
||||
harness.run("rejects_invalid_layer_metadata", rejects_invalid_layer_metadata);
|
||||
harness.run("creates_document_from_snapshot_metadata", creates_document_from_snapshot_metadata);
|
||||
harness.run("preserves_per_layer_snapshot_timelines", preserves_per_layer_snapshot_timelines);
|
||||
harness.run("rejects_invalid_snapshot_metadata", rejects_invalid_snapshot_metadata);
|
||||
harness.run("manages_animation_frames_and_duration", manages_animation_frames_and_duration);
|
||||
harness.run("moves_frames_and_preserves_active_frame_identity", moves_frames_and_preserves_active_frame_identity);
|
||||
harness.run("rejects_invalid_animation_frame_operations", rejects_invalid_animation_frame_operations);
|
||||
harness.run("attaches_layer_frame_face_pixels", attaches_layer_frame_face_pixels);
|
||||
harness.run("replaces_existing_face_pixel_payload", replaces_existing_face_pixel_payload);
|
||||
harness.run("rejects_invalid_face_pixel_payloads", rejects_invalid_face_pixel_payloads);
|
||||
harness.run("records_document_history_and_restores_snapshots", records_document_history_and_restores_snapshots);
|
||||
harness.run("applying_after_undo_discards_redo_branch", applying_after_undo_discards_redo_branch);
|
||||
harness.run("bounds_document_history_capacity", bounds_document_history_capacity);
|
||||
return harness.finish();
|
||||
}
|
||||
148
tests/document/ppi_import_tests.cpp
Normal file
148
tests/document/ppi_import_tests.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "assets/ppi_header.h"
|
||||
#include "document/ppi_import.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using pp::assets::decode_ppi_project_images;
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::document::import_ppi_project_document;
|
||||
|
||||
namespace {
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::byte> transparent_png_1x1()
|
||||
{
|
||||
return {
|
||||
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
|
||||
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
|
||||
std::byte { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 },
|
||||
std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 },
|
||||
std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 },
|
||||
std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 },
|
||||
std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 },
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<std::byte> ppi_project_with_face_payload(std::vector<std::byte> payload)
|
||||
{
|
||||
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, 2);
|
||||
append_u32(bytes, 3);
|
||||
append_u32(bytes, 1024);
|
||||
append_u32(bytes, 128);
|
||||
append_u32(bytes, 128);
|
||||
append_u32(bytes, 4);
|
||||
bytes.resize(pp::assets::ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
|
||||
|
||||
append_u32(bytes, 64);
|
||||
append_u32(bytes, 32);
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 0);
|
||||
append_f32(bytes, 1.0F);
|
||||
append_u32(bytes, 3);
|
||||
append_ascii(bytes, "Ink");
|
||||
append_u32(bytes, 0);
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 1 });
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 100);
|
||||
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 2);
|
||||
append_u32(bytes, 3);
|
||||
append_u32(bytes, 3);
|
||||
append_u32(bytes, 4);
|
||||
append_u32(bytes, static_cast<std::uint32_t>(payload.size()));
|
||||
bytes.insert(bytes.end(), payload.begin(), payload.end());
|
||||
for (std::uint32_t i = 1; i < 6U; ++i) {
|
||||
append_u32(bytes, 0);
|
||||
}
|
||||
append_u32(bytes, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void imports_decoded_ppi_pixels_into_document(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project_bytes = ppi_project_with_face_payload(transparent_png_1x1());
|
||||
const auto decoded = decode_ppi_project_images(project_bytes);
|
||||
PP_EXPECT(h, decoded.ok());
|
||||
|
||||
const auto document = import_ppi_project_document(decoded.value());
|
||||
|
||||
PP_EXPECT(h, document.ok());
|
||||
PP_EXPECT(h, document.value().width() == 64U);
|
||||
PP_EXPECT(h, document.value().height() == 32U);
|
||||
PP_EXPECT(h, document.value().face_pixel_payload_count() == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels.size() == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].face_index == 0U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].x == 2U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].y == 3U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].width == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].height == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].rgba8.size() == 4U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].rgba8[3] == 0U);
|
||||
}
|
||||
|
||||
void rejects_decoded_payloads_outside_document_layers(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project_bytes = ppi_project_with_face_payload(transparent_png_1x1());
|
||||
const auto decoded = decode_ppi_project_images(project_bytes);
|
||||
PP_EXPECT(h, decoded.ok());
|
||||
auto decoded_value = decoded.value();
|
||||
decoded_value.faces[0].layer_index = 99;
|
||||
|
||||
const auto document = import_ppi_project_document(decoded_value);
|
||||
|
||||
PP_EXPECT(h, !document.ok());
|
||||
PP_EXPECT(h, document.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("imports_decoded_ppi_pixels_into_document", imports_decoded_ppi_pixels_into_document);
|
||||
harness.run("rejects_decoded_payloads_outside_document_layers", rejects_decoded_payloads_outside_document_layers);
|
||||
return harness.finish();
|
||||
}
|
||||
100
tests/foundation/binary_stream_tests.cpp
Normal file
100
tests/foundation/binary_stream_tests.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#include "foundation/binary_stream.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
using pp::foundation::ByteReader;
|
||||
using pp::foundation::ByteWriter;
|
||||
|
||||
namespace {
|
||||
|
||||
void round_trips_little_endian_values(pp::tests::Harness& h)
|
||||
{
|
||||
std::vector<std::byte> bytes;
|
||||
ByteWriter writer(bytes);
|
||||
|
||||
PP_EXPECT(h, writer.write_u8(0x12U).ok());
|
||||
PP_EXPECT(h, writer.write_u16_le(0x3456U).ok());
|
||||
PP_EXPECT(h, writer.write_u32_le(0x789abcdeU).ok());
|
||||
PP_EXPECT(h, writer.size() == 7U);
|
||||
|
||||
ByteReader reader(bytes);
|
||||
const auto u8 = reader.read_u8();
|
||||
const auto u16 = reader.read_u16_le();
|
||||
const auto u32 = reader.read_u32_le();
|
||||
|
||||
PP_EXPECT(h, u8.ok());
|
||||
PP_EXPECT(h, u8.value() == 0x12U);
|
||||
PP_EXPECT(h, u16.ok());
|
||||
PP_EXPECT(h, u16.value() == 0x3456U);
|
||||
PP_EXPECT(h, u32.ok());
|
||||
PP_EXPECT(h, u32.value() == 0x789abcdeU);
|
||||
PP_EXPECT(h, reader.empty());
|
||||
}
|
||||
|
||||
void rejects_overread_without_moving_cursor(pp::tests::Harness& h)
|
||||
{
|
||||
const std::array bytes {
|
||||
std::byte { 0x01 },
|
||||
std::byte { 0x02 },
|
||||
std::byte { 0x03 },
|
||||
};
|
||||
ByteReader reader(bytes);
|
||||
|
||||
PP_EXPECT(h, reader.seek(2).ok());
|
||||
const auto before = reader.position();
|
||||
const auto value = reader.read_u32_le();
|
||||
|
||||
PP_EXPECT(h, !value.ok());
|
||||
PP_EXPECT(h, reader.position() == before);
|
||||
PP_EXPECT(h, reader.remaining() == 1U);
|
||||
}
|
||||
|
||||
void rejects_out_of_range_seek(pp::tests::Harness& h)
|
||||
{
|
||||
const std::array bytes {
|
||||
std::byte { 0x01 },
|
||||
std::byte { 0x02 },
|
||||
};
|
||||
ByteReader reader(bytes);
|
||||
|
||||
PP_EXPECT(h, !reader.seek(3).ok());
|
||||
PP_EXPECT(h, reader.position() == 0U);
|
||||
PP_EXPECT(h, reader.seek(2).ok());
|
||||
PP_EXPECT(h, reader.empty());
|
||||
}
|
||||
|
||||
void boundary_reads_are_consistent(pp::tests::Harness& h)
|
||||
{
|
||||
std::array<std::byte, 16> bytes {};
|
||||
for (std::size_t i = 0; i < bytes.size(); ++i) {
|
||||
bytes[i] = static_cast<std::byte>(i);
|
||||
}
|
||||
|
||||
for (std::size_t length = 0; length <= bytes.size(); ++length) {
|
||||
ByteReader reader(std::span<const std::byte>(bytes.data(), length));
|
||||
const auto exact = reader.read_bytes(length);
|
||||
PP_EXPECT(h, exact.ok());
|
||||
PP_EXPECT(h, exact.value().size() == length);
|
||||
PP_EXPECT(h, reader.empty());
|
||||
|
||||
const auto too_much = reader.read_u8();
|
||||
PP_EXPECT(h, !too_much.ok());
|
||||
PP_EXPECT(h, reader.position() == length);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("round_trips_little_endian_values", round_trips_little_endian_values);
|
||||
harness.run("rejects_overread_without_moving_cursor", rejects_overread_without_moving_cursor);
|
||||
harness.run("rejects_out_of_range_seek", rejects_out_of_range_seek);
|
||||
harness.run("boundary_reads_are_consistent", boundary_reads_are_consistent);
|
||||
return harness.finish();
|
||||
}
|
||||
136
tests/foundation/event_tests.cpp
Normal file
136
tests/foundation/event_tests.cpp
Normal file
@@ -0,0 +1,136 @@
|
||||
#include "foundation/event.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
using pp::foundation::Event;
|
||||
using pp::foundation::EventDispatcher;
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::foundation::max_event_subscriptions;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Receiver {
|
||||
int count = 0;
|
||||
std::uint64_t payload_sum = 0;
|
||||
std::uint64_t last_source = 0;
|
||||
};
|
||||
|
||||
void receive_event(const Event& event, void* user_data) noexcept
|
||||
{
|
||||
auto* receiver = static_cast<Receiver*>(user_data);
|
||||
++receiver->count;
|
||||
receiver->payload_sum += event.payload_u64;
|
||||
receiver->last_source = event.source_id;
|
||||
}
|
||||
|
||||
void subscribe_and_publish_matching_events(pp::tests::Harness& h)
|
||||
{
|
||||
EventDispatcher dispatcher(4);
|
||||
Receiver receiver;
|
||||
|
||||
const auto subscription = dispatcher.subscribe(7, receive_event, &receiver);
|
||||
PP_EXPECT(h, subscription.ok());
|
||||
PP_EXPECT(h, subscription.value() == 1U);
|
||||
PP_EXPECT(h, dispatcher.size() == 1U);
|
||||
PP_EXPECT(h, dispatcher.max_subscriptions() == 4U);
|
||||
|
||||
const auto delivered = dispatcher.publish(Event {
|
||||
.type = 7,
|
||||
.source_id = 42,
|
||||
.frame_id = 3,
|
||||
.payload_u64 = 11,
|
||||
});
|
||||
|
||||
PP_EXPECT(h, delivered == 1U);
|
||||
PP_EXPECT(h, receiver.count == 1);
|
||||
PP_EXPECT(h, receiver.payload_sum == 11U);
|
||||
PP_EXPECT(h, receiver.last_source == 42U);
|
||||
}
|
||||
|
||||
void ignores_non_matching_or_zero_events(pp::tests::Harness& h)
|
||||
{
|
||||
EventDispatcher dispatcher(4);
|
||||
Receiver receiver;
|
||||
|
||||
PP_EXPECT(h, dispatcher.subscribe(2, receive_event, &receiver).ok());
|
||||
PP_EXPECT(h, dispatcher.publish(Event { .type = 3, .payload_u64 = 1 }) == 0U);
|
||||
PP_EXPECT(h, dispatcher.publish(Event { .type = 0, .payload_u64 = 1 }) == 0U);
|
||||
PP_EXPECT(h, receiver.count == 0);
|
||||
}
|
||||
|
||||
void preserves_subscription_order_and_unsubscribes(pp::tests::Harness& h)
|
||||
{
|
||||
EventDispatcher dispatcher(4);
|
||||
Receiver first;
|
||||
Receiver second;
|
||||
|
||||
const auto first_subscription = dispatcher.subscribe(9, receive_event, &first);
|
||||
const auto second_subscription = dispatcher.subscribe(9, receive_event, &second);
|
||||
PP_EXPECT(h, first_subscription.ok());
|
||||
PP_EXPECT(h, second_subscription.ok());
|
||||
|
||||
PP_EXPECT(h, dispatcher.publish(Event { .type = 9, .payload_u64 = 5 }) == 2U);
|
||||
PP_EXPECT(h, first.payload_sum == 5U);
|
||||
PP_EXPECT(h, second.payload_sum == 5U);
|
||||
|
||||
PP_EXPECT(h, dispatcher.unsubscribe(first_subscription.value()).ok());
|
||||
PP_EXPECT(h, dispatcher.publish(Event { .type = 9, .payload_u64 = 7 }) == 1U);
|
||||
PP_EXPECT(h, first.payload_sum == 5U);
|
||||
PP_EXPECT(h, second.payload_sum == 12U);
|
||||
|
||||
const auto missing = dispatcher.unsubscribe(first_subscription.value());
|
||||
PP_EXPECT(h, !missing.ok());
|
||||
PP_EXPECT(h, missing.code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void rejects_invalid_subscriptions_and_capacity(pp::tests::Harness& h)
|
||||
{
|
||||
EventDispatcher dispatcher(1);
|
||||
EventDispatcher zero_capacity(0);
|
||||
EventDispatcher excessive_capacity(max_event_subscriptions + 1U);
|
||||
Receiver receiver;
|
||||
|
||||
const auto zero_type = dispatcher.subscribe(0, receive_event, &receiver);
|
||||
const auto null_callback = dispatcher.subscribe(1, nullptr, &receiver);
|
||||
PP_EXPECT(h, !zero_type.ok());
|
||||
PP_EXPECT(h, zero_type.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !null_callback.ok());
|
||||
PP_EXPECT(h, null_callback.status().code == StatusCode::invalid_argument);
|
||||
|
||||
PP_EXPECT(h, dispatcher.subscribe(1, receive_event, &receiver).ok());
|
||||
const auto full = dispatcher.subscribe(2, receive_event, &receiver);
|
||||
PP_EXPECT(h, !full.ok());
|
||||
PP_EXPECT(h, full.status().code == StatusCode::out_of_range);
|
||||
|
||||
const auto zero_capacity_result = zero_capacity.subscribe(1, receive_event, &receiver);
|
||||
const auto excessive_capacity_result = excessive_capacity.subscribe(1, receive_event, &receiver);
|
||||
PP_EXPECT(h, !zero_capacity_result.ok());
|
||||
PP_EXPECT(h, zero_capacity_result.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !excessive_capacity_result.ok());
|
||||
PP_EXPECT(h, excessive_capacity_result.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void clear_removes_all_subscriptions(pp::tests::Harness& h)
|
||||
{
|
||||
EventDispatcher dispatcher(4);
|
||||
Receiver receiver;
|
||||
|
||||
PP_EXPECT(h, dispatcher.subscribe(1, receive_event, &receiver).ok());
|
||||
PP_EXPECT(h, dispatcher.subscribe(2, receive_event, &receiver).ok());
|
||||
dispatcher.clear();
|
||||
PP_EXPECT(h, dispatcher.empty());
|
||||
PP_EXPECT(h, dispatcher.publish(Event { .type = 1, .payload_u64 = 5 }) == 0U);
|
||||
PP_EXPECT(h, receiver.count == 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("subscribe_and_publish_matching_events", subscribe_and_publish_matching_events);
|
||||
harness.run("ignores_non_matching_or_zero_events", ignores_non_matching_or_zero_events);
|
||||
harness.run("preserves_subscription_order_and_unsubscribes", preserves_subscription_order_and_unsubscribes);
|
||||
harness.run("rejects_invalid_subscriptions_and_capacity", rejects_invalid_subscriptions_and_capacity);
|
||||
harness.run("clear_removes_all_subscriptions", clear_removes_all_subscriptions);
|
||||
return harness.finish();
|
||||
}
|
||||
82
tests/foundation/log_tests.cpp
Normal file
82
tests/foundation/log_tests.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "foundation/log.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
using pp::foundation::LogLevel;
|
||||
using pp::foundation::Logger;
|
||||
using pp::foundation::MemoryLogSink;
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::foundation::log_level_name;
|
||||
|
||||
namespace {
|
||||
|
||||
void writes_structured_records(pp::tests::Harness& h)
|
||||
{
|
||||
MemoryLogSink sink;
|
||||
Logger logger(sink);
|
||||
|
||||
const auto status = logger.write(LogLevel::info, "paint", "stroke committed", 7, 11, 3);
|
||||
|
||||
PP_EXPECT(h, status.ok());
|
||||
PP_EXPECT(h, sink.records().size() == 1U);
|
||||
PP_EXPECT(h, sink.records()[0].level == LogLevel::info);
|
||||
PP_EXPECT(h, sink.records()[0].component == std::string_view("paint"));
|
||||
PP_EXPECT(h, sink.records()[0].message == std::string_view("stroke committed"));
|
||||
PP_EXPECT(h, sink.records()[0].frame_id == 7U);
|
||||
PP_EXPECT(h, sink.records()[0].stroke_id == 11U);
|
||||
PP_EXPECT(h, sink.records()[0].thread_id == 3U);
|
||||
}
|
||||
|
||||
void filters_below_minimum_level(pp::tests::Harness& h)
|
||||
{
|
||||
MemoryLogSink sink;
|
||||
Logger logger(sink);
|
||||
logger.set_min_level(LogLevel::warning);
|
||||
|
||||
PP_EXPECT(h, logger.min_level() == LogLevel::warning);
|
||||
PP_EXPECT(h, logger.write(LogLevel::debug, "ui", "layout pass").ok());
|
||||
PP_EXPECT(h, logger.write(LogLevel::warning, "ui", "slow layout").ok());
|
||||
PP_EXPECT(h, sink.records().size() == 1U);
|
||||
PP_EXPECT(h, sink.records()[0].level == LogLevel::warning);
|
||||
}
|
||||
|
||||
void rejects_empty_component_or_message(pp::tests::Harness& h)
|
||||
{
|
||||
MemoryLogSink sink;
|
||||
Logger logger(sink);
|
||||
|
||||
const auto empty_component = logger.write(LogLevel::error, "", "message");
|
||||
const auto empty_message = logger.write(LogLevel::error, "renderer", "");
|
||||
|
||||
PP_EXPECT(h, !empty_component.ok());
|
||||
PP_EXPECT(h, empty_component.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !empty_message.ok());
|
||||
PP_EXPECT(h, empty_message.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, sink.records().empty());
|
||||
}
|
||||
|
||||
void exposes_stable_level_names_and_clear(pp::tests::Harness& h)
|
||||
{
|
||||
MemoryLogSink sink;
|
||||
Logger logger(sink);
|
||||
|
||||
PP_EXPECT(h, log_level_name(LogLevel::trace) == std::string_view("trace"));
|
||||
PP_EXPECT(h, log_level_name(LogLevel::error) == std::string_view("error"));
|
||||
PP_EXPECT(h, logger.write(LogLevel::info, "assets", "loaded").ok());
|
||||
PP_EXPECT(h, sink.records().size() == 1U);
|
||||
sink.clear();
|
||||
PP_EXPECT(h, sink.records().empty());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("writes_structured_records", writes_structured_records);
|
||||
harness.run("filters_below_minimum_level", filters_below_minimum_level);
|
||||
harness.run("rejects_empty_component_or_message", rejects_empty_component_or_message);
|
||||
harness.run("exposes_stable_level_names_and_clear", exposes_stable_level_names_and_clear);
|
||||
return harness.finish();
|
||||
}
|
||||
66
tests/foundation/parse_tests.cpp
Normal file
66
tests/foundation/parse_tests.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
#include "foundation/parse.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
using pp::foundation::parse_u32;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
void accepts_decimal_uint32_values(pp::tests::Harness& h)
|
||||
{
|
||||
const auto zero = parse_u32("0");
|
||||
const auto ordinary = parse_u32("12345");
|
||||
const auto max = parse_u32("4294967295");
|
||||
|
||||
PP_EXPECT(h, zero.ok());
|
||||
PP_EXPECT(h, zero.value() == 0U);
|
||||
PP_EXPECT(h, ordinary.ok());
|
||||
PP_EXPECT(h, ordinary.value() == 12345U);
|
||||
PP_EXPECT(h, max.ok());
|
||||
PP_EXPECT(h, max.value() == UINT32_MAX);
|
||||
}
|
||||
|
||||
void rejects_empty_signed_and_mixed_input(pp::tests::Harness& h)
|
||||
{
|
||||
const auto empty = parse_u32("");
|
||||
const auto negative = parse_u32("-1");
|
||||
const auto positive = parse_u32("+1");
|
||||
const auto trailing = parse_u32("12px");
|
||||
const auto spaced = parse_u32(" 12");
|
||||
|
||||
PP_EXPECT(h, !empty.ok());
|
||||
PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !negative.ok());
|
||||
PP_EXPECT(h, negative.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !positive.ok());
|
||||
PP_EXPECT(h, positive.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !trailing.ok());
|
||||
PP_EXPECT(h, trailing.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !spaced.ok());
|
||||
PP_EXPECT(h, spaced.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void rejects_overflow_without_wrapping(pp::tests::Harness& h)
|
||||
{
|
||||
const auto overflow = parse_u32("4294967296");
|
||||
const auto very_large = parse_u32("999999999999999999999999999999999999");
|
||||
|
||||
PP_EXPECT(h, !overflow.ok());
|
||||
PP_EXPECT(h, overflow.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !very_large.ok());
|
||||
PP_EXPECT(h, very_large.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("accepts_decimal_uint32_values", accepts_decimal_uint32_values);
|
||||
harness.run("rejects_empty_signed_and_mixed_input", rejects_empty_signed_and_mixed_input);
|
||||
harness.run("rejects_overflow_without_wrapping", rejects_overflow_without_wrapping);
|
||||
return harness.finish();
|
||||
}
|
||||
109
tests/foundation/task_queue_tests.cpp
Normal file
109
tests/foundation/task_queue_tests.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "foundation/task_queue.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::foundation::TaskItem;
|
||||
using pp::foundation::TaskQueue;
|
||||
using pp::foundation::max_task_queue_entries;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Counter {
|
||||
int value = 0;
|
||||
};
|
||||
|
||||
void increment(void* user_data) noexcept
|
||||
{
|
||||
auto* counter = static_cast<Counter*>(user_data);
|
||||
++counter->value;
|
||||
}
|
||||
|
||||
void add_two(void* user_data) noexcept
|
||||
{
|
||||
auto* counter = static_cast<Counter*>(user_data);
|
||||
counter->value += 2;
|
||||
}
|
||||
|
||||
void runs_tasks_in_fifo_order(pp::tests::Harness& h)
|
||||
{
|
||||
Counter counter;
|
||||
TaskQueue queue(4);
|
||||
|
||||
PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter, .id = 1 }).ok());
|
||||
PP_EXPECT(h, queue.push(TaskItem { .callback = add_two, .user_data = &counter, .id = 2 }).ok());
|
||||
PP_EXPECT(h, queue.size() == 2U);
|
||||
PP_EXPECT(h, queue.run_next().ok());
|
||||
PP_EXPECT(h, counter.value == 1);
|
||||
PP_EXPECT(h, queue.run_next().ok());
|
||||
PP_EXPECT(h, counter.value == 3);
|
||||
PP_EXPECT(h, queue.empty());
|
||||
}
|
||||
|
||||
void pops_without_running(pp::tests::Harness& h)
|
||||
{
|
||||
Counter counter;
|
||||
TaskQueue queue(2);
|
||||
|
||||
PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter, .id = 42 }).ok());
|
||||
const auto task = queue.pop();
|
||||
|
||||
PP_EXPECT(h, task.ok());
|
||||
PP_EXPECT(h, task.value().id == 42U);
|
||||
PP_EXPECT(h, counter.value == 0);
|
||||
PP_EXPECT(h, queue.empty());
|
||||
}
|
||||
|
||||
void rejects_invalid_or_excessive_work(pp::tests::Harness& h)
|
||||
{
|
||||
Counter counter;
|
||||
TaskQueue queue(1);
|
||||
TaskQueue invalid_queue(0);
|
||||
TaskQueue too_large(max_task_queue_entries + 1U);
|
||||
|
||||
const auto null_task = queue.push(TaskItem {});
|
||||
PP_EXPECT(h, !null_task.ok());
|
||||
PP_EXPECT(h, null_task.code == StatusCode::invalid_argument);
|
||||
|
||||
PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter }).ok());
|
||||
const auto full = queue.push(TaskItem { .callback = increment, .user_data = &counter });
|
||||
PP_EXPECT(h, !full.ok());
|
||||
PP_EXPECT(h, full.code == StatusCode::out_of_range);
|
||||
|
||||
const auto invalid_capacity = invalid_queue.push(TaskItem { .callback = increment, .user_data = &counter });
|
||||
const auto excessive_capacity = too_large.push(TaskItem { .callback = increment, .user_data = &counter });
|
||||
PP_EXPECT(h, !invalid_capacity.ok());
|
||||
PP_EXPECT(h, invalid_capacity.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !excessive_capacity.ok());
|
||||
PP_EXPECT(h, excessive_capacity.code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void run_all_and_clear_are_bounded(pp::tests::Harness& h)
|
||||
{
|
||||
Counter counter;
|
||||
TaskQueue queue(4);
|
||||
|
||||
PP_EXPECT(h, queue.max_entries() == 4U);
|
||||
PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter }).ok());
|
||||
PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter }).ok());
|
||||
PP_EXPECT(h, queue.run_all() == 2U);
|
||||
PP_EXPECT(h, counter.value == 2);
|
||||
|
||||
PP_EXPECT(h, queue.push(TaskItem { .callback = add_two, .user_data = &counter }).ok());
|
||||
queue.clear();
|
||||
PP_EXPECT(h, queue.empty());
|
||||
const auto empty_pop = queue.pop();
|
||||
PP_EXPECT(h, !empty_pop.ok());
|
||||
PP_EXPECT(h, empty_pop.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("runs_tasks_in_fifo_order", runs_tasks_in_fifo_order);
|
||||
harness.run("pops_without_running", pops_without_running);
|
||||
harness.run("rejects_invalid_or_excessive_work", rejects_invalid_or_excessive_work);
|
||||
harness.run("run_all_and_clear_are_bounded", run_all_and_clear_are_bounded);
|
||||
return harness.finish();
|
||||
}
|
||||
109
tests/foundation/trace_tests.cpp
Normal file
109
tests/foundation/trace_tests.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "foundation/trace.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::foundation::TraceRecorder;
|
||||
using pp::foundation::TraceSpanDesc;
|
||||
|
||||
namespace {
|
||||
|
||||
void records_completed_spans_with_context(pp::tests::Harness& h)
|
||||
{
|
||||
TraceRecorder recorder;
|
||||
|
||||
const auto id = recorder.begin_span(
|
||||
TraceSpanDesc {
|
||||
.component = "paint",
|
||||
.name = "stroke_commit",
|
||||
.thread_id = 7,
|
||||
.frame_id = 11,
|
||||
.stroke_id = 13,
|
||||
},
|
||||
100);
|
||||
|
||||
PP_EXPECT(h, id.ok());
|
||||
PP_EXPECT(h, recorder.active_span_count() == 1U);
|
||||
PP_EXPECT(h, recorder.end_span(id.value(), 145).ok());
|
||||
PP_EXPECT(h, recorder.active_span_count() == 0U);
|
||||
PP_EXPECT(h, recorder.events().size() == 1U);
|
||||
|
||||
const auto& event = recorder.events()[0];
|
||||
PP_EXPECT(h, event.component == "paint");
|
||||
PP_EXPECT(h, event.name == "stroke_commit");
|
||||
PP_EXPECT(h, event.thread_id == 7U);
|
||||
PP_EXPECT(h, event.frame_id == 11U);
|
||||
PP_EXPECT(h, event.stroke_id == 13U);
|
||||
PP_EXPECT(h, event.start_us == 100U);
|
||||
PP_EXPECT(h, event.duration_us == 45U);
|
||||
}
|
||||
|
||||
void rejects_invalid_span_descriptions(pp::tests::Harness& h)
|
||||
{
|
||||
TraceRecorder recorder;
|
||||
|
||||
const auto no_component = recorder.begin_span(
|
||||
TraceSpanDesc { .component = "", .name = "load" },
|
||||
1);
|
||||
const auto no_name = recorder.begin_span(
|
||||
TraceSpanDesc { .component = "assets", .name = "" },
|
||||
1);
|
||||
|
||||
PP_EXPECT(h, !no_component.ok());
|
||||
PP_EXPECT(h, no_component.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !no_name.ok());
|
||||
PP_EXPECT(h, no_name.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, recorder.events().empty());
|
||||
}
|
||||
|
||||
void rejects_bad_end_calls_without_recording_events(pp::tests::Harness& h)
|
||||
{
|
||||
TraceRecorder recorder;
|
||||
const auto id = recorder.begin_span(
|
||||
TraceSpanDesc { .component = "renderer", .name = "readback" },
|
||||
50);
|
||||
|
||||
PP_EXPECT(h, id.ok());
|
||||
|
||||
const auto backwards = recorder.end_span(id.value(), 49);
|
||||
PP_EXPECT(h, !backwards.ok());
|
||||
PP_EXPECT(h, backwards.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, recorder.active_span_count() == 1U);
|
||||
PP_EXPECT(h, recorder.events().empty());
|
||||
|
||||
PP_EXPECT(h, recorder.end_span(id.value(), 51).ok());
|
||||
const auto duplicate = recorder.end_span(id.value(), 52);
|
||||
PP_EXPECT(h, !duplicate.ok());
|
||||
PP_EXPECT(h, duplicate.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, recorder.events().size() == 1U);
|
||||
}
|
||||
|
||||
void clear_resets_events_and_span_ids(pp::tests::Harness& h)
|
||||
{
|
||||
TraceRecorder recorder;
|
||||
const auto first = recorder.begin_span(
|
||||
TraceSpanDesc { .component = "ui", .name = "layout" },
|
||||
10);
|
||||
PP_EXPECT(h, first.ok());
|
||||
PP_EXPECT(h, recorder.end_span(first.value(), 20).ok());
|
||||
recorder.clear();
|
||||
|
||||
const auto second = recorder.begin_span(
|
||||
TraceSpanDesc { .component = "ui", .name = "layout" },
|
||||
30);
|
||||
PP_EXPECT(h, second.ok());
|
||||
PP_EXPECT(h, second.value() == first.value());
|
||||
PP_EXPECT(h, recorder.events().empty());
|
||||
PP_EXPECT(h, recorder.active_span_count() == 1U);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("records_completed_spans_with_context", records_completed_spans_with_context);
|
||||
harness.run("rejects_invalid_span_descriptions", rejects_invalid_span_descriptions);
|
||||
harness.run("rejects_bad_end_calls_without_recording_events", rejects_bad_end_calls_without_recording_events);
|
||||
harness.run("clear_resets_events_and_span_ids", clear_resets_events_and_span_ids);
|
||||
return harness.finish();
|
||||
}
|
||||
105
tests/paint/blend_tests.cpp
Normal file
105
tests/paint/blend_tests.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "paint/blend.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <string_view>
|
||||
|
||||
using pp::paint::BlendMode;
|
||||
using pp::paint::Rgba;
|
||||
using pp::paint::blend_mode_name;
|
||||
using pp::paint::blend_pixels;
|
||||
|
||||
namespace {
|
||||
|
||||
bool near(float a, float b)
|
||||
{
|
||||
return std::fabs(a - b) < 0.0001F;
|
||||
}
|
||||
|
||||
void normal_blend_matches_source_over_alpha(pp::tests::Harness& h)
|
||||
{
|
||||
const auto result = blend_pixels(
|
||||
Rgba { .r = 0.2F, .g = 0.4F, .b = 0.6F, .a = 0.5F },
|
||||
Rgba { .r = 0.8F, .g = 0.2F, .b = 0.1F, .a = 0.25F },
|
||||
BlendMode::normal);
|
||||
|
||||
PP_EXPECT(h, near(result.a, 0.625F));
|
||||
PP_EXPECT(h, near(result.r, 0.44F));
|
||||
PP_EXPECT(h, near(result.g, 0.32F));
|
||||
PP_EXPECT(h, near(result.b, 0.4F));
|
||||
}
|
||||
|
||||
void zero_alpha_stroke_leaves_base_unchanged(pp::tests::Harness& h)
|
||||
{
|
||||
const Rgba base { .r = 0.2F, .g = 0.3F, .b = 0.4F, .a = 0.5F };
|
||||
const auto result = blend_pixels(
|
||||
base,
|
||||
Rgba { .r = 1.0F, .g = 1.0F, .b = 1.0F, .a = 0.0F },
|
||||
BlendMode::screen);
|
||||
|
||||
PP_EXPECT(h, near(result.r, base.r));
|
||||
PP_EXPECT(h, near(result.g, base.g));
|
||||
PP_EXPECT(h, near(result.b, base.b));
|
||||
PP_EXPECT(h, near(result.a, base.a));
|
||||
}
|
||||
|
||||
void multiply_and_screen_are_bounded(pp::tests::Harness& h)
|
||||
{
|
||||
const Rgba base { .r = 0.25F, .g = 0.5F, .b = 0.75F, .a = 1.0F };
|
||||
const Rgba stroke { .r = 0.5F, .g = 0.5F, .b = 0.5F, .a = 1.0F };
|
||||
const auto multiply = blend_pixels(base, stroke, BlendMode::multiply);
|
||||
const auto screen = blend_pixels(base, stroke, BlendMode::screen);
|
||||
|
||||
PP_EXPECT(h, near(multiply.r, 0.125F));
|
||||
PP_EXPECT(h, near(multiply.g, 0.25F));
|
||||
PP_EXPECT(h, near(multiply.b, 0.375F));
|
||||
PP_EXPECT(h, near(screen.r, 0.625F));
|
||||
PP_EXPECT(h, near(screen.g, 0.75F));
|
||||
PP_EXPECT(h, near(screen.b, 0.875F));
|
||||
}
|
||||
|
||||
void color_dodge_and_overlay_handle_extremes(pp::tests::Harness& h)
|
||||
{
|
||||
const auto dodge = blend_pixels(
|
||||
Rgba { .r = 0.4F, .g = 0.5F, .b = 0.6F, .a = 1.0F },
|
||||
Rgba { .r = 1.0F, .g = 0.5F, .b = 0.0F, .a = 1.0F },
|
||||
BlendMode::color_dodge);
|
||||
const auto overlay = blend_pixels(
|
||||
Rgba { .r = 0.25F, .g = 0.5F, .b = 0.75F, .a = 1.0F },
|
||||
Rgba { .r = 0.5F, .g = 0.5F, .b = 0.5F, .a = 1.0F },
|
||||
BlendMode::overlay);
|
||||
|
||||
PP_EXPECT(h, near(dodge.r, 1.0F));
|
||||
PP_EXPECT(h, near(dodge.g, 1.0F));
|
||||
PP_EXPECT(h, near(dodge.b, 0.6F));
|
||||
PP_EXPECT(h, near(overlay.r, 0.25F));
|
||||
PP_EXPECT(h, near(overlay.g, 0.5F));
|
||||
PP_EXPECT(h, near(overlay.b, 0.75F));
|
||||
}
|
||||
|
||||
void clamps_inputs_and_names_modes(pp::tests::Harness& h)
|
||||
{
|
||||
const auto result = blend_pixels(
|
||||
Rgba { .r = -1.0F, .g = 2.0F, .b = 0.5F, .a = 2.0F },
|
||||
Rgba { .r = 2.0F, .g = -1.0F, .b = 0.5F, .a = 2.0F },
|
||||
BlendMode::normal);
|
||||
|
||||
PP_EXPECT(h, near(result.r, 1.0F));
|
||||
PP_EXPECT(h, near(result.g, 0.0F));
|
||||
PP_EXPECT(h, near(result.b, 0.5F));
|
||||
PP_EXPECT(h, near(result.a, 1.0F));
|
||||
PP_EXPECT(h, blend_mode_name(BlendMode::overlay) == std::string_view("overlay"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("normal_blend_matches_source_over_alpha", normal_blend_matches_source_over_alpha);
|
||||
harness.run("zero_alpha_stroke_leaves_base_unchanged", zero_alpha_stroke_leaves_base_unchanged);
|
||||
harness.run("multiply_and_screen_are_bounded", multiply_and_screen_are_bounded);
|
||||
harness.run("color_dodge_and_overlay_handle_extremes", color_dodge_and_overlay_handle_extremes);
|
||||
harness.run("clamps_inputs_and_names_modes", clamps_inputs_and_names_modes);
|
||||
return harness.finish();
|
||||
}
|
||||
127
tests/paint/brush_tests.cpp
Normal file
127
tests/paint/brush_tests.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "paint/brush.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::paint::BrushParams;
|
||||
using pp::paint::evaluate_brush_stamp;
|
||||
using pp::paint::max_brush_size;
|
||||
using pp::paint::min_brush_size;
|
||||
using pp::paint::validate_brush_params;
|
||||
|
||||
namespace {
|
||||
|
||||
bool near(float a, float b)
|
||||
{
|
||||
return std::fabs(a - b) < 0.0001F;
|
||||
}
|
||||
|
||||
void accepts_default_and_boundary_params(pp::tests::Harness& h)
|
||||
{
|
||||
BrushParams defaults;
|
||||
BrushParams minimums {
|
||||
.size = min_brush_size,
|
||||
.spacing = 0.01F,
|
||||
.opacity = 0.0F,
|
||||
.flow = 0.0F,
|
||||
.angle_degrees = -360.0F,
|
||||
.size_jitter = 0.0F,
|
||||
.opacity_jitter = 0.0F,
|
||||
};
|
||||
BrushParams maximums {
|
||||
.size = max_brush_size,
|
||||
.spacing = 16.0F,
|
||||
.opacity = 1.0F,
|
||||
.flow = 1.0F,
|
||||
.angle_degrees = 360.0F,
|
||||
.size_jitter = 1.0F,
|
||||
.opacity_jitter = 1.0F,
|
||||
};
|
||||
|
||||
PP_EXPECT(h, validate_brush_params(defaults).ok());
|
||||
PP_EXPECT(h, validate_brush_params(minimums).ok());
|
||||
PP_EXPECT(h, validate_brush_params(maximums).ok());
|
||||
}
|
||||
|
||||
void rejects_invalid_params(pp::tests::Harness& h)
|
||||
{
|
||||
BrushParams params;
|
||||
|
||||
params.size = 0.0F;
|
||||
PP_EXPECT(h, validate_brush_params(params).code == StatusCode::out_of_range);
|
||||
params = BrushParams {};
|
||||
params.spacing = 0.0F;
|
||||
PP_EXPECT(h, validate_brush_params(params).code == StatusCode::out_of_range);
|
||||
params = BrushParams {};
|
||||
params.opacity = -0.1F;
|
||||
PP_EXPECT(h, validate_brush_params(params).code == StatusCode::out_of_range);
|
||||
params = BrushParams {};
|
||||
params.flow = 1.1F;
|
||||
PP_EXPECT(h, validate_brush_params(params).code == StatusCode::out_of_range);
|
||||
params = BrushParams {};
|
||||
params.angle_degrees = 361.0F;
|
||||
PP_EXPECT(h, validate_brush_params(params).code == StatusCode::out_of_range);
|
||||
params = BrushParams {};
|
||||
params.size_jitter = std::nanf("");
|
||||
PP_EXPECT(h, validate_brush_params(params).code == StatusCode::out_of_range);
|
||||
params = BrushParams {};
|
||||
params.opacity_jitter = 2.0F;
|
||||
PP_EXPECT(h, validate_brush_params(params).code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void evaluates_pressure_controlled_stamp(pp::tests::Harness& h)
|
||||
{
|
||||
const BrushParams params {
|
||||
.size = 20.0F,
|
||||
.spacing = 0.5F,
|
||||
.opacity = 0.8F,
|
||||
.flow = 0.6F,
|
||||
.angle_degrees = 45.0F,
|
||||
.size_jitter = 0.0F,
|
||||
.opacity_jitter = 0.0F,
|
||||
.pressure_controls_size = true,
|
||||
.pressure_controls_opacity = true,
|
||||
};
|
||||
|
||||
const auto stamp = evaluate_brush_stamp(params, 0.5F);
|
||||
PP_EXPECT(h, near(stamp.size, 10.0F));
|
||||
PP_EXPECT(h, near(stamp.opacity, 0.4F));
|
||||
PP_EXPECT(h, near(stamp.flow, 0.6F));
|
||||
PP_EXPECT(h, near(stamp.angle_degrees, 45.0F));
|
||||
}
|
||||
|
||||
void clamps_bad_pressure_and_applies_deterministic_jitter_scale(pp::tests::Harness& h)
|
||||
{
|
||||
const BrushParams params {
|
||||
.size = 20.0F,
|
||||
.spacing = 0.5F,
|
||||
.opacity = 0.8F,
|
||||
.flow = 0.6F,
|
||||
.angle_degrees = 0.0F,
|
||||
.size_jitter = 0.5F,
|
||||
.opacity_jitter = 1.0F,
|
||||
.pressure_controls_size = false,
|
||||
.pressure_controls_opacity = false,
|
||||
};
|
||||
|
||||
const auto nan_pressure = evaluate_brush_stamp(params, std::nanf(""));
|
||||
const auto high_pressure = evaluate_brush_stamp(params, 2.0F);
|
||||
|
||||
PP_EXPECT(h, near(nan_pressure.size, 15.0F));
|
||||
PP_EXPECT(h, near(nan_pressure.opacity, 0.4F));
|
||||
PP_EXPECT(h, near(high_pressure.size, 15.0F));
|
||||
PP_EXPECT(h, near(high_pressure.opacity, 0.4F));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("accepts_default_and_boundary_params", accepts_default_and_boundary_params);
|
||||
harness.run("rejects_invalid_params", rejects_invalid_params);
|
||||
harness.run("evaluates_pressure_controlled_stamp", evaluates_pressure_controlled_stamp);
|
||||
harness.run("clamps_bad_pressure_and_applies_deterministic_jitter_scale", clamps_bad_pressure_and_applies_deterministic_jitter_scale);
|
||||
return harness.finish();
|
||||
}
|
||||
86
tests/paint/stroke_script_tests.cpp
Normal file
86
tests/paint/stroke_script_tests.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include "paint/stroke_script.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::paint::max_stroke_script_bytes;
|
||||
using pp::paint::max_stroke_script_line_length;
|
||||
using pp::paint::parse_stroke_script;
|
||||
|
||||
namespace {
|
||||
|
||||
void parses_comments_and_multiple_strokes(pp::tests::Harness& h)
|
||||
{
|
||||
constexpr std::string_view script_text =
|
||||
"# scripted automation fixture\n"
|
||||
"stroke 0 0 0.25 10 0 0.75 2\n"
|
||||
"\n"
|
||||
"stroke 10 0 1 10 10 0.5 5 # trailing comment\n";
|
||||
|
||||
const auto script = parse_stroke_script(script_text);
|
||||
|
||||
PP_EXPECT(h, script.ok());
|
||||
PP_EXPECT(h, script.value().strokes.size() == 2U);
|
||||
PP_EXPECT(h, script.value().strokes[0].start.x == 0.0F);
|
||||
PP_EXPECT(h, script.value().strokes[0].start.pressure == 0.25F);
|
||||
PP_EXPECT(h, script.value().strokes[0].end.x == 10.0F);
|
||||
PP_EXPECT(h, script.value().strokes[0].spacing == 2.0F);
|
||||
PP_EXPECT(h, script.value().strokes[1].end.y == 10.0F);
|
||||
PP_EXPECT(h, script.value().strokes[1].end.pressure == 0.5F);
|
||||
}
|
||||
|
||||
void rejects_malformed_stroke_scripts(pp::tests::Harness& h)
|
||||
{
|
||||
const auto empty = parse_stroke_script("");
|
||||
const auto comments_only = parse_stroke_script("# nope\n\n");
|
||||
const auto unknown = parse_stroke_script("move 0 0 1 10 0 1 2\n");
|
||||
const auto missing_tokens = parse_stroke_script("stroke 0 0 1 10 0 1\n");
|
||||
const auto too_many_tokens = parse_stroke_script("stroke 0 0 1 10 0 1 2 extra\n");
|
||||
const auto bad_number = parse_stroke_script("stroke 0 0 1 10 nope 1 2\n");
|
||||
const auto nan_number = parse_stroke_script("stroke 0 0 1 10 nan 1 2\n");
|
||||
const auto zero_spacing = parse_stroke_script("stroke 0 0 1 10 0 1 0\n");
|
||||
|
||||
PP_EXPECT(h, !empty.ok());
|
||||
PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !comments_only.ok());
|
||||
PP_EXPECT(h, comments_only.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !unknown.ok());
|
||||
PP_EXPECT(h, unknown.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !missing_tokens.ok());
|
||||
PP_EXPECT(h, missing_tokens.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !too_many_tokens.ok());
|
||||
PP_EXPECT(h, too_many_tokens.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_number.ok());
|
||||
PP_EXPECT(h, bad_number.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !nan_number.ok());
|
||||
PP_EXPECT(h, nan_number.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !zero_spacing.ok());
|
||||
PP_EXPECT(h, zero_spacing.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void rejects_oversized_stroke_scripts(pp::tests::Harness& h)
|
||||
{
|
||||
const std::string oversized_script(max_stroke_script_bytes + 1U, 'x');
|
||||
const std::string oversized_line(max_stroke_script_line_length + 1U, 'x');
|
||||
|
||||
const auto too_large_script = parse_stroke_script(oversized_script);
|
||||
const auto too_large_line = parse_stroke_script(oversized_line);
|
||||
|
||||
PP_EXPECT(h, !too_large_script.ok());
|
||||
PP_EXPECT(h, too_large_script.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !too_large_line.ok());
|
||||
PP_EXPECT(h, too_large_line.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("parses_comments_and_multiple_strokes", parses_comments_and_multiple_strokes);
|
||||
harness.run("rejects_malformed_stroke_scripts", rejects_malformed_stroke_scripts);
|
||||
harness.run("rejects_oversized_stroke_scripts", rejects_oversized_stroke_scripts);
|
||||
return harness.finish();
|
||||
}
|
||||
128
tests/paint/stroke_tests.cpp
Normal file
128
tests/paint/stroke_tests.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
#include "paint/stroke.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::paint::StrokePoint;
|
||||
using pp::paint::StrokeSamplingConfig;
|
||||
using pp::paint::max_stroke_samples;
|
||||
using pp::paint::sample_stroke;
|
||||
|
||||
namespace {
|
||||
|
||||
bool near(float a, float b)
|
||||
{
|
||||
return std::fabs(a - b) < 0.0001F;
|
||||
}
|
||||
|
||||
void samples_straight_line_at_fixed_spacing(pp::tests::Harness& h)
|
||||
{
|
||||
const std::array points {
|
||||
StrokePoint { .x = 0.0F, .y = 0.0F, .pressure = 0.25F },
|
||||
StrokePoint { .x = 10.0F, .y = 0.0F, .pressure = 0.75F },
|
||||
};
|
||||
|
||||
const auto result = sample_stroke(points, StrokeSamplingConfig { .spacing = 2.5F });
|
||||
PP_EXPECT(h, result.ok());
|
||||
PP_EXPECT(h, result.value().size() == 5U);
|
||||
PP_EXPECT(h, near(result.value()[0].x, 0.0F));
|
||||
PP_EXPECT(h, near(result.value()[1].x, 2.5F));
|
||||
PP_EXPECT(h, near(result.value()[2].x, 5.0F));
|
||||
PP_EXPECT(h, near(result.value()[3].x, 7.5F));
|
||||
PP_EXPECT(h, near(result.value()[4].x, 10.0F));
|
||||
PP_EXPECT(h, near(result.value()[2].pressure, 0.5F));
|
||||
PP_EXPECT(h, near(result.value()[4].distance, 10.0F));
|
||||
}
|
||||
|
||||
void carries_spacing_across_segments(pp::tests::Harness& h)
|
||||
{
|
||||
const std::array points {
|
||||
StrokePoint { .x = 0.0F, .y = 0.0F, .pressure = 1.0F },
|
||||
StrokePoint { .x = 3.0F, .y = 0.0F, .pressure = 1.0F },
|
||||
StrokePoint { .x = 3.0F, .y = 4.0F, .pressure = 0.0F },
|
||||
};
|
||||
|
||||
const auto result = sample_stroke(points, StrokeSamplingConfig { .spacing = 2.0F });
|
||||
PP_EXPECT(h, result.ok());
|
||||
PP_EXPECT(h, result.value().size() == 5U);
|
||||
PP_EXPECT(h, near(result.value()[1].x, 2.0F));
|
||||
PP_EXPECT(h, near(result.value()[1].y, 0.0F));
|
||||
PP_EXPECT(h, near(result.value()[2].x, 3.0F));
|
||||
PP_EXPECT(h, near(result.value()[2].y, 1.0F));
|
||||
PP_EXPECT(h, near(result.value()[3].x, 3.0F));
|
||||
PP_EXPECT(h, near(result.value()[3].y, 3.0F));
|
||||
PP_EXPECT(h, near(result.value()[4].distance, 7.0F));
|
||||
}
|
||||
|
||||
void can_skip_endpoint_and_clamps_pressure(pp::tests::Harness& h)
|
||||
{
|
||||
const std::array points {
|
||||
StrokePoint { .x = 0.0F, .y = 0.0F, .pressure = -1.0F },
|
||||
StrokePoint { .x = 5.0F, .y = 0.0F, .pressure = 2.0F },
|
||||
};
|
||||
|
||||
const auto result = sample_stroke(
|
||||
points,
|
||||
StrokeSamplingConfig {
|
||||
.spacing = 2.0F,
|
||||
.include_endpoint = false,
|
||||
});
|
||||
PP_EXPECT(h, result.ok());
|
||||
PP_EXPECT(h, result.value().size() == 3U);
|
||||
PP_EXPECT(h, near(result.value()[0].pressure, 0.0F));
|
||||
PP_EXPECT(h, near(result.value()[2].pressure, 1.0F));
|
||||
PP_EXPECT(h, near(result.value()[2].distance, 4.0F));
|
||||
}
|
||||
|
||||
void rejects_invalid_sampling_inputs(pp::tests::Harness& h)
|
||||
{
|
||||
const std::array one_point {
|
||||
StrokePoint { .x = 0.0F, .y = 0.0F },
|
||||
};
|
||||
const std::array zero_length {
|
||||
StrokePoint { .x = 1.0F, .y = 1.0F },
|
||||
StrokePoint { .x = 1.0F, .y = 1.0F },
|
||||
};
|
||||
const std::array non_finite {
|
||||
StrokePoint { .x = 0.0F, .y = 0.0F },
|
||||
StrokePoint { .x = std::nanf(""), .y = 1.0F },
|
||||
};
|
||||
const std::array valid {
|
||||
StrokePoint { .x = 0.0F, .y = 0.0F },
|
||||
StrokePoint { .x = 10.0F, .y = 0.0F },
|
||||
};
|
||||
|
||||
const auto missing_points = sample_stroke(one_point, StrokeSamplingConfig {});
|
||||
const auto bad_spacing = sample_stroke(valid, StrokeSamplingConfig { .spacing = 0.0F });
|
||||
const auto bad_limit = sample_stroke(valid, StrokeSamplingConfig { .max_samples = max_stroke_samples + 1U });
|
||||
const auto no_distance = sample_stroke(zero_length, StrokeSamplingConfig {});
|
||||
const auto bad_point = sample_stroke(non_finite, StrokeSamplingConfig {});
|
||||
const auto too_many = sample_stroke(valid, StrokeSamplingConfig { .spacing = 1.0F, .max_samples = 2U });
|
||||
|
||||
PP_EXPECT(h, !missing_points.ok());
|
||||
PP_EXPECT(h, missing_points.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_spacing.ok());
|
||||
PP_EXPECT(h, bad_spacing.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_limit.ok());
|
||||
PP_EXPECT(h, bad_limit.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !no_distance.ok());
|
||||
PP_EXPECT(h, no_distance.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_point.ok());
|
||||
PP_EXPECT(h, bad_point.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !too_many.ok());
|
||||
PP_EXPECT(h, too_many.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("samples_straight_line_at_fixed_spacing", samples_straight_line_at_fixed_spacing);
|
||||
harness.run("carries_spacing_across_segments", carries_spacing_across_segments);
|
||||
harness.run("can_skip_endpoint_and_clamps_pressure", can_skip_endpoint_and_clamps_pressure);
|
||||
harness.run("rejects_invalid_sampling_inputs", rejects_invalid_sampling_inputs);
|
||||
return harness.finish();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user