Add paint stroke script automation

This commit is contained in:
2026-06-01 12:34:15 +02:00
parent dc252b2f24
commit 37854ea8b9
11 changed files with 443 additions and 9 deletions

210
src/paint/stroke_script.cpp Normal file
View File

@@ -0,0 +1,210 @@
#include "paint/stroke_script.h"
#include <array>
#include <cerrno>
#include <cmath>
#include <cstdlib>
namespace pp::paint {
namespace {
[[nodiscard]] std::string_view trim(std::string_view text) noexcept
{
while (!text.empty() && (text.front() == ' ' || text.front() == '\t' || text.front() == '\r')) {
text.remove_prefix(1);
}
while (!text.empty() && (text.back() == ' ' || text.back() == '\t' || text.back() == '\r')) {
text.remove_suffix(1);
}
return text;
}
[[nodiscard]] std::string_view strip_comment(std::string_view line) noexcept
{
const auto comment = line.find('#');
if (comment == std::string_view::npos) {
return line;
}
return line.substr(0, comment);
}
[[nodiscard]] pp::foundation::Result<float> parse_float_token(std::string_view token) noexcept
{
token = trim(token);
if (token.empty() || token.size() >= 64U) {
return pp::foundation::Result<float>::failure(
pp::foundation::Status::invalid_argument("stroke script numeric token is invalid"));
}
std::array<char, 64> buffer {};
for (std::size_t i = 0; i < token.size(); ++i) {
buffer[i] = token[i];
}
char* end = nullptr;
errno = 0;
const auto value = std::strtof(buffer.data(), &end);
if (errno != 0 || end != buffer.data() + static_cast<std::ptrdiff_t>(token.size()) || !std::isfinite(value)) {
return pp::foundation::Result<float>::failure(
pp::foundation::Status::invalid_argument("stroke script numeric token is invalid"));
}
return pp::foundation::Result<float>::success(value);
}
[[nodiscard]] pp::foundation::Result<std::size_t> split_tokens(
std::string_view line,
std::array<std::string_view, 8>& tokens) noexcept
{
std::size_t count = 0;
std::size_t offset = 0;
while (offset < line.size()) {
while (offset < line.size() && (line[offset] == ' ' || line[offset] == '\t')) {
++offset;
}
if (offset >= line.size()) {
break;
}
const auto token_start = offset;
while (offset < line.size() && line[offset] != ' ' && line[offset] != '\t') {
++offset;
}
if (count >= tokens.size()) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::invalid_argument("stroke script line has too many tokens"));
}
tokens[count] = line.substr(token_start, offset - token_start);
++count;
}
return pp::foundation::Result<std::size_t>::success(count);
}
[[nodiscard]] pp::foundation::Result<StrokeScriptStroke> parse_stroke_line(std::string_view line) noexcept
{
std::array<std::string_view, 8> tokens {};
const auto token_count = split_tokens(line, tokens);
if (!token_count) {
return pp::foundation::Result<StrokeScriptStroke>::failure(token_count.status());
}
if (token_count.value() != tokens.size() || tokens[0] != "stroke") {
return pp::foundation::Result<StrokeScriptStroke>::failure(
pp::foundation::Status::invalid_argument("stroke script line must be 'stroke x1 y1 p1 x2 y2 p2 spacing'"));
}
const auto x1 = parse_float_token(tokens[1]);
const auto y1 = parse_float_token(tokens[2]);
const auto p1 = parse_float_token(tokens[3]);
const auto x2 = parse_float_token(tokens[4]);
const auto y2 = parse_float_token(tokens[5]);
const auto p2 = parse_float_token(tokens[6]);
const auto spacing = parse_float_token(tokens[7]);
if (!x1) {
return pp::foundation::Result<StrokeScriptStroke>::failure(x1.status());
}
if (!y1) {
return pp::foundation::Result<StrokeScriptStroke>::failure(y1.status());
}
if (!p1) {
return pp::foundation::Result<StrokeScriptStroke>::failure(p1.status());
}
if (!x2) {
return pp::foundation::Result<StrokeScriptStroke>::failure(x2.status());
}
if (!y2) {
return pp::foundation::Result<StrokeScriptStroke>::failure(y2.status());
}
if (!p2) {
return pp::foundation::Result<StrokeScriptStroke>::failure(p2.status());
}
if (!spacing) {
return pp::foundation::Result<StrokeScriptStroke>::failure(spacing.status());
}
if (spacing.value() <= 0.0F) {
return pp::foundation::Result<StrokeScriptStroke>::failure(
pp::foundation::Status::invalid_argument("stroke script spacing must be greater than zero"));
}
return pp::foundation::Result<StrokeScriptStroke>::success(StrokeScriptStroke {
.start = StrokePoint {
.x = x1.value(),
.y = y1.value(),
.pressure = p1.value(),
},
.end = StrokePoint {
.x = x2.value(),
.y = y2.value(),
.pressure = p2.value(),
},
.spacing = spacing.value(),
});
}
}
pp::foundation::Result<StrokeScript> parse_stroke_script(std::string_view text)
{
if (text.empty()) {
return pp::foundation::Result<StrokeScript>::failure(
pp::foundation::Status::invalid_argument("stroke script must not be empty"));
}
if (text.size() > max_stroke_script_bytes) {
return pp::foundation::Result<StrokeScript>::failure(
pp::foundation::Status::out_of_range("stroke script exceeds the configured size limit"));
}
StrokeScript script;
std::size_t offset = 0;
while (offset <= text.size()) {
const auto line_start = offset;
const auto line_end = text.find('\n', line_start);
if (line_end == std::string_view::npos) {
offset = text.size() + 1U;
} else {
offset = line_end + 1U;
}
auto line = text.substr(line_start, (line_end == std::string_view::npos) ? std::string_view::npos : line_end - line_start);
if (line.size() > max_stroke_script_line_length) {
return pp::foundation::Result<StrokeScript>::failure(
pp::foundation::Status::out_of_range("stroke script line exceeds the configured length limit"));
}
line = trim(strip_comment(line));
if (line.empty()) {
continue;
}
if (script.strokes.size() >= max_stroke_script_strokes) {
return pp::foundation::Result<StrokeScript>::failure(
pp::foundation::Status::out_of_range("stroke script stroke count exceeds the configured limit"));
}
const auto stroke = parse_stroke_line(line);
if (!stroke) {
return pp::foundation::Result<StrokeScript>::failure(stroke.status());
}
script.strokes.push_back(stroke.value());
}
if (script.strokes.empty()) {
return pp::foundation::Result<StrokeScript>::failure(
pp::foundation::Status::invalid_argument("stroke script must contain at least one stroke"));
}
return pp::foundation::Result<StrokeScript>::success(script);
}
}

28
src/paint/stroke_script.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include "foundation/result.h"
#include "paint/stroke.h"
#include <cstddef>
#include <string_view>
#include <vector>
namespace pp::paint {
constexpr std::size_t max_stroke_script_bytes = 1024 * 1024;
constexpr std::size_t max_stroke_script_line_length = 512;
constexpr std::size_t max_stroke_script_strokes = 10000;
struct StrokeScriptStroke {
StrokePoint start;
StrokePoint end;
float spacing = 1.0F;
};
struct StrokeScript {
std::vector<StrokeScriptStroke> strokes;
};
[[nodiscard]] pp::foundation::Result<StrokeScript> parse_stroke_script(std::string_view text);
}