Add paint brush parameter tests
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
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
|
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
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