Add paint stroke sampling tests

This commit is contained in:
2026-06-01 08:08:27 +02:00
parent f9e4bcaeea
commit 93d8aaaffd
9 changed files with 350 additions and 8 deletions

162
src/paint/stroke.cpp Normal file
View File

@@ -0,0 +1,162 @@
#include "paint/stroke.h"
#include <algorithm>
#include <cmath>
namespace pp::paint {
namespace {
[[nodiscard]] bool is_finite_point(const StrokePoint& point) noexcept
{
return std::isfinite(point.x) && std::isfinite(point.y) && std::isfinite(point.pressure);
}
[[nodiscard]] float clamp_pressure(float pressure) noexcept
{
return std::clamp(pressure, 0.0F, 1.0F);
}
[[nodiscard]] float distance_between(const StrokePoint& a, const StrokePoint& b) noexcept
{
const auto dx = b.x - a.x;
const auto dy = b.y - a.y;
return std::sqrt(dx * dx + dy * dy);
}
[[nodiscard]] StrokeSample interpolate_sample(
const StrokePoint& a,
const StrokePoint& b,
float t,
float distance) noexcept
{
return StrokeSample {
.x = a.x + ((b.x - a.x) * t),
.y = a.y + ((b.y - a.y) * t),
.pressure = clamp_pressure(a.pressure + ((b.pressure - a.pressure) * t)),
.distance = distance,
};
}
[[nodiscard]] pp::foundation::Status validate_input(
std::span<const StrokePoint> points,
StrokeSamplingConfig config) noexcept
{
if (points.size() < 2U) {
return pp::foundation::Status::invalid_argument("stroke sampling requires at least two points");
}
if (points.size() > max_stroke_points) {
return pp::foundation::Status::out_of_range("stroke point count exceeds the configured limit");
}
if (!std::isfinite(config.spacing) || config.spacing <= 0.0F) {
return pp::foundation::Status::invalid_argument("stroke spacing must be finite and greater than zero");
}
if (config.max_samples == 0U || config.max_samples > max_stroke_samples) {
return pp::foundation::Status::out_of_range("stroke sample count limit is outside the configured range");
}
for (const auto& point : points) {
if (!is_finite_point(point)) {
return pp::foundation::Status::invalid_argument("stroke points must contain finite coordinates and pressure");
}
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status append_sample(
std::vector<StrokeSample>& samples,
StrokeSample sample,
std::size_t max_samples)
{
if (samples.size() >= max_samples) {
return pp::foundation::Status::out_of_range("stroke sampling exceeded the configured sample limit");
}
samples.push_back(sample);
return pp::foundation::Status::success();
}
}
pp::foundation::Result<std::vector<StrokeSample>> sample_stroke(
std::span<const StrokePoint> points,
StrokeSamplingConfig config) noexcept
{
const auto input_status = validate_input(points, config);
if (!input_status.ok()) {
return pp::foundation::Result<std::vector<StrokeSample>>::failure(input_status);
}
std::vector<StrokeSample> samples;
samples.reserve(std::min<std::size_t>(points.size(), config.max_samples));
auto status = append_sample(
samples,
StrokeSample {
.x = points.front().x,
.y = points.front().y,
.pressure = clamp_pressure(points.front().pressure),
.distance = 0.0F,
},
config.max_samples);
if (!status.ok()) {
return pp::foundation::Result<std::vector<StrokeSample>>::failure(status);
}
float segment_start_distance = 0.0F;
float next_sample_distance = config.spacing;
float total_distance = 0.0F;
for (std::size_t i = 1; i < points.size(); ++i) {
const auto& a = points[i - 1U];
const auto& b = points[i];
const auto segment_length = distance_between(a, b);
if (segment_length <= 0.0F) {
continue;
}
const auto segment_end_distance = segment_start_distance + segment_length;
while (next_sample_distance <= segment_end_distance) {
const auto t = (next_sample_distance - segment_start_distance) / segment_length;
status = append_sample(
samples,
interpolate_sample(a, b, t, next_sample_distance),
config.max_samples);
if (!status.ok()) {
return pp::foundation::Result<std::vector<StrokeSample>>::failure(status);
}
next_sample_distance += config.spacing;
}
segment_start_distance = segment_end_distance;
total_distance = segment_end_distance;
}
if (total_distance <= 0.0F) {
return pp::foundation::Result<std::vector<StrokeSample>>::failure(
pp::foundation::Status::invalid_argument("stroke path must have nonzero length"));
}
if (config.include_endpoint && samples.back().distance < total_distance) {
status = append_sample(
samples,
StrokeSample {
.x = points.back().x,
.y = points.back().y,
.pressure = clamp_pressure(points.back().pressure),
.distance = total_distance,
},
config.max_samples);
if (!status.ok()) {
return pp::foundation::Result<std::vector<StrokeSample>>::failure(status);
}
}
return pp::foundation::Result<std::vector<StrokeSample>>::success(samples);
}
}

39
src/paint/stroke.h Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
namespace pp::paint {
constexpr std::size_t max_stroke_points = 1000000;
constexpr std::size_t max_stroke_samples = 1000000;
struct StrokePoint {
float x = 0.0F;
float y = 0.0F;
float pressure = 1.0F;
std::uint32_t time_ms = 0;
};
struct StrokeSample {
float x = 0.0F;
float y = 0.0F;
float pressure = 1.0F;
float distance = 0.0F;
};
struct StrokeSamplingConfig {
float spacing = 1.0F;
bool include_endpoint = true;
std::size_t max_samples = max_stroke_samples;
};
[[nodiscard]] pp::foundation::Result<std::vector<StrokeSample>> sample_stroke(
std::span<const StrokePoint> points,
StrokeSamplingConfig config) noexcept;
}