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)
add_library(pp_paint STATIC
src/paint/brush.cpp
src/paint/blend.cpp
src/paint/stroke.cpp)
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_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.

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

View File

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

View File

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

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

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