Add paint stroke sampling tests
This commit is contained in:
162
src/paint/stroke.cpp
Normal file
162
src/paint/stroke.cpp
Normal 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
39
src/paint/stroke.h
Normal 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;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user