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

View File

@@ -113,7 +113,8 @@ target_link_libraries(pp_assets
add_library(pp_paint STATIC add_library(pp_paint STATIC
src/paint/brush.cpp src/paint/brush.cpp
src/paint/blend.cpp src/paint/blend.cpp
src/paint/stroke.cpp) src/paint/stroke.cpp
src/paint/stroke_script.cpp)
target_include_directories(pp_paint target_include_directories(pp_paint
PUBLIC PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src") "${CMAKE_CURRENT_SOURCE_DIR}/src")

View File

@@ -92,14 +92,16 @@ Known local toolchain state:
`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, PNG metadata, PPI including foundation event/logging/task queue coverage, PNG metadata, PPI
header, settings document, document frame move/duration coverage, paint header, settings document, document frame move/duration coverage, paint
brush/stroke coverage, renderer shader descriptor coverage, UI color brush/stroke/stroke-script coverage, renderer shader descriptor coverage, UI
parsing, and layout XML parse coverage. color parsing, and layout XML parse coverage.
- `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by - `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by
`pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture. `pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture.
- `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and - `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and
is covered by `pano_cli_create_animation_document_smoke`. is covered by `pano_cli_create_animation_document_smoke`.
- `pano_cli simulate-stroke` exposes the pure stroke sampler for scripted - `pano_cli simulate-stroke` exposes the pure stroke sampler for scripted
automation and is covered by `pano_cli_simulate_stroke_smoke`. automation and is covered by `pano_cli_simulate_stroke_smoke`.
- `pano_cli simulate-stroke-script` loads a text stroke script fixture and is
covered by `pano_cli_simulate_stroke_script_smoke`.
- `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.

View File

@@ -317,7 +317,8 @@ document model, with corrupt/truncated/unsupported, extreme-dimension, and
key/value limit tests. key/value limit tests.
`pp_paint` has started with pure brush parameter validation/stamp evaluation, `pp_paint` has started with pure brush parameter validation/stamp evaluation,
CPU reference math for the five current shader blend modes, and deterministic CPU reference math for the five current shader blend modes, and deterministic
stroke spacing/interpolation. `pp_document` has stroke spacing/interpolation plus a pure text stroke-script parser.
`pp_document` has
started with a pure canvas/layer/frame model, layer metadata operations, frame started with a pure canvas/layer/frame model, layer metadata operations, frame
move/duration queries, and layer/frame/undo-redo history invariant tests. move/duration queries, and layer/frame/undo-redo history invariant tests.
`pp_renderer_api` has started with renderer-neutral `pp_renderer_api` has started with renderer-neutral
@@ -329,9 +330,11 @@ input tests.
`pano_cli inspect-image` exposes PNG IHDR metadata as JSON, and `pano_cli inspect-image` exposes PNG IHDR metadata as JSON, and
`pano_cli create-document` can create simple animation documents with explicit `pano_cli create-document` can create simple animation documents with explicit
frame count/duration, and `pano_cli simulate-stroke` exercises the pure stroke frame count/duration, and `pano_cli simulate-stroke` exercises the pure stroke
sampler for scripted-stroke automation. `pano_cli parse-layout` exercises the sampler for scripted-stroke automation. `pano_cli simulate-stroke-script`
XML layout path. Continue expanding document behavior toward legacy Canvas loads stroke script fixtures, parses them through `pp_paint`, and samples every
parity and then port OpenGL classes behind the renderer boundary. stroke. `pano_cli parse-layout` exercises the XML layout path. Continue
expanding document behavior toward legacy Canvas parity and then port OpenGL
classes behind the renderer boundary.
Implementation tasks: Implementation tasks:
@@ -568,6 +571,7 @@ Results:
- `pp_paint_brush_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_paint_stroke_script_tests` passed.
- `pp_document_tests` passed, including frame move, duration, and history - `pp_document_tests` passed, including frame move, duration, and history
invariants. invariants.
- `pp_renderer_api_tests` passed, including shader descriptor validation. - `pp_renderer_api_tests` passed, including shader descriptor validation.
@@ -585,6 +589,8 @@ Results:
- `pano_cli_parse_layout_smoke` passed. - `pano_cli_parse_layout_smoke` passed.
- `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke - `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke
sample counts/distances. sample counts/distances.
- `pano_cli_simulate_stroke_script_smoke` passed and reports deterministic
aggregate stroke-script counts/distances.
- `panopainter_validate_shaders` passed, validating 25 shader programs and 7 - `panopainter_validate_shaders` passed, validating 25 shader programs and 7
shader includes for stage markers and include graph integrity. shader includes for stage markers and include graph integrity.
- PowerShell analyze automation returns JSON summaries and includes the shader - PowerShell analyze automation returns JSON summaries and includes the shader

View File

@@ -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_image_metadata_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") [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_image_metadata_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_paint_stroke_script_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"

View File

@@ -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_image_metadata_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}" 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_image_metadata_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_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"

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);
}

View File

@@ -136,6 +136,16 @@ add_test(NAME pp_paint_stroke_tests COMMAND pp_paint_stroke_tests)
set_tests_properties(pp_paint_stroke_tests PROPERTIES set_tests_properties(pp_paint_stroke_tests PROPERTIES
LABELS "paint;desktop-fast") LABELS "paint;desktop-fast")
add_executable(pp_paint_stroke_script_tests
paint/stroke_script_tests.cpp)
target_link_libraries(pp_paint_stroke_script_tests PRIVATE
pp_paint
pp_test_harness)
add_test(NAME pp_paint_stroke_script_tests COMMAND pp_paint_stroke_script_tests)
set_tests_properties(pp_paint_stroke_script_tests PROPERTIES
LABELS "paint;desktop-fast")
add_executable(pp_document_tests add_executable(pp_document_tests
document/document_tests.cpp) document/document_tests.cpp)
target_link_libraries(pp_document_tests PRIVATE target_link_libraries(pp_document_tests PRIVATE
@@ -230,4 +240,10 @@ if(TARGET pano_cli)
set_tests_properties(pano_cli_simulate_stroke_smoke PROPERTIES set_tests_properties(pano_cli_simulate_stroke_smoke PROPERTIES
LABELS "paint;integration;desktop-fast" LABELS "paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"samples\":6.*\"distance\":10") PASS_REGULAR_EXPRESSION "\"samples\":6.*\"distance\":10")
add_test(NAME pano_cli_simulate_stroke_script_smoke
COMMAND pano_cli simulate-stroke-script --path "${CMAKE_CURRENT_SOURCE_DIR}/data/strokes/two-strokes.ppstroke")
set_tests_properties(pano_cli_simulate_stroke_script_smoke PROPERTIES
LABELS "paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"strokes\":2.*\"samples\":9.*\"distance\":20")
endif() endif()

View File

@@ -0,0 +1,3 @@
# PanoPainter automation stroke script
stroke 0 0 0.25 10 0 0.75 2
stroke 10 0 1 10 10 0.5 5

View File

@@ -0,0 +1,86 @@
#include "paint/stroke_script.h"
#include "test_harness.h"
#include <string>
#include <string_view>
using pp::foundation::StatusCode;
using pp::paint::max_stroke_script_bytes;
using pp::paint::max_stroke_script_line_length;
using pp::paint::parse_stroke_script;
namespace {
void parses_comments_and_multiple_strokes(pp::tests::Harness& h)
{
constexpr std::string_view script_text =
"# scripted automation fixture\n"
"stroke 0 0 0.25 10 0 0.75 2\n"
"\n"
"stroke 10 0 1 10 10 0.5 5 # trailing comment\n";
const auto script = parse_stroke_script(script_text);
PP_EXPECT(h, script.ok());
PP_EXPECT(h, script.value().strokes.size() == 2U);
PP_EXPECT(h, script.value().strokes[0].start.x == 0.0F);
PP_EXPECT(h, script.value().strokes[0].start.pressure == 0.25F);
PP_EXPECT(h, script.value().strokes[0].end.x == 10.0F);
PP_EXPECT(h, script.value().strokes[0].spacing == 2.0F);
PP_EXPECT(h, script.value().strokes[1].end.y == 10.0F);
PP_EXPECT(h, script.value().strokes[1].end.pressure == 0.5F);
}
void rejects_malformed_stroke_scripts(pp::tests::Harness& h)
{
const auto empty = parse_stroke_script("");
const auto comments_only = parse_stroke_script("# nope\n\n");
const auto unknown = parse_stroke_script("move 0 0 1 10 0 1 2\n");
const auto missing_tokens = parse_stroke_script("stroke 0 0 1 10 0 1\n");
const auto too_many_tokens = parse_stroke_script("stroke 0 0 1 10 0 1 2 extra\n");
const auto bad_number = parse_stroke_script("stroke 0 0 1 10 nope 1 2\n");
const auto nan_number = parse_stroke_script("stroke 0 0 1 10 nan 1 2\n");
const auto zero_spacing = parse_stroke_script("stroke 0 0 1 10 0 1 0\n");
PP_EXPECT(h, !empty.ok());
PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !comments_only.ok());
PP_EXPECT(h, comments_only.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !unknown.ok());
PP_EXPECT(h, unknown.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !missing_tokens.ok());
PP_EXPECT(h, missing_tokens.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !too_many_tokens.ok());
PP_EXPECT(h, too_many_tokens.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_number.ok());
PP_EXPECT(h, bad_number.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !nan_number.ok());
PP_EXPECT(h, nan_number.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !zero_spacing.ok());
PP_EXPECT(h, zero_spacing.status().code == StatusCode::invalid_argument);
}
void rejects_oversized_stroke_scripts(pp::tests::Harness& h)
{
const std::string oversized_script(max_stroke_script_bytes + 1U, 'x');
const std::string oversized_line(max_stroke_script_line_length + 1U, 'x');
const auto too_large_script = parse_stroke_script(oversized_script);
const auto too_large_line = parse_stroke_script(oversized_line);
PP_EXPECT(h, !too_large_script.ok());
PP_EXPECT(h, too_large_script.status().code == StatusCode::out_of_range);
PP_EXPECT(h, !too_large_line.ok());
PP_EXPECT(h, too_large_line.status().code == StatusCode::out_of_range);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("parses_comments_and_multiple_strokes", parses_comments_and_multiple_strokes);
harness.run("rejects_malformed_stroke_scripts", rejects_malformed_stroke_scripts);
harness.run("rejects_oversized_stroke_scripts", rejects_oversized_stroke_scripts);
return harness.finish();
}

View File

@@ -5,6 +5,7 @@
#include "foundation/parse.h" #include "foundation/parse.h"
#include "foundation/result.h" #include "foundation/result.h"
#include "paint/stroke.h" #include "paint/stroke.h"
#include "paint/stroke_script.h"
#include "ui_core/layout_xml.h" #include "ui_core/layout_xml.h"
#include <cstdint> #include <cstdint>
@@ -46,6 +47,10 @@ struct SimulateStrokeArgs {
std::uint32_t spacing = 1; std::uint32_t spacing = 1;
}; };
struct SimulateStrokeScriptArgs {
std::string path;
};
void print_error(std::string_view command, std::string_view message) void print_error(std::string_view command, std::string_view message)
{ {
std::cout << "{\"ok\":false,\"command\":\"" << command std::cout << "{\"ok\":false,\"command\":\"" << command
@@ -61,6 +66,7 @@ void print_help()
<< " inspect-project --path FILE\n" << " inspect-project --path FILE\n"
<< " parse-layout --path FILE\n" << " parse-layout --path FILE\n"
<< " simulate-stroke --x1 N --y1 N --x2 N --y2 N [--spacing N]\n" << " simulate-stroke --x1 N --y1 N --x2 N --y2 N [--spacing N]\n"
<< " simulate-stroke-script --path FILE\n"
<< " --help\n"; << " --help\n";
} }
@@ -379,6 +385,78 @@ int simulate_stroke(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_simulate_stroke_script_args(int argc, char** argv, SimulateStrokeScriptArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--path") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.path = argv[++i];
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
if (args.path.empty()) {
return pp::foundation::Status::invalid_argument("path must not be empty");
}
return pp::foundation::Status::success();
}
int simulate_stroke_script(int argc, char** argv)
{
SimulateStrokeScriptArgs args;
const auto status = parse_simulate_stroke_script_args(argc, argv, args);
if (!status.ok()) {
print_error("simulate-stroke-script", status.message);
return 2;
}
std::ifstream stream(args.path, std::ios::binary);
if (!stream) {
print_error("simulate-stroke-script", "stroke script file could not be opened");
return 2;
}
const std::string text {
std::istreambuf_iterator<char>(stream),
std::istreambuf_iterator<char>()
};
const auto script = pp::paint::parse_stroke_script(text);
if (!script) {
print_error("simulate-stroke-script", script.status().message);
return 2;
}
std::size_t total_samples = 0;
float total_distance = 0.0F;
for (const auto& stroke : script.value().strokes) {
const pp::paint::StrokePoint points[] { stroke.start, stroke.end };
const auto samples = pp::paint::sample_stroke(
points,
pp::paint::StrokeSamplingConfig {
.spacing = stroke.spacing,
});
if (!samples) {
print_error("simulate-stroke-script", samples.status().message);
return 2;
}
total_samples += samples.value().size();
total_distance += samples.value().back().distance;
}
std::cout << "{\"ok\":true,\"command\":\"simulate-stroke-script\""
<< ",\"strokes\":" << script.value().strokes.size()
<< ",\"samples\":" << total_samples
<< ",\"distance\":" << total_distance << "}\n";
return 0;
}
pp::foundation::Status parse_layout_args(int argc, char** argv, ParseLayoutArgs& args) pp::foundation::Status parse_layout_args(int argc, char** argv, ParseLayoutArgs& args)
{ {
for (int i = 2; i < argc; ++i) { for (int i = 2; i < argc; ++i) {
@@ -464,6 +542,10 @@ int main(int argc, char** argv)
return simulate_stroke(argc, argv); return simulate_stroke(argc, argv);
} }
if (command == "simulate-stroke-script") {
return simulate_stroke_script(argc, argv);
}
if (command == "parse-layout") { if (command == "parse-layout") {
return parse_layout(argc, argv); return parse_layout(argc, argv);
} }