Add paint stroke script automation
This commit is contained in:
@@ -113,7 +113,8 @@ target_link_libraries(pp_assets
|
||||
add_library(pp_paint STATIC
|
||||
src/paint/brush.cpp
|
||||
src/paint/blend.cpp
|
||||
src/paint/stroke.cpp)
|
||||
src/paint/stroke.cpp
|
||||
src/paint/stroke_script.cpp)
|
||||
target_include_directories(pp_paint
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
|
||||
@@ -92,14 +92,16 @@ Known local toolchain state:
|
||||
`pp_ui_core`, `pano_cli`, and their current headless test binaries,
|
||||
including foundation event/logging/task queue coverage, PNG metadata, PPI
|
||||
header, settings document, document frame move/duration coverage, paint
|
||||
brush/stroke coverage, renderer shader descriptor coverage, UI color
|
||||
parsing, and layout XML parse coverage.
|
||||
brush/stroke/stroke-script coverage, renderer shader descriptor coverage, UI
|
||||
color parsing, and layout XML parse coverage.
|
||||
- `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 create-document` supports `--frames` and `--frame-duration-ms` and
|
||||
is covered by `pano_cli_create_animation_document_smoke`.
|
||||
- `pano_cli simulate-stroke` exposes the pure stroke sampler for scripted
|
||||
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
|
||||
files for one vertex stage marker, one fragment stage marker, valid marker
|
||||
order, and existing relative includes.
|
||||
|
||||
@@ -317,7 +317,8 @@ document model, with corrupt/truncated/unsupported, extreme-dimension, and
|
||||
key/value limit tests.
|
||||
`pp_paint` has started with pure brush parameter validation/stamp evaluation,
|
||||
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
|
||||
move/duration queries, and layer/frame/undo-redo history invariant tests.
|
||||
`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 create-document` can create simple animation documents with explicit
|
||||
frame count/duration, and `pano_cli simulate-stroke` exercises the pure stroke
|
||||
sampler for scripted-stroke automation. `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.
|
||||
sampler for scripted-stroke automation. `pano_cli simulate-stroke-script`
|
||||
loads stroke script fixtures, parses them through `pp_paint`, and samples every
|
||||
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:
|
||||
|
||||
@@ -568,6 +571,7 @@ Results:
|
||||
- `pp_paint_brush_tests` passed.
|
||||
- `pp_paint_blend_tests` passed.
|
||||
- `pp_paint_stroke_tests` passed.
|
||||
- `pp_paint_stroke_script_tests` passed.
|
||||
- `pp_document_tests` passed, including frame move, duration, and history
|
||||
invariants.
|
||||
- `pp_renderer_api_tests` passed, including shader descriptor validation.
|
||||
@@ -585,6 +589,8 @@ Results:
|
||||
- `pano_cli_parse_layout_smoke` passed.
|
||||
- `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke
|
||||
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
|
||||
shader includes for stage markers and include graph integrity.
|
||||
- PowerShell analyze automation returns JSON summaries and includes the shader
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[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"
|
||||
|
||||
@@ -3,7 +3,7 @@ set -u
|
||||
|
||||
preset="${1:-android-arm64}"
|
||||
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)"
|
||||
|
||||
cmake --preset "$preset"
|
||||
|
||||
210
src/paint/stroke_script.cpp
Normal file
210
src/paint/stroke_script.cpp
Normal 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
28
src/paint/stroke_script.h
Normal 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);
|
||||
|
||||
}
|
||||
@@ -136,6 +136,16 @@ add_test(NAME pp_paint_stroke_tests COMMAND pp_paint_stroke_tests)
|
||||
set_tests_properties(pp_paint_stroke_tests PROPERTIES
|
||||
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
|
||||
document/document_tests.cpp)
|
||||
target_link_libraries(pp_document_tests PRIVATE
|
||||
@@ -230,4 +240,10 @@ if(TARGET pano_cli)
|
||||
set_tests_properties(pano_cli_simulate_stroke_smoke PROPERTIES
|
||||
LABELS "paint;integration;desktop-fast"
|
||||
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()
|
||||
|
||||
3
tests/data/strokes/two-strokes.ppstroke
Normal file
3
tests/data/strokes/two-strokes.ppstroke
Normal 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
|
||||
86
tests/paint/stroke_script_tests.cpp
Normal file
86
tests/paint/stroke_script_tests.cpp
Normal 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();
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "foundation/parse.h"
|
||||
#include "foundation/result.h"
|
||||
#include "paint/stroke.h"
|
||||
#include "paint/stroke_script.h"
|
||||
#include "ui_core/layout_xml.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -46,6 +47,10 @@ struct SimulateStrokeArgs {
|
||||
std::uint32_t spacing = 1;
|
||||
};
|
||||
|
||||
struct SimulateStrokeScriptArgs {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
void print_error(std::string_view command, std::string_view message)
|
||||
{
|
||||
std::cout << "{\"ok\":false,\"command\":\"" << command
|
||||
@@ -61,6 +66,7 @@ void print_help()
|
||||
<< " inspect-project --path FILE\n"
|
||||
<< " parse-layout --path FILE\n"
|
||||
<< " simulate-stroke --x1 N --y1 N --x2 N --y2 N [--spacing N]\n"
|
||||
<< " simulate-stroke-script --path FILE\n"
|
||||
<< " --help\n";
|
||||
}
|
||||
|
||||
@@ -379,6 +385,78 @@ int simulate_stroke(int argc, char** argv)
|
||||
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)
|
||||
{
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
@@ -464,6 +542,10 @@ int main(int argc, char** argv)
|
||||
return simulate_stroke(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "simulate-stroke-script") {
|
||||
return simulate_stroke_script(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "parse-layout") {
|
||||
return parse_layout(argc, argv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user