32 Commits

Author SHA1 Message Date
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
98 changed files with 8486 additions and 1556 deletions

2
.gitignore vendored
View File

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

279
CMakeLists.txt Normal file
View File

@@ -0,0 +1,279 @@
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/ppi_header.cpp
src/assets/settings_document.cpp)
target_include_directories(pp_assets
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
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)
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)
target_include_directories(pp_document
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_document
PUBLIC
pp_foundation
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,118 @@
# 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, PPI
header, settings document, document frame move/duration coverage, paint
brush/stroke 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 create-document` supports `--frames` and `--frame-duration-ms` and
is covered by `pano_cli_create_animation_document_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 but is not yet wired to legacy `Canvas`, PPI load/save, selection masks, 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` | 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` and `pano_cli inspect-project` recognize only the fixed PPI header, not thumbnail bytes or the project body | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_ppi_header_tests` | Full PPI load/save fixtures cover thumbnail, layers, 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,645 @@
# 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 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 recognition, 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. `pp_document` has
started with a pure canvas/layer/frame model, layer metadata operations, frame
move/duration queries, 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, and
`pano_cli create-document` can create simple animation documents with explicit
frame count/duration. `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_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_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_ppi_header_tests` passed.
- `pp_assets_settings_document_tests` passed.
- `pp_paint_brush_tests` passed.
- `pp_paint_blend_tests` passed.
- `pp_paint_stroke_tests` passed.
- `pp_document_tests` passed, including frame move, duration, and history
invariants.
- `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_parse_layout_smoke` passed.
- `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_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_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_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_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;
}

73
src/assets/ppi_header.cpp Normal file
View File

@@ -0,0 +1,73 @@
#include "assets/ppi_header.h"
#include "foundation/binary_stream.h"
namespace pp::assets {
namespace {
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_u32(pp::foundation::ByteReader& reader) noexcept
{
return reader.read_u32_le();
}
}
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);
}
}

40
src/assets/ppi_header.h Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <span>
namespace pp::assets {
constexpr std::size_t ppi_header_size = 40;
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;
};
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
std::span<const std::byte> bytes) noexcept;
}

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

462
src/document/document.cpp Normal file
View File

@@ -0,0 +1,462 @@
#include "document/document.h"
#include <algorithm>
#include <cmath>
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]] 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_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();
}
}
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.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) });
}
document.frames_.push_back(AnimationFrame {});
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 = 0;
for (const auto& frame : frames_) {
duration += frame.duration_ms;
}
return duration;
}
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);
}
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_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;
}
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");
}
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;
}
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:
layers_[index].blend_mode = blend_mode;
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("layer blend mode is not supported");
}
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"));
}
if (duration_ms < min_frame_duration_ms) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::invalid_argument("frame duration must be greater than zero"));
}
frames_.push_back(AnimationFrame { .duration_ms = duration_ms });
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]);
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));
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);
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;
}
if (duration_ms < min_frame_duration_ms) {
return pp::foundation::Status::invalid_argument("frame duration must be greater than zero");
}
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::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();
}
}

98
src/document/document.h Normal file
View File

@@ -0,0 +1,98 @@
#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;
struct DocumentConfig {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t layer_count = 1;
};
struct Layer {
std::string name;
bool visible = true;
float opacity = 1.0F;
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
};
struct AnimationFrame {
std::uint32_t duration_ms = 100;
};
class CanvasDocument {
public:
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create(DocumentConfig 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]] 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_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;
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_;
};
}

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

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>
@@ -181,4 +186,4 @@
#ifndef EMSCRIPTEN
#include <curl/curl.h>
#endif
#endif

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

227
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,227 @@
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_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_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_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_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")
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,108 @@
#include "assets/ppi_header.h"
#include "test_harness.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>
using pp::assets::parse_ppi_header;
using pp::assets::ppi_header_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));
}
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 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);
}
}
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);
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>

View File

@@ -0,0 +1,376 @@
#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::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().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_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, 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_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_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 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);
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.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 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("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("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,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,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();
}

View File

@@ -0,0 +1,109 @@
#include "paint_renderer/compositor.h"
#include "test_harness.h"
#include <cmath>
#include <vector>
using pp::foundation::StatusCode;
using pp::paint::BlendMode;
using pp::paint::Rgba;
using pp::paint_renderer::LayerCompositeView;
using pp::paint_renderer::composite_layer;
using pp::renderer::Extent2D;
namespace {
bool near(float a, float b)
{
return std::fabs(a - b) < 0.0001F;
}
void composites_visible_layer_with_opacity(pp::tests::Harness& h)
{
std::vector<Rgba> destination {
Rgba { .r = 0.2F, .g = 0.4F, .b = 0.6F, .a = 0.5F },
};
const std::vector<Rgba> foreground {
Rgba { .r = 0.8F, .g = 0.2F, .b = 0.1F, .a = 0.5F },
};
const auto status = composite_layer(
destination,
Extent2D { .width = 1, .height = 1 },
LayerCompositeView {
.pixels = foreground,
.opacity = 0.5F,
.visible = true,
.blend_mode = BlendMode::normal,
});
PP_EXPECT(h, status.ok());
PP_EXPECT(h, near(destination[0].a, 0.625F));
PP_EXPECT(h, near(destination[0].r, 0.44F));
PP_EXPECT(h, near(destination[0].g, 0.32F));
PP_EXPECT(h, near(destination[0].b, 0.4F));
}
void invisible_and_zero_opacity_layers_are_noops(pp::tests::Harness& h)
{
const Rgba original { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 0.4F };
std::vector<Rgba> destination { original };
const std::vector<Rgba> foreground {
Rgba { .r = 1.0F, .g = 1.0F, .b = 1.0F, .a = 1.0F },
};
PP_EXPECT(h, composite_layer(
destination,
Extent2D { .width = 1, .height = 1 },
LayerCompositeView { .pixels = foreground, .opacity = 1.0F, .visible = false }).ok());
PP_EXPECT(h, near(destination[0].r, original.r));
PP_EXPECT(h, near(destination[0].g, original.g));
PP_EXPECT(h, near(destination[0].b, original.b));
PP_EXPECT(h, near(destination[0].a, original.a));
PP_EXPECT(h, composite_layer(
destination,
Extent2D { .width = 1, .height = 1 },
LayerCompositeView { .pixels = foreground, .opacity = 0.0F, .visible = true }).ok());
PP_EXPECT(h, near(destination[0].r, original.r));
PP_EXPECT(h, near(destination[0].g, original.g));
PP_EXPECT(h, near(destination[0].b, original.b));
PP_EXPECT(h, near(destination[0].a, original.a));
}
void rejects_invalid_sizes_and_opacity(pp::tests::Harness& h)
{
std::vector<Rgba> destination(2);
const std::vector<Rgba> foreground(1);
const auto mismatched = composite_layer(
destination,
Extent2D { .width = 2, .height = 1 },
LayerCompositeView { .pixels = foreground });
const auto bad_opacity = composite_layer(
destination,
Extent2D { .width = 2, .height = 1 },
LayerCompositeView { .pixels = destination, .opacity = 1.5F });
const auto bad_extent = composite_layer(
destination,
Extent2D { .width = 0, .height = 1 },
LayerCompositeView { .pixels = destination });
PP_EXPECT(h, !mismatched.ok());
PP_EXPECT(h, mismatched.code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_opacity.ok());
PP_EXPECT(h, bad_opacity.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_extent.ok());
PP_EXPECT(h, bad_extent.code == StatusCode::invalid_argument);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("composites_visible_layer_with_opacity", composites_visible_layer_with_opacity);
harness.run("invisible_and_zero_opacity_layers_are_noops", invisible_and_zero_opacity_layers_are_noops);
harness.run("rejects_invalid_sizes_and_opacity", rejects_invalid_sizes_and_opacity);
return harness.finish();
}

View File

@@ -0,0 +1,315 @@
#include "renderer_api/renderer_api.h"
#include "test_harness.h"
#include <string_view>
using pp::foundation::StatusCode;
using pp::renderer::ClearColor;
using pp::renderer::Extent2D;
using pp::renderer::ICommandContext;
using pp::renderer::IMesh;
using pp::renderer::IRenderDevice;
using pp::renderer::IRenderTarget;
using pp::renderer::IRenderTrace;
using pp::renderer::IShaderProgram;
using pp::renderer::MeshDesc;
using pp::renderer::PrimitiveTopology;
using pp::renderer::ReadbackRegion;
using pp::renderer::ShaderProgramDesc;
using pp::renderer::ShaderStageSource;
using pp::renderer::TextureDesc;
using pp::renderer::TextureFormat;
using pp::renderer::Viewport;
using pp::renderer::max_shader_source_bytes;
using pp::renderer::max_texture_dimension;
using pp::renderer::primitive_topology_name;
using pp::renderer::texture_byte_size;
using pp::renderer::texture_format_name;
using pp::renderer::validate_extent;
using pp::renderer::validate_mesh_desc;
using pp::renderer::validate_readback_region;
using pp::renderer::validate_shader_program_desc;
using pp::renderer::validate_viewport;
namespace {
class FakeRenderTarget final : public IRenderTarget {
public:
[[nodiscard]] TextureDesc color_desc() const noexcept override
{
return TextureDesc {
.extent = Extent2D { .width = 64, .height = 32 },
.format = TextureFormat::rgba8,
.render_target = true,
};
}
};
class FakeShaderProgram final : public IShaderProgram {
public:
[[nodiscard]] const char* debug_name() const noexcept override
{
return "fake-shader";
}
};
class FakeMesh final : public IMesh {
public:
[[nodiscard]] MeshDesc desc() const noexcept override
{
return MeshDesc { .vertex_count = 3, .index_count = 0, .topology = PrimitiveTopology::triangles };
}
};
class FakeTrace final : public IRenderTrace {
public:
void marker(const char* component, const char* name) noexcept override
{
last_component = component;
last_name = name;
}
const char* last_component = nullptr;
const char* last_name = nullptr;
};
class FakeCommandContext final : public ICommandContext {
public:
[[nodiscard]] pp::foundation::Status begin_render_pass(
IRenderTarget& target,
ClearColor) noexcept override
{
in_render_pass = true;
return validate_extent(target.color_desc().extent);
}
[[nodiscard]] pp::foundation::Status set_viewport(Viewport viewport) noexcept override
{
if (!in_render_pass) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
return validate_viewport(viewport, Extent2D { .width = 64, .height = 32 });
}
[[nodiscard]] pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept override
{
shader_name = shader.debug_name();
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status bind_mesh(IMesh& mesh) noexcept override
{
return validate_mesh_desc(mesh.desc());
}
[[nodiscard]] pp::foundation::Status draw() noexcept override
{
return in_render_pass ? pp::foundation::Status::success()
: pp::foundation::Status::invalid_argument("render pass has not begun");
}
void end_render_pass() noexcept override
{
in_render_pass = false;
}
bool in_render_pass = false;
const char* shader_name = nullptr;
};
class FakeRenderDevice final : public IRenderDevice {
public:
[[nodiscard]] const char* backend_name() const noexcept override
{
return "fake";
}
[[nodiscard]] ICommandContext& immediate_context() noexcept override
{
return context;
}
[[nodiscard]] IRenderTrace* trace() noexcept override
{
return &trace_recorder;
}
FakeCommandContext context;
FakeTrace trace_recorder;
};
void computes_texture_sizes(pp::tests::Harness& h)
{
const auto rgba = texture_byte_size(TextureDesc {
.extent = Extent2D { .width = 16, .height = 8 },
.format = TextureFormat::rgba8,
});
const auto r8 = texture_byte_size(TextureDesc {
.extent = Extent2D { .width = 16, .height = 8 },
.format = TextureFormat::r8,
});
PP_EXPECT(h, rgba.ok());
PP_EXPECT(h, rgba.value() == 512U);
PP_EXPECT(h, r8.ok());
PP_EXPECT(h, r8.value() == 128U);
PP_EXPECT(h, texture_format_name(TextureFormat::depth24_stencil8) == std::string_view("depth24_stencil8"));
}
void rejects_invalid_or_excessive_extents(pp::tests::Harness& h)
{
const auto zero = validate_extent(Extent2D { .width = 0, .height = 1 });
const auto huge = validate_extent(Extent2D { .width = max_texture_dimension + 1U, .height = 1 });
const auto excessive_bytes = texture_byte_size(TextureDesc {
.extent = Extent2D { .width = max_texture_dimension, .height = max_texture_dimension },
.format = TextureFormat::rgba8,
});
PP_EXPECT(h, !zero.ok());
PP_EXPECT(h, zero.code == StatusCode::invalid_argument);
PP_EXPECT(h, !huge.ok());
PP_EXPECT(h, huge.code == StatusCode::out_of_range);
PP_EXPECT(h, !excessive_bytes.ok());
PP_EXPECT(h, excessive_bytes.status().code == StatusCode::out_of_range);
}
void validates_readback_bounds(pp::tests::Harness& h)
{
const TextureDesc desc {
.extent = Extent2D { .width = 64, .height = 32 },
.format = TextureFormat::rgba8,
.render_target = true,
};
PP_EXPECT(h, validate_readback_region(desc, ReadbackRegion { .x = 0, .y = 0, .width = 64, .height = 32 }).ok());
PP_EXPECT(h, validate_readback_region(desc, ReadbackRegion { .x = 63, .y = 31, .width = 1, .height = 1 }).ok());
const auto empty = validate_readback_region(desc, ReadbackRegion { .x = 0, .y = 0, .width = 0, .height = 1 });
const auto origin_outside = validate_readback_region(desc, ReadbackRegion { .x = 65, .y = 0, .width = 1, .height = 1 });
const auto overrun = validate_readback_region(desc, ReadbackRegion { .x = 63, .y = 31, .width = 2, .height = 1 });
PP_EXPECT(h, !empty.ok());
PP_EXPECT(h, empty.code == StatusCode::invalid_argument);
PP_EXPECT(h, !origin_outside.ok());
PP_EXPECT(h, origin_outside.code == StatusCode::out_of_range);
PP_EXPECT(h, !overrun.ok());
PP_EXPECT(h, overrun.code == StatusCode::out_of_range);
}
void validates_viewports_and_mesh_descriptors(pp::tests::Harness& h)
{
const Extent2D target { .width = 64, .height = 32 };
PP_EXPECT(h, validate_viewport(
Viewport { .x = 0, .y = 0, .width = 64, .height = 32, .min_depth = 0.0F, .max_depth = 1.0F },
target)
.ok());
PP_EXPECT(h, validate_mesh_desc(MeshDesc { .vertex_count = 3, .topology = PrimitiveTopology::triangles }).ok());
PP_EXPECT(h, primitive_topology_name(PrimitiveTopology::lines) == std::string_view("lines"));
const auto negative_origin = validate_viewport(Viewport { .x = -1, .y = 0, .width = 1, .height = 1 }, target);
const auto too_wide = validate_viewport(Viewport { .x = 63, .y = 0, .width = 2, .height = 1 }, target);
const auto bad_depth = validate_viewport(
Viewport { .x = 0, .y = 0, .width = 1, .height = 1, .min_depth = 0.75F, .max_depth = 0.25F },
target);
const auto empty_mesh = validate_mesh_desc(MeshDesc {});
PP_EXPECT(h, !negative_origin.ok());
PP_EXPECT(h, negative_origin.code == StatusCode::invalid_argument);
PP_EXPECT(h, !too_wide.ok());
PP_EXPECT(h, too_wide.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_depth.ok());
PP_EXPECT(h, bad_depth.code == StatusCode::out_of_range);
PP_EXPECT(h, !empty_mesh.ok());
PP_EXPECT(h, empty_mesh.code == StatusCode::invalid_argument);
}
void validates_shader_program_descriptors(pp::tests::Harness& h)
{
constexpr char vertex_source[] = "#version 330 core\nvoid main(){}";
constexpr char fragment_source[] = "#version 330 core\nout vec4 color; void main(){ color = vec4(1); }";
const ShaderProgramDesc valid {
.debug_name = "solid-color",
.vertex = ShaderStageSource {
.entry_point = "main",
.source = vertex_source,
.source_size = sizeof(vertex_source) - 1U,
},
.fragment = ShaderStageSource {
.entry_point = "main",
.source = fragment_source,
.source_size = sizeof(fragment_source) - 1U,
},
};
PP_EXPECT(h, validate_shader_program_desc(valid).ok());
auto missing_name = valid;
missing_name.debug_name = nullptr;
auto missing_vertex_entry = valid;
missing_vertex_entry.vertex.entry_point = "";
auto missing_fragment_source = valid;
missing_fragment_source.fragment.source = nullptr;
auto empty_fragment_source = valid;
empty_fragment_source.fragment.source_size = 0;
auto excessive_source = valid;
excessive_source.vertex.source_size = max_shader_source_bytes + 1U;
const auto missing_name_status = validate_shader_program_desc(missing_name);
const auto missing_vertex_entry_status = validate_shader_program_desc(missing_vertex_entry);
const auto missing_fragment_source_status = validate_shader_program_desc(missing_fragment_source);
const auto empty_fragment_source_status = validate_shader_program_desc(empty_fragment_source);
const auto excessive_source_status = validate_shader_program_desc(excessive_source);
PP_EXPECT(h, !missing_name_status.ok());
PP_EXPECT(h, missing_name_status.code == StatusCode::invalid_argument);
PP_EXPECT(h, !missing_vertex_entry_status.ok());
PP_EXPECT(h, missing_vertex_entry_status.code == StatusCode::invalid_argument);
PP_EXPECT(h, !missing_fragment_source_status.ok());
PP_EXPECT(h, missing_fragment_source_status.code == StatusCode::invalid_argument);
PP_EXPECT(h, !empty_fragment_source_status.ok());
PP_EXPECT(h, empty_fragment_source_status.code == StatusCode::invalid_argument);
PP_EXPECT(h, !excessive_source_status.ok());
PP_EXPECT(h, excessive_source_status.code == StatusCode::out_of_range);
}
void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h)
{
FakeRenderDevice device;
FakeRenderTarget target;
FakeShaderProgram shader;
FakeMesh mesh;
PP_EXPECT(h, device.backend_name() == std::string_view("fake"));
device.trace()->marker("renderer", "begin");
PP_EXPECT(h, device.trace_recorder.last_component == std::string_view("renderer"));
PP_EXPECT(h, device.trace_recorder.last_name == std::string_view("begin"));
auto& context = device.immediate_context();
PP_EXPECT(h, context.begin_render_pass(target, ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }).ok());
PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok());
PP_EXPECT(h, context.bind_shader(shader).ok());
PP_EXPECT(h, context.bind_mesh(mesh).ok());
PP_EXPECT(h, context.draw().ok());
context.end_render_pass();
const auto draw_after_end = context.draw();
PP_EXPECT(h, !draw_after_end.ok());
PP_EXPECT(h, draw_after_end.code == StatusCode::invalid_argument);
PP_EXPECT(h, device.context.shader_name == std::string_view("fake-shader"));
}
}
int main()
{
pp::tests::Harness harness;
harness.run("computes_texture_sizes", computes_texture_sizes);
harness.run("rejects_invalid_or_excessive_extents", rejects_invalid_or_excessive_extents);
harness.run("validates_readback_bounds", validates_readback_bounds);
harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors);
harness.run("validates_shader_program_descriptors", validates_shader_program_descriptors);
harness.run("renderer_interfaces_support_backend_neutral_dispatch", renderer_interfaces_support_backend_neutral_dispatch);
return harness.finish();
}

50
tests/test_harness.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
#include <cstdlib>
#include <iostream>
#include <string_view>
namespace pp::tests {
class Harness {
public:
void expect(bool condition, std::string_view expression, std::string_view file, int line)
{
++assertions_;
if (!condition) {
++failures_;
std::cerr << file << ":" << line << ": expectation failed: " << expression << "\n";
}
}
template <typename Callback>
void run(std::string_view name, Callback callback)
{
const auto failures_before = failures_;
callback(*this);
++tests_;
if (failures_ == failures_before) {
std::cout << "[pass] " << name << "\n";
} else {
std::cout << "[fail] " << name << "\n";
}
}
[[nodiscard]] int finish() const
{
std::cout << "{\"tests\":" << tests_
<< ",\"assertions\":" << assertions_
<< ",\"failures\":" << failures_ << "}\n";
return failures_ == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}
private:
int tests_ = 0;
int assertions_ = 0;
int failures_ = 0;
};
}
#define PP_EXPECT(harness, expression) \
(harness).expect(static_cast<bool>(expression), #expression, __FILE__, __LINE__)

View File

@@ -0,0 +1,71 @@
#include "test_harness.h"
#include "ui_core/color.h"
using pp::foundation::StatusCode;
using pp::ui::parse_hex_color;
namespace {
void parses_short_and_long_rgb_forms(pp::tests::Harness& h)
{
const auto short_rgb = parse_hex_color("#0f8");
const auto long_rgb = parse_hex_color("#102aFF");
PP_EXPECT(h, short_rgb.ok());
PP_EXPECT(h, short_rgb.value().r == 0x00);
PP_EXPECT(h, short_rgb.value().g == 0xff);
PP_EXPECT(h, short_rgb.value().b == 0x88);
PP_EXPECT(h, short_rgb.value().a == 0xff);
PP_EXPECT(h, long_rgb.ok());
PP_EXPECT(h, long_rgb.value().r == 0x10);
PP_EXPECT(h, long_rgb.value().g == 0x2a);
PP_EXPECT(h, long_rgb.value().b == 0xff);
PP_EXPECT(h, long_rgb.value().a == 0xff);
}
void parses_alpha_forms(pp::tests::Harness& h)
{
const auto short_rgba = parse_hex_color("#1234");
const auto long_rgba = parse_hex_color("#11223344");
PP_EXPECT(h, short_rgba.ok());
PP_EXPECT(h, short_rgba.value().r == 0x11);
PP_EXPECT(h, short_rgba.value().g == 0x22);
PP_EXPECT(h, short_rgba.value().b == 0x33);
PP_EXPECT(h, short_rgba.value().a == 0x44);
PP_EXPECT(h, long_rgba.ok());
PP_EXPECT(h, long_rgba.value().r == 0x11);
PP_EXPECT(h, long_rgba.value().g == 0x22);
PP_EXPECT(h, long_rgba.value().b == 0x33);
PP_EXPECT(h, long_rgba.value().a == 0x44);
}
void rejects_invalid_colors(pp::tests::Harness& h)
{
const auto empty = parse_hex_color("");
const auto missing_hash = parse_hex_color("112233");
const auto bad_length = parse_hex_color("#12");
const auto bad_character = parse_hex_color("#12xz45");
PP_EXPECT(h, !empty.ok());
PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !missing_hash.ok());
PP_EXPECT(h, missing_hash.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_length.ok());
PP_EXPECT(h, bad_length.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_character.ok());
PP_EXPECT(h, bad_character.status().code == StatusCode::invalid_argument);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("parses_short_and_long_rgb_forms", parses_short_and_long_rgb_forms);
harness.run("parses_alpha_forms", parses_alpha_forms);
harness.run("rejects_invalid_colors", rejects_invalid_colors);
return harness.finish();
}

View File

@@ -0,0 +1,58 @@
#include "ui_core/layout_value.h"
#include "test_harness.h"
#include <string_view>
using pp::foundation::StatusCode;
using pp::ui::LayoutLengthKind;
using pp::ui::layout_length_kind_name;
using pp::ui::parse_layout_length;
namespace {
void parses_auto_pixels_and_percent(pp::tests::Harness& h)
{
const auto auto_value = parse_layout_length("auto");
const auto pixels = parse_layout_length("28");
const auto percent = parse_layout_length("100%");
PP_EXPECT(h, auto_value.ok());
PP_EXPECT(h, auto_value.value().kind == LayoutLengthKind::auto_value);
PP_EXPECT(h, pixels.ok());
PP_EXPECT(h, pixels.value().kind == LayoutLengthKind::pixels);
PP_EXPECT(h, pixels.value().value == 28U);
PP_EXPECT(h, percent.ok());
PP_EXPECT(h, percent.value().kind == LayoutLengthKind::percent);
PP_EXPECT(h, percent.value().value == 100U);
PP_EXPECT(h, layout_length_kind_name(LayoutLengthKind::percent) == std::string_view("percent"));
}
void rejects_invalid_layout_lengths(pp::tests::Harness& h)
{
const auto empty = parse_layout_length("");
const auto negative = parse_layout_length("-1");
const auto trailing = parse_layout_length("28px");
const auto too_large_percent = parse_layout_length("101%");
const auto bare_percent = parse_layout_length("%");
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, !trailing.ok());
PP_EXPECT(h, trailing.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !too_large_percent.ok());
PP_EXPECT(h, too_large_percent.status().code == StatusCode::out_of_range);
PP_EXPECT(h, !bare_percent.ok());
PP_EXPECT(h, bare_percent.status().code == StatusCode::invalid_argument);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("parses_auto_pixels_and_percent", parses_auto_pixels_and_percent);
harness.run("rejects_invalid_layout_lengths", rejects_invalid_layout_lengths);
return harness.finish();
}

View File

@@ -0,0 +1,47 @@
#include "ui_core/layout_xml.h"
#include "test_harness.h"
using pp::foundation::StatusCode;
using pp::ui::parse_layout_xml;
namespace {
void parses_nested_layout_xml_and_lengths(pp::tests::Harness& h)
{
constexpr auto xml =
"<layout width=\"100%\" height=\"auto\">"
" <panel width=\"320\" height=\"200\">"
" <button width=\"64\" height=\"28\"/>"
" </panel>"
"</layout>";
const auto summary = parse_layout_xml(xml);
PP_EXPECT(h, summary.ok());
PP_EXPECT(h, summary.value().node_count == 3U);
PP_EXPECT(h, summary.value().length_attribute_count == 6U);
}
void rejects_malformed_empty_and_invalid_lengths(pp::tests::Harness& h)
{
const auto empty = parse_layout_xml("");
const auto malformed = parse_layout_xml("<layout><panel></layout>");
const auto invalid_length = parse_layout_xml("<layout width=\"101%\"/>");
PP_EXPECT(h, !empty.ok());
PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !malformed.ok());
PP_EXPECT(h, malformed.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !invalid_length.ok());
PP_EXPECT(h, invalid_length.status().code == StatusCode::out_of_range);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("parses_nested_layout_xml_and_lengths", parses_nested_layout_xml_and_lengths);
harness.run("rejects_malformed_empty_and_invalid_lengths", rejects_malformed_empty_and_invalid_lengths);
return harness.finish();
}

View File

@@ -0,0 +1,9 @@
add_executable(pano_cli
main.cpp)
target_link_libraries(pano_cli PRIVATE
pp_project_options
pp_project_warnings
pp_foundation
pp_assets
pp_document
pp_ui_core)

381
tools/pano_cli/main.cpp Normal file
View File

@@ -0,0 +1,381 @@
#include "assets/image_format.h"
#include "assets/image_metadata.h"
#include "assets/ppi_header.h"
#include "document/document.h"
#include "foundation/parse.h"
#include "foundation/result.h"
#include "ui_core/layout_xml.h"
#include <cstdint>
#include <fstream>
#include <iostream>
#include <iterator>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace {
struct DocumentArgs {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t layers = 1;
std::uint32_t frames = 1;
std::uint32_t frame_duration_ms = 100;
};
struct InspectImageArgs {
std::string path;
};
struct ParseLayoutArgs {
std::string path;
};
struct InspectProjectArgs {
std::string path;
};
void print_error(std::string_view command, std::string_view message)
{
std::cout << "{\"ok\":false,\"command\":\"" << command
<< "\",\"error\":\"" << message << "\"}\n";
}
void print_help()
{
std::cout
<< "pano_cli commands:\n"
<< " create-document --width N --height N [--layers N] [--frames N] [--frame-duration-ms N]\n"
<< " inspect-image --path FILE\n"
<< " inspect-project --path FILE\n"
<< " parse-layout --path FILE\n"
<< " --help\n";
}
pp::foundation::Status parse_document_args(int argc, char** argv, DocumentArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--width" || key == "--height" || key == "--layers" || key == "--frames"
|| key == "--frame-duration-ms") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = pp::foundation::parse_u32(argv[++i]);
if (!value) {
return value.status();
}
if (key == "--width") {
args.width = value.value();
} else if (key == "--height") {
args.height = value.value();
} else if (key == "--layers") {
args.layers = value.value();
} else if (key == "--frames") {
args.frames = value.value();
} else {
args.frame_duration_ms = value.value();
}
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
if (args.width == 0 || args.height == 0) {
return pp::foundation::Status::invalid_argument("width and height must be greater than zero");
}
if (args.layers == 0) {
return pp::foundation::Status::invalid_argument("layer count must be greater than zero");
}
if (args.frames == 0) {
return pp::foundation::Status::invalid_argument("frame count must be greater than zero");
}
if (args.frame_duration_ms == 0) {
return pp::foundation::Status::invalid_argument("frame duration must be greater than zero");
}
return pp::foundation::Status::success();
}
int create_document(int argc, char** argv)
{
DocumentArgs args;
const auto status = parse_document_args(argc, argv, args);
if (!status.ok()) {
print_error("create-document", status.message);
return 2;
}
const auto document_result = pp::document::CanvasDocument::create(
pp::document::DocumentConfig {
.width = args.width,
.height = args.height,
.layer_count = args.layers,
});
if (!document_result) {
print_error("create-document", document_result.status().message);
return 2;
}
auto document = document_result.value();
const auto duration_status = document.set_frame_duration(0, args.frame_duration_ms);
if (!duration_status.ok()) {
print_error("create-document", duration_status.message);
return 2;
}
for (std::uint32_t i = 1; i < args.frames; ++i) {
const auto added_frame = document.add_frame(args.frame_duration_ms);
if (!added_frame) {
print_error("create-document", added_frame.status().message);
return 2;
}
}
std::cout << "{\"ok\":true,\"command\":\"create-document\",\"document\":{"
<< "\"width\":" << document.width()
<< ",\"height\":" << document.height()
<< ",\"layers\":" << document.layers().size()
<< ",\"activeLayer\":" << document.active_layer_index()
<< ",\"frames\":" << document.frames().size()
<< ",\"activeFrame\":" << document.active_frame_index()
<< ",\"animationDurationMs\":" << document.animation_duration_ms()
<< "}}\n";
return 0;
}
pp::foundation::Status parse_inspect_image_args(int argc, char** argv, InspectImageArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--path") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.path = argv[++i];
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
if (args.path.empty()) {
return pp::foundation::Status::invalid_argument("path must not be empty");
}
return pp::foundation::Status::success();
}
int inspect_image(int argc, char** argv)
{
InspectImageArgs args;
const auto status = parse_inspect_image_args(argc, argv, args);
if (!status.ok()) {
print_error("inspect-image", status.message);
return 2;
}
std::ifstream stream(args.path, std::ios::binary);
if (!stream) {
print_error("inspect-image", "image file could not be opened");
return 2;
}
const std::vector<char> chars {
std::istreambuf_iterator<char>(stream),
std::istreambuf_iterator<char>()
};
const auto* data = reinterpret_cast<const std::byte*>(chars.data());
const auto format = pp::assets::detect_image_format(std::span<const std::byte>(data, chars.size()));
if (!format) {
print_error("inspect-image", format.status().message);
return 2;
}
pp::foundation::Result<pp::assets::ImageMetadata> metadata =
pp::foundation::Result<pp::assets::ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("image metadata is unavailable"));
if (format.value() == pp::assets::ImageFormat::png) {
metadata = pp::assets::parse_png_metadata(std::span<const std::byte>(data, chars.size()));
if (!metadata) {
print_error("inspect-image", metadata.status().message);
return 2;
}
}
std::cout << "{\"ok\":true,\"command\":\"inspect-image\",\"format\":\""
<< pp::assets::image_format_name(format.value())
<< "\",\"bytes\":" << chars.size();
if (metadata) {
std::cout << ",\"metadata\":{\"width\":" << metadata.value().width
<< ",\"height\":" << metadata.value().height
<< ",\"bitDepth\":" << static_cast<int>(metadata.value().bit_depth)
<< ",\"components\":" << static_cast<int>(metadata.value().components)
<< ",\"colorType\":\"" << pp::assets::image_color_type_name(metadata.value().color_type)
<< "\"}";
} else {
std::cout << ",\"metadata\":null";
}
std::cout << "}\n";
return 0;
}
pp::foundation::Status parse_inspect_project_args(int argc, char** argv, InspectProjectArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--path") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.path = argv[++i];
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
if (args.path.empty()) {
return pp::foundation::Status::invalid_argument("path must not be empty");
}
return pp::foundation::Status::success();
}
int inspect_project(int argc, char** argv)
{
InspectProjectArgs args;
const auto status = parse_inspect_project_args(argc, argv, args);
if (!status.ok()) {
print_error("inspect-project", status.message);
return 2;
}
std::ifstream stream(args.path, std::ios::binary);
if (!stream) {
print_error("inspect-project", "project file could not be opened");
return 2;
}
const std::vector<char> chars {
std::istreambuf_iterator<char>(stream),
std::istreambuf_iterator<char>()
};
const auto* data = reinterpret_cast<const std::byte*>(chars.data());
const auto header = pp::assets::parse_ppi_header(std::span<const std::byte>(data, chars.size()));
if (!header) {
print_error("inspect-project", header.status().message);
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"inspect-project\""
<< ",\"documentVersion\":\"" << header.value().document_version.major
<< "." << header.value().document_version.minor << "\""
<< ",\"softwareVersion\":\"" << header.value().software_version.major
<< "." << header.value().software_version.minor
<< "." << header.value().software_version.fix
<< "." << header.value().software_version.build << "\""
<< ",\"thumbnail\":{\"width\":" << header.value().thumbnail.width
<< ",\"height\":" << header.value().thumbnail.height
<< ",\"components\":" << header.value().thumbnail.components
<< "}}\n";
return 0;
}
pp::foundation::Status parse_layout_args(int argc, char** argv, ParseLayoutArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--path") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.path = argv[++i];
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
if (args.path.empty()) {
return pp::foundation::Status::invalid_argument("path must not be empty");
}
return pp::foundation::Status::success();
}
int parse_layout(int argc, char** argv)
{
ParseLayoutArgs args;
const auto status = parse_layout_args(argc, argv, args);
if (!status.ok()) {
print_error("parse-layout", status.message);
return 2;
}
std::ifstream stream(args.path, std::ios::binary);
if (!stream) {
print_error("parse-layout", "layout file could not be opened");
return 2;
}
const std::string xml {
std::istreambuf_iterator<char>(stream),
std::istreambuf_iterator<char>()
};
const auto summary = pp::ui::parse_layout_xml(xml);
if (!summary) {
print_error("parse-layout", summary.status().message);
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"parse-layout\""
<< ",\"nodes\":" << summary.value().node_count
<< ",\"lengthAttributes\":" << summary.value().length_attribute_count
<< "}\n";
return 0;
}
}
int main(int argc, char** argv)
{
if (argc < 2) {
print_help();
return 1;
}
const std::string_view command(argv[1]);
if (command == "--help" || command == "-h") {
print_help();
return 0;
}
if (command == "create-document") {
return create_document(argc, argv);
}
if (command == "inspect-image") {
return inspect_image(argc, argv);
}
if (command == "inspect-project") {
return inspect_project(argc, argv);
}
if (command == "parse-layout") {
return parse_layout(argc, argv);
}
print_error(command, "unknown command");
return 2;
}

16
vcpkg.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "panopainter",
"version-string": "0.0.0-modernization",
"description": "PanoPainter modernization dependency manifest.",
"builtin-baseline": "f9ffbaa46ad8e284b2b74919f7e0ba259564d424",
"dependencies": [
"catch2",
"curl",
"fmt",
"glad",
"glm",
"sqlite3",
"stb",
"tinyxml2"
]
}