From abe578a338058230ba94f8634c2af4bb549b670e Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 08:40:46 +0200 Subject: [PATCH] Add paint brush parameter tests --- CMakeLists.txt | 1 + docs/modernization/build-inventory.md | 4 +- docs/modernization/roadmap.md | 8 +- scripts/automation/platform-build.ps1 | 2 +- scripts/automation/platform-build.sh | 2 +- src/paint/brush.cpp | 72 +++++++++++++++ src/paint/brush.h | 37 ++++++++ tests/CMakeLists.txt | 10 ++ tests/paint/brush_tests.cpp | 127 ++++++++++++++++++++++++++ 9 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 src/paint/brush.cpp create mode 100644 src/paint/brush.h create mode 100644 tests/paint/brush_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 87e2525..457c4b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ target_link_libraries(pp_assets 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 diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 3aa1ade..9cfe704 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -81,8 +81,8 @@ Known local toolchain state: `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, PPI header, settings - document, paint stroke sampling, UI color parsing, and layout XML parse - coverage. + document, paint brush/stroke coverage, UI color parsing, and layout XML + parse coverage. - `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. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 27e8884..97ce155 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -310,8 +310,9 @@ component/name/thread/frame/stroke metadata with filtering, capacity, and invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection, PPI header recognition, and a pure typed settings document model, with corrupt/truncated/unsupported and key/value limit tests. -`pp_paint` has started with CPU reference math for the five current shader -blend modes plus deterministic stroke spacing/interpolation. `pp_document` has +`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, 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 @@ -526,7 +527,7 @@ 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_ppi_header_tests pp_assets_settings_document_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 +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_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 @@ -548,6 +549,7 @@ Results: - `pp_assets_image_format_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. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 578ac60..b4d67c7 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -1,7 +1,7 @@ [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_ppi_header_tests", "pp_assets_settings_document_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") + [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_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" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index 6b69024..a250684 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u preset="${1:-android-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_assets_settings_document_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}" +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_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" diff --git a/src/paint/brush.cpp b/src/paint/brush.cpp new file mode 100644 index 0000000..8f90192 --- /dev/null +++ b/src/paint/brush.cpp @@ -0,0 +1,72 @@ +#include "paint/brush.h" + +#include +#include + +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, + }; +} + +} diff --git a/src/paint/brush.h b/src/paint/brush.h new file mode 100644 index 0000000..169a5c7 --- /dev/null +++ b/src/paint/brush.h @@ -0,0 +1,37 @@ +#pragma once + +#include "foundation/result.h" + +#include + +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; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 526b937..61eb6c7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -96,6 +96,16 @@ add_test(NAME pp_assets_settings_document_tests COMMAND pp_assets_settings_docum 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 diff --git a/tests/paint/brush_tests.cpp b/tests/paint/brush_tests.cpp new file mode 100644 index 0000000..de67ab2 --- /dev/null +++ b/tests/paint/brush_tests.cpp @@ -0,0 +1,127 @@ +#include "paint/brush.h" +#include "test_harness.h" + +#include + +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(); +}