From 1bc90d88b468d393ebf83e2b80c4e1b5224f7575 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 2 Jun 2026 11:00:29 +0200 Subject: [PATCH] Add targeted PPI payload automation --- docs/modernization/build-inventory.md | 13 ++- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 18 +-- src/assets/ppi_header.cpp | 34 ++++-- src/assets/ppi_header.h | 2 + tests/assets/ppi_header_tests.cpp | 106 ++++++++++++++++++ ...o_cli_save_project_payload_roundtrip.cmake | 26 ++++- tools/pano_cli/main.cpp | 28 ++++- 8 files changed, 203 insertions(+), 26 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index a82d8b1..473b89a 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -122,9 +122,11 @@ Known local toolchain state: `pano_cli_load_project_metadata_smoke`. - `pano_cli save-project` writes generated multi-layer, multi-frame PPI files with configurable layer opacity, blend mode, alpha lock, and visibility - through `pp_assets` and is covered by `pano_cli_save_project_roundtrip_smoke` + through `pp_assets`; test dirty-face payloads can target explicit generated + layer/frame slots. It is covered by `pano_cli_save_project_roundtrip_smoke` and `pano_cli_save_project_payload_roundtrip_smoke`, which reload generated - metadata-only and dirty-face-payload projects through `pano_cli load-project`. + metadata-only and targeted dirty-face-payload projects through + `pano_cli load-project`. - `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and is covered by `pano_cli_create_animation_document_smoke`. - `pano_cli simulate-document-edits` exercises pure document layer/frame edit @@ -287,9 +289,10 @@ Known local toolchain state: automation and is covered by `pano_cli_export_image_roundtrip_smoke`; full legacy canvas export remains a future CLI task. - `pano_cli save-project` exposes generated multi-layer, multi-frame PPI - writing with layer metadata through JSON automation and is covered by - metadata-only and dirty-face-payload round-trip smoke tests; full legacy - canvas save parity remains tracked by DEBT-0013. + writing with layer metadata and targeted dirty-face layer/frame payloads + through JSON automation and is covered by metadata-only and + dirty-face-payload round-trip smoke tests; full legacy canvas save parity + remains tracked by DEBT-0013. - `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON` through the vcpkg preset; default and Android validation still use the retained vendored fallback tracked by DEBT-0012. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index b765ba7..7fed844 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -31,7 +31,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, renderer-free RGBA8 face payload storage, and renderer-free alpha8 selection-mask storage, but it is not yet wired to legacy `Canvas`, save, or legacy action commands | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pp_document_ppi_import_tests`; `pano_cli_simulate_document_edits_smoke` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade | | DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact only, not AppX/APK/Apple/WebGL package outputs | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Package-smoke covers Windows AppX, Android APK variants, Apple bundles, and WebGL output where local toolchains are present | | DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception | -| DEBT-0013 | Open | Modernization | `pp_assets`, `pano_cli inspect-project`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, generated multi-layer/multi-frame PPI writing with layer opacity/blend/alpha-lock/visibility metadata, metadata-only and dirty-face-payload save/load round-trips, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, and decoded pixel attachment to `pp_document`, but full legacy PPI round-trip parity is not yet extracted | Full PPI save parity requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke`; `pano_cli_save_project_roundtrip_smoke`; `pano_cli_save_project_payload_roundtrip_smoke` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, corrupt payloads, dirty-face payload saving, arbitrary legacy canvas payload/layout combinations, and legacy app round-trip compatibility | +| DEBT-0013 | Open | Modernization | `pp_assets`, `pano_cli inspect-project`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, generated multi-layer/multi-frame PPI writing with layer opacity/blend/alpha-lock/visibility metadata, metadata-only and targeted dirty-face-payload save/load round-trips, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, and decoded pixel attachment to `pp_document`, but full legacy PPI round-trip parity is not yet extracted | Full PPI save parity requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke`; `pano_cli_save_project_roundtrip_smoke`; `pano_cli_save_project_payload_roundtrip_smoke` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, corrupt payloads, dirty-face payload saving, arbitrary legacy canvas payload/layout combinations, and legacy app round-trip compatibility | | DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index c5e74a3..6d0128b 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -347,7 +347,8 @@ payloads are present. `pano_cli save-project` writes generated multi-layer, multi-frame PPI files with layer opacity, blend mode, alpha lock, and visibility metadata through the extracted `pp_assets` writer and round-trips metadata-only and test -dirty-face-payload variants through `load-project`. +dirty-face-payload variants through `load-project`; dirty-face payloads can be +targeted to explicit generated layer/frame slots for animation coverage. `pano_cli create-document` can create simple animation documents with explicit frame count/duration. `pano_cli simulate-document-edits` exercises pure layer metadata, frame reordering, active-index preservation, tiny face-payload @@ -680,7 +681,8 @@ Results: payload rejection. - `pp_assets_ppi_header_tests` passed, including PPI thumbnail/body layout, body summary validation, layer/frame indexing, dirty-face PNG payload - metadata validation, and decoded dirty-face payload coverage. + metadata validation, targeted layer/frame dirty-face writing, and decoded + dirty-face payload coverage. - `pp_assets_settings_document_tests` passed. - `pp_paint_brush_tests` passed. - `pp_paint_blend_tests` passed. @@ -725,8 +727,9 @@ Results: `pp_assets` PPI writer can save a generated multi-frame PPI and reload it through `pano_cli load-project`. - `pano_cli_save_project_payload_roundtrip_smoke` passed and proves the - `pp_assets` PPI writer can save a compressed RGBA PNG dirty-face payload and - reload it as decoded `pp_document` face-pixel data. + `pp_assets` PPI writer can save a compressed RGBA PNG dirty-face payload to + an explicit layer/frame slot, inspect the serialized descriptor, and reload + it as decoded `pp_document` face-pixel data. - `pano_cli_parse_layout_smoke` passed. - `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke sample counts/distances. @@ -794,9 +797,10 @@ Results: and has a save/import round-trip smoke test. Full legacy canvas export remains a future `pano_cli` task. - `pano_cli save-project` exposes generated multi-layer, multi-frame PPI - writing with layer metadata through JSON automation and is covered by - metadata-only and dirty-face-payload save/load round-trip smoke tests. Full - legacy canvas save parity remains tracked by DEBT-0013. + writing with layer metadata and targeted dirty-face layer/frame payloads + through JSON automation and is covered by metadata-only and + dirty-face-payload save/load round-trip smoke tests. Full legacy canvas save + parity remains tracked by DEBT-0013. - PowerShell package-smoke wrapper validates the Windows CMake app executable and runtime `data/` copy. - Android arm64 configured with NDK 29.0.14206865 through the platform-build diff --git a/src/assets/ppi_header.cpp b/src/assets/ppi_header.cpp index 627c9e3..ec530fc 100644 --- a/src/assets/ppi_header.cpp +++ b/src/assets/ppi_header.cpp @@ -3,6 +3,7 @@ #include "assets/image_metadata.h" #include "foundation/binary_stream.h" +#include #include #include #include @@ -705,19 +706,33 @@ pp::foundation::Result> create_minimal_ppi_project(PpiMin } } - bool seen_faces[6] {}; + std::vector> seen_faces( + static_cast(config.layer_count) * static_cast(config.frame_count)); std::uint64_t total_payload_bytes = 0; for (const auto& face : config.dirty_faces) { + if (face.layer_index >= config.layer_count) { + return pp::foundation::Result>::failure( + pp::foundation::Status::out_of_range("PPI dirty face layer index is outside the layer list")); + } + + if (face.frame_index >= config.frame_count) { + return pp::foundation::Result>::failure( + pp::foundation::Status::out_of_range("PPI dirty face frame index is outside the frame list")); + } + if (face.face_index >= 6U) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI dirty face index is outside the cube face list")); } - if (seen_faces[face.face_index]) { + const auto slot_index = + static_cast(face.layer_index) * static_cast(config.frame_count) + + static_cast(face.frame_index); + if (seen_faces[slot_index][face.face_index]) { return pp::foundation::Result>::failure( - pp::foundation::Status::invalid_argument("PPI dirty face index is duplicated")); + pp::foundation::Status::invalid_argument("PPI dirty face slot is duplicated")); } - seen_faces[face.face_index] = true; + seen_faces[slot_index][face.face_index] = true; if (face.width == 0 || face.height == 0) { return pp::foundation::Result>::failure( @@ -790,12 +805,11 @@ pp::foundation::Result> create_minimal_ppi_project(PpiMin append_u32(bytes, config.frame_duration_ms); for (std::uint32_t face = 0; face < 6U; ++face) { const PpiDirtyFacePayloadConfig* dirty_face = nullptr; - if (layer == 0U && frame == 0U) { - for (const auto& candidate : config.dirty_faces) { - if (candidate.face_index == face) { - dirty_face = &candidate; - break; - } + for (const auto& candidate : config.dirty_faces) { + if (candidate.layer_index == layer && candidate.frame_index == frame + && candidate.face_index == face) { + dirty_face = &candidate; + break; } } diff --git a/src/assets/ppi_header.h b/src/assets/ppi_header.h index 884e3c3..0e6a99b 100644 --- a/src/assets/ppi_header.h +++ b/src/assets/ppi_header.h @@ -119,6 +119,8 @@ struct PpiDecodedProjectImages { }; struct PpiDirtyFacePayloadConfig { + std::uint32_t layer_index = 0; + std::uint32_t frame_index = 0; std::uint32_t face_index = 0; std::uint32_t x = 0; std::uint32_t y = 0; diff --git a/tests/assets/ppi_header_tests.cpp b/tests/assets/ppi_header_tests.cpp index 647024b..b59ded0 100644 --- a/tests/assets/ppi_header_tests.cpp +++ b/tests/assets/ppi_header_tests.cpp @@ -486,6 +486,8 @@ void creates_minimal_project_with_dirty_face_payload(pp::tests::Harness& h) const auto png_payload = transparent_png_1x1(); const pp::assets::PpiDirtyFacePayloadConfig dirty_faces[] { { + .layer_index = 0, + .frame_index = 0, .face_index = 2, .x = 4, .y = 5, @@ -520,11 +522,64 @@ void creates_minimal_project_with_dirty_face_payload(pp::tests::Harness& h) PP_EXPECT(h, decoded.value().faces[0].image.pixels.size() == 4U); } +void creates_minimal_project_with_targeted_dirty_face_payloads(pp::tests::Harness& h) +{ + const auto png_payload = transparent_png_1x1(); + const pp::assets::PpiDirtyFacePayloadConfig dirty_faces[] { + { + .layer_index = 0, + .frame_index = 1, + .face_index = 2, + .x = 4, + .y = 5, + .width = 1, + .height = 1, + .png_rgba8 = std::span(png_payload.data(), png_payload.size()), + }, + { + .layer_index = 1, + .frame_index = 0, + .face_index = 2, + .x = 6, + .y = 7, + .width = 1, + .height = 1, + .png_rgba8 = std::span(png_payload.data(), png_payload.size()), + }, + }; + const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { + .width = 256, + .height = 128, + .layer_name = "Payload", + .layer_metadata = {}, + .layer_count = 2, + .frame_count = 2, + .frame_duration_ms = 333, + .dirty_faces = std::span(dirty_faces, 2), + }); + + PP_EXPECT(h, project.ok()); + const auto decoded = decode_ppi_project_images(project.value()); + PP_EXPECT(h, decoded.ok()); + PP_EXPECT(h, decoded.value().project.body.summary.dirty_face_count == 2U); + PP_EXPECT(h, decoded.value().faces.size() == 2U); + PP_EXPECT(h, decoded.value().faces[0].layer_index == 0U); + PP_EXPECT(h, decoded.value().faces[0].frame_index == 1U); + PP_EXPECT(h, decoded.value().faces[0].face_index == 2U); + PP_EXPECT(h, decoded.value().faces[0].descriptor.x0 == 4U); + PP_EXPECT(h, decoded.value().faces[1].layer_index == 1U); + PP_EXPECT(h, decoded.value().faces[1].frame_index == 0U); + PP_EXPECT(h, decoded.value().faces[1].face_index == 2U); + PP_EXPECT(h, decoded.value().faces[1].descriptor.x0 == 6U); +} + void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) { const auto png_payload = transparent_png_1x1(); const pp::assets::PpiDirtyFacePayloadConfig duplicate_faces[] { { + .layer_index = 0, + .frame_index = 0, .face_index = 0, .x = 0, .y = 0, @@ -533,6 +588,8 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) .png_rgba8 = std::span(png_payload.data(), png_payload.size()), }, { + .layer_index = 0, + .frame_index = 0, .face_index = 0, .x = 1, .y = 1, @@ -541,6 +598,30 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) .png_rgba8 = std::span(png_payload.data(), png_payload.size()), }, }; + const pp::assets::PpiDirtyFacePayloadConfig bad_layer_faces[] { + { + .layer_index = 1, + .frame_index = 0, + .face_index = 0, + .x = 0, + .y = 0, + .width = 1, + .height = 1, + .png_rgba8 = std::span(png_payload.data(), png_payload.size()), + }, + }; + const pp::assets::PpiDirtyFacePayloadConfig bad_frame_faces[] { + { + .layer_index = 0, + .frame_index = 1, + .face_index = 0, + .x = 0, + .y = 0, + .width = 1, + .height = 1, + .png_rgba8 = std::span(png_payload.data(), png_payload.size()), + }, + }; const auto no_size = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = 0, .height = 128, @@ -625,6 +706,26 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) .frame_duration_ms = 100, .dirty_faces = std::span(duplicate_faces, 2), }); + const auto bad_layer_dirty_face = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { + .width = 128, + .height = 128, + .layer_name = "Ink", + .layer_metadata = {}, + .layer_count = 1, + .frame_count = 1, + .frame_duration_ms = 100, + .dirty_faces = std::span(bad_layer_faces, 1), + }); + const auto bad_frame_dirty_face = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { + .width = 128, + .height = 128, + .layer_name = "Ink", + .layer_metadata = {}, + .layer_count = 1, + .frame_count = 1, + .frame_duration_ms = 100, + .dirty_faces = std::span(bad_frame_faces, 1), + }); PP_EXPECT(h, !no_size.ok()); PP_EXPECT(h, no_size.status().code == StatusCode::invalid_argument); @@ -642,6 +743,10 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) PP_EXPECT(h, bad_blend_mode.status().code == StatusCode::out_of_range); PP_EXPECT(h, !duplicate_dirty_face.ok()); PP_EXPECT(h, duplicate_dirty_face.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_layer_dirty_face.ok()); + PP_EXPECT(h, bad_layer_dirty_face.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_frame_dirty_face.ok()); + PP_EXPECT(h, bad_frame_dirty_face.status().code == StatusCode::out_of_range); } } @@ -665,6 +770,7 @@ int main() harness.run("creates_minimal_project_with_multiple_layers", creates_minimal_project_with_multiple_layers); harness.run("creates_minimal_project_with_multiple_frames", creates_minimal_project_with_multiple_frames); harness.run("creates_minimal_project_with_dirty_face_payload", creates_minimal_project_with_dirty_face_payload); + harness.run("creates_minimal_project_with_targeted_dirty_face_payloads", creates_minimal_project_with_targeted_dirty_face_payloads); harness.run("rejects_invalid_minimal_project_writer_inputs", rejects_invalid_minimal_project_writer_inputs); return harness.finish(); } diff --git a/tests/cmake/pano_cli_save_project_payload_roundtrip.cmake b/tests/cmake/pano_cli_save_project_payload_roundtrip.cmake index dfd00e6..0e945c0 100644 --- a/tests/cmake/pano_cli_save_project_payload_roundtrip.cmake +++ b/tests/cmake/pano_cli_save_project_payload_roundtrip.cmake @@ -20,6 +20,8 @@ execute_process( --frames 2 --frame-duration-ms 321 --include-test-face-payload + --payload-layer 1 + --payload-frame 1 RESULT_VARIABLE save_result OUTPUT_VARIABLE save_output ERROR_VARIABLE save_error) @@ -32,7 +34,14 @@ string(FIND "${save_output}" "\"command\":\"save-project\"" save_command_index) string(FIND "${save_output}" "\"layers\":2" save_layers_index) string(FIND "${save_output}" "\"frames\":2" save_frames_index) string(FIND "${save_output}" "\"facePayloads\":1" save_payload_index) -if(save_command_index LESS 0 OR save_layers_index LESS 0 OR save_frames_index LESS 0 OR save_payload_index LESS 0) +string(FIND "${save_output}" "\"payloadLayer\":1" save_payload_layer_index) +string(FIND "${save_output}" "\"payloadFrame\":1" save_payload_frame_index) +if(save_command_index LESS 0 + OR save_layers_index LESS 0 + OR save_frames_index LESS 0 + OR save_payload_index LESS 0 + OR save_payload_layer_index LESS 0 + OR save_payload_frame_index LESS 0) message(FATAL_ERROR "save-project payload output did not contain expected summary: ${save_output}") endif() @@ -40,6 +49,21 @@ if(NOT EXISTS "${OUTPUT_PATH}") message(FATAL_ERROR "save-project did not create ${OUTPUT_PATH}") endif() +execute_process( + COMMAND "${PANO_CLI}" inspect-project --path "${OUTPUT_PATH}" + RESULT_VARIABLE inspect_result + OUTPUT_VARIABLE inspect_output + ERROR_VARIABLE inspect_error) + +if(NOT inspect_result EQUAL 0) + message(FATAL_ERROR "inspect-project failed after payload save-project: ${inspect_output}${inspect_error}") +endif() + +string(REGEX MATCH "\"index\":1,\"storedOrder\":1,\"name\":\"Payload 2\".*\"index\":1,\"durationMs\":321,\"dirtyFaces\":\\[\\{\"face\":0" inspect_targeted_payload "${inspect_output}") +if(NOT inspect_targeted_payload) + message(FATAL_ERROR "inspect-project output did not show the payload on layer 1 frame 1: ${inspect_output}") +endif() + execute_process( COMMAND "${PANO_CLI}" load-project --path "${OUTPUT_PATH}" RESULT_VARIABLE load_result diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 1aa236d..4e24159 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -46,6 +46,8 @@ struct SaveProjectArgs { std::uint32_t frames = 1; std::uint32_t frame_duration_ms = 100; bool include_test_face_payload = false; + std::uint32_t payload_layer = 0; + std::uint32_t payload_frame = 0; }; struct InspectImageArgs { @@ -206,7 +208,7 @@ void print_help() << " load-project --path FILE\n" << " parse-layout --path FILE\n" << " record-render [--width N] [--height N]\n" - << " save-project --path FILE --width N --height N [--layer-name NAME] [--layer-opacity N] [--blend-mode N] [--alpha-locked] [--hidden] [--layers N] [--frames N] [--frame-duration-ms N] [--include-test-face-payload]\n" + << " save-project --path FILE --width N --height N [--layer-name NAME] [--layer-opacity N] [--blend-mode N] [--alpha-locked] [--hidden] [--layers N] [--frames N] [--frame-duration-ms N] [--include-test-face-payload] [--payload-layer N] [--payload-frame N]\n" << " simulate-document-edits [--width N] [--height N]\n" << " simulate-document-history [--width N] [--height N] [--history N]\n" << " simulate-image-import [--width N] [--height N]\n" @@ -341,7 +343,8 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec } else if (key == "--hidden") { args.visible = false; } else if (key == "--width" || key == "--height" || key == "--layers" || key == "--frames" - || key == "--blend-mode" || key == "--frame-duration-ms") { + || key == "--blend-mode" || key == "--frame-duration-ms" || key == "--payload-layer" + || key == "--payload-frame") { if (i + 1 >= argc) { return pp::foundation::Status::invalid_argument("missing value for option"); } @@ -361,6 +364,10 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec args.frames = value.value(); } else if (key == "--blend-mode") { args.blend_mode = value.value(); + } else if (key == "--payload-layer") { + args.payload_layer = value.value(); + } else if (key == "--payload-frame") { + args.payload_frame = value.value(); } else { args.frame_duration_ms = value.value(); } @@ -403,6 +410,19 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec return pp::foundation::Status::invalid_argument("frame duration must be greater than zero"); } + if (args.include_test_face_payload && args.payload_layer >= args.layers) { + return pp::foundation::Status::out_of_range("payload layer must be inside the generated layer list"); + } + + if (args.include_test_face_payload && args.payload_frame >= args.frames) { + return pp::foundation::Status::out_of_range("payload frame must be inside the generated frame list"); + } + + if (!args.include_test_face_payload && (args.payload_layer != 0 || args.payload_frame != 0)) { + return pp::foundation::Status::invalid_argument( + "payload layer/frame options require --include-test-face-payload"); + } + return pp::foundation::Status::success(); } @@ -418,6 +438,8 @@ int save_project(int argc, char** argv) const auto test_payload = transparent_png_1x1_bytes(); const pp::assets::PpiDirtyFacePayloadConfig dirty_faces[] { { + .layer_index = args.payload_layer, + .frame_index = args.payload_frame, .face_index = 0, .x = 0, .y = 0, @@ -476,6 +498,8 @@ int save_project(int argc, char** argv) << ",\"visible\":" << (args.visible ? "true" : "false") << ",\"frameDurationMs\":" << args.frame_duration_ms << ",\"facePayloads\":" << (args.include_test_face_payload ? 1 : 0) + << ",\"payloadLayer\":" << args.payload_layer + << ",\"payloadFrame\":" << args.payload_frame << "}}\n"; return 0; }