Add paint brush parameter tests

This commit is contained in:
2026-06-01 08:40:46 +02:00
parent 313a360c01
commit abe578a338
9 changed files with 256 additions and 7 deletions

View File

@@ -98,6 +98,7 @@ target_link_libraries(pp_assets
pp_project_warnings) pp_project_warnings)
add_library(pp_paint STATIC add_library(pp_paint STATIC
src/paint/brush.cpp
src/paint/blend.cpp src/paint/blend.cpp
src/paint/stroke.cpp) src/paint/stroke.cpp)
target_include_directories(pp_paint target_include_directories(pp_paint

View File

@@ -81,8 +81,8 @@ Known local toolchain state:
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
`pp_ui_core`, `pano_cli`, and their current headless test binaries, `pp_ui_core`, `pano_cli`, and their current headless test binaries,
including foundation event/logging/task queue coverage, PPI header, settings including foundation event/logging/task queue coverage, PPI header, settings
document, paint stroke sampling, UI color parsing, and layout XML parse document, paint brush/stroke coverage, UI color parsing, and layout XML
coverage. parse coverage.
- `panopainter_validate_shaders` validates the current combined GLSL shader - `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes. order, and existing relative includes.

View File

@@ -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, invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection,
PPI header recognition, and a pure typed settings document model, with PPI header recognition, and a pure typed settings document model, with
corrupt/truncated/unsupported and key/value limit tests. corrupt/truncated/unsupported and key/value limit tests.
`pp_paint` has started with CPU reference math for the five current shader `pp_paint` has started with pure brush parameter validation/stamp evaluation,
blend modes plus deterministic stroke spacing/interpolation. `pp_document` has 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 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 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 texture/readback descriptors and validation tests. `pp_paint_renderer` has
@@ -526,7 +527,7 @@ Last verified on 2026-06-01:
```powershell ```powershell
cmake --preset windows-msvc-default 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 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\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli 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_image_format_tests` passed.
- `pp_assets_ppi_header_tests` passed. - `pp_assets_ppi_header_tests` passed.
- `pp_assets_settings_document_tests` passed. - `pp_assets_settings_document_tests` passed.
- `pp_paint_brush_tests` passed.
- `pp_paint_blend_tests` passed. - `pp_paint_blend_tests` passed.
- `pp_paint_stroke_tests` passed. - `pp_paint_stroke_tests` passed.
- `pp_document_tests` passed. - `pp_document_tests` passed.

View File

@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string[]]$Presets = @("android-arm64"), [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" $ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}" preset="${1:-android-arm64}"
shift || true 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)" start="$(date +%s)"
cmake --preset "$preset" cmake --preset "$preset"

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

View File

@@ -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 set_tests_properties(pp_assets_settings_document_tests PROPERTIES
LABELS "assets;desktop-fast") 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 add_executable(pp_paint_blend_tests
paint/blend_tests.cpp) paint/blend_tests.cpp)
target_link_libraries(pp_paint_blend_tests PRIVATE target_link_libraries(pp_paint_blend_tests PRIVATE

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