Add paint brush parameter tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
72
src/paint/brush.cpp
Normal 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
37
src/paint/brush.h
Normal 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;
|
||||
|
||||
}
|
||||
@@ -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
127
tests/paint/brush_tests.cpp
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user