42 Commits

Author SHA1 Message Date
ad255a6ddf Attach PPI pixels to documents 2026-06-01 13:43:27 +02:00
88507df90e Decode PPI face payloads 2026-06-01 13:35:03 +02:00
10e5d5b5ae Preserve per-layer document timelines 2026-06-01 13:05:14 +02:00
c16cab87bd Load PPI metadata into documents 2026-06-01 13:00:14 +02:00
7319cb9aa9 Index PPI project layers 2026-06-01 12:53:48 +02:00
677d0b33a8 Validate PPI face payload metadata 2026-06-01 12:48:55 +02:00
f1ee1b28a1 Summarize PPI project bodies 2026-06-01 12:44:49 +02:00
2da247f0fb Validate PPI project layout 2026-06-01 12:38:21 +02:00
37854ea8b9 Add paint stroke script automation 2026-06-01 12:34:15 +02:00
dc252b2f24 Simulate strokes from pano cli 2026-06-01 09:12:55 +02:00
d0ef88be89 Create animation documents from pano cli 2026-06-01 09:10:59 +02:00
4ec2d093e8 Fix clang-cl ASan preset setup 2026-06-01 09:09:16 +02:00
4eee018367 Validate renderer shader descriptors 2026-06-01 09:05:43 +02:00
44aebf61b2 Add document frame move coverage 2026-06-01 09:03:46 +02:00
f6d3de8cbf Expose PNG metadata in pano cli 2026-06-01 09:01:21 +02:00
c62bc4d744 Add assets PNG metadata tests 2026-06-01 08:58:28 +02:00
8ebb22325c Use vcpkg tinyxml2 in headless preset 2026-06-01 08:52:31 +02:00
e5d98c2dc3 Validate headless vcpkg preset 2026-06-01 08:49:37 +02:00
abe578a338 Add paint brush parameter tests 2026-06-01 08:40:46 +02:00
313a360c01 Add UI core color parser tests 2026-06-01 08:38:05 +02:00
551013c771 Add document layer metadata tests 2026-06-01 08:34:26 +02:00
cc377b5eb5 Add assets settings document tests 2026-06-01 08:32:29 +02:00
6c435dafb7 Add foundation event dispatcher tests 2026-06-01 08:28:57 +02:00
3f5711773e Add foundation task queue tests 2026-06-01 08:23:59 +02:00
a7bb04f54b Add foundation logging facade 2026-06-01 08:20:58 +02:00
6604f30ef3 Expand renderer API interfaces 2026-06-01 08:15:21 +02:00
93d8aaaffd Add paint stroke sampling tests 2026-06-01 08:08:27 +02:00
f9e4bcaeea Add shader validation automation hook 2026-06-01 07:55:39 +02:00
3d80791245 Add document undo history tests 2026-06-01 07:39:42 +02:00
126280ff7c Add PPI header recognition tests 2026-06-01 00:26:06 +02:00
20b5dba41e Add UI layout XML automation 2026-06-01 00:21:23 +02:00
dfdb7a4468 Add document animation frame tests 2026-06-01 00:16:34 +02:00
4d715afd60 Add paint renderer compositor tests 2026-06-01 00:13:53 +02:00
ac0d0ab49c Add package smoke automation 2026-06-01 00:09:34 +02:00
a67e7fc9bb Start UI core layout value tests 2026-06-01 00:07:55 +02:00
31322bbd83 Add renderer API validation tests 2026-06-01 00:05:41 +02:00
23eba07901 Start document model tests 2026-06-01 00:02:42 +02:00
8014345b99 Add paint blend reference tests 2026-05-31 23:58:47 +02:00
99eda95cee Start assets component image signature tests 2026-05-31 23:55:20 +02:00
ec5ecbdb54 Add foundation tracing and platform build wrapper 2026-05-31 23:51:41 +02:00
e0ea4597e6 Add Android headless preset and parser tests 2026-05-31 23:46:41 +02:00
c38ff8209b Start CMake modernization scaffold 2026-05-31 23:40:43 +02:00
109 changed files with 11269 additions and 1556 deletions

2
.gitignore vendored
View File

@@ -53,3 +53,5 @@ linux/Makefile
webgl/build
webgl/.vscode
out/

291
CMakeLists.txt Normal file
View 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
View 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"
}
}
}
]
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View 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)

View 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"
)

View 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()

View 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()

View 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}")

View 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.

View 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.

View 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 |

View 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` |

View 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.

View File

@@ -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 )

View 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

View 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"

View 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

View 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"

View 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

View 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"

View 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

View 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"

View 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

View 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"

View 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

View 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"

View 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
View 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;
}

View 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";
}
}

View 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;
}

View 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
View 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
View 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
View 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);
}

View 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";
}
}

View 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
View 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
View 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
View 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
View 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);
}

View 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();
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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_{};
};
}

View 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();
}
}

View 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
View 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
View 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;
};
}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}

View 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();
}
}

View 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;
}

View File

@@ -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>

View 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";
}
}

View 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
View 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
View 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;
}

View 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";
}
}

View 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;
}

View 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
View 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);
}

View File

@@ -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
View 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()

View 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();
}

View 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();
}

View 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();
}

View 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();
}

View 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();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 B

View File

@@ -0,0 +1 @@
GIF8

View File

@@ -0,0 +1,5 @@
<layout width="100%" height="auto">
<panel width="320" height="200">
<button width="64" height="28"/>
</panel>
</layout>

Binary file not shown.

View 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

View 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();
}

View 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();
}

View 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();
}

View 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();
}

View 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();
}

View 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();
}

View 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();
}

View 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
View 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
View 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();
}

View 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();
}

View 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