diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index c15f630..1123948 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -120,9 +120,10 @@ Known local toolchain state: frame counts, durations, and decoded face-pixel payloads when present; the metadata-only minimal fixture remains covered by `pano_cli_load_project_metadata_smoke`. -- `pano_cli save-project` writes generated multi-layer, multi-frame PPI files through - `pp_assets` and is covered by `pano_cli_save_project_roundtrip_smoke` and - `pano_cli_save_project_payload_roundtrip_smoke`, which reload generated +- `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` + and `pano_cli_save_project_payload_roundtrip_smoke`, which reload generated metadata-only and 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`. @@ -283,10 +284,10 @@ Known local toolchain state: - `pano_cli export-image` exposes deterministic RGBA8 PNG writing through JSON 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 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. +- `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. - `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 f764186..0173f4c 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, and renderer-free RGBA8 face payload storage, but it is not yet wired to legacy `Canvas`, selection masks, 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` | 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, 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, metadata, corrupt payloads, dirty-face payload saving, broader legacy layer metadata, 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 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 1d36a18..c7da7f8 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -343,7 +343,8 @@ asset-level decode coverage, and `pano_cli load-project` creates a `pp_document` projection with per-layer frame counts, durations, and decoded face-pixel payload attachment when PPI image payloads are present. -`pano_cli save-project` writes generated multi-layer, multi-frame PPI files through the +`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`. `pano_cli create-document` can create simple animation documents with explicit @@ -791,10 +792,10 @@ Results: - `pano_cli export-image` writes deterministic RGBA8 PNGs through `pp_assets` 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 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. +- `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. - 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 8fac4d7..627c9e3 100644 --- a/src/assets/ppi_header.cpp +++ b/src/assets/ppi_header.cpp @@ -667,6 +667,16 @@ pp::foundation::Result> create_minimal_ppi_project(PpiMin pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit")); } + if (config.layer_metadata.opacity < 0.0F || config.layer_metadata.opacity > 1.0F) { + return pp::foundation::Result>::failure( + pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range")); + } + + if (config.layer_metadata.blend_mode > 4U) { + return pp::foundation::Result>::failure( + pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range")); + } + if (config.layer_count == 0 || config.layer_count > max_ppi_layer_count) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI layer count is outside the configured range")); @@ -769,12 +779,12 @@ pp::foundation::Result> create_minimal_ppi_project(PpiMin for (std::uint32_t layer = 0; layer < config.layer_count; ++layer) { const auto name = generated_layer_name(config.layer_name, layer, config.layer_count); append_u32(bytes, layer); - append_f32(bytes, 1.0F); + append_f32(bytes, config.layer_metadata.opacity); append_u32(bytes, static_cast(name.size())); append_ascii(bytes, name); - append_u32(bytes, 0); - bytes.push_back(std::byte { 0 }); - bytes.push_back(std::byte { 1 }); + append_u32(bytes, config.layer_metadata.blend_mode); + bytes.push_back(config.layer_metadata.alpha_locked ? std::byte { 1 } : std::byte { 0 }); + bytes.push_back(config.layer_metadata.visible ? std::byte { 1 } : std::byte { 0 }); append_u32(bytes, config.frame_count); for (std::uint32_t frame = 0; frame < config.frame_count; ++frame) { append_u32(bytes, config.frame_duration_ms); diff --git a/src/assets/ppi_header.h b/src/assets/ppi_header.h index 80cd6fd..884e3c3 100644 --- a/src/assets/ppi_header.h +++ b/src/assets/ppi_header.h @@ -127,10 +127,18 @@ struct PpiDirtyFacePayloadConfig { std::span png_rgba8; }; +struct PpiLayerMetadataConfig { + float opacity = 1.0F; + std::uint32_t blend_mode = 0; + bool alpha_locked = false; + bool visible = true; +}; + struct PpiMinimalProjectConfig { std::uint32_t width = 0; std::uint32_t height = 0; std::string layer_name; + PpiLayerMetadataConfig layer_metadata; std::uint32_t layer_count = 1; std::uint32_t frame_count = 1; std::uint32_t frame_duration_ms = 100; diff --git a/tests/assets/ppi_header_tests.cpp b/tests/assets/ppi_header_tests.cpp index 29e2168..647024b 100644 --- a/tests/assets/ppi_header_tests.cpp +++ b/tests/assets/ppi_header_tests.cpp @@ -392,6 +392,7 @@ void creates_minimal_project_for_roundtrip_load(pp::tests::Harness& h) .width = 256, .height = 128, .layer_name = "Roundtrip", + .layer_metadata = {}, .layer_count = 1, .frame_count = 1, .frame_duration_ms = 333, @@ -419,6 +420,12 @@ void creates_minimal_project_with_multiple_layers(pp::tests::Harness& h) .width = 256, .height = 128, .layer_name = "Layer", + .layer_metadata = pp::assets::PpiLayerMetadataConfig { + .opacity = 0.625F, + .blend_mode = 4, + .alpha_locked = true, + .visible = false, + }, .layer_count = 2, .frame_count = 2, .frame_duration_ms = 111, @@ -436,6 +443,14 @@ void creates_minimal_project_with_multiple_layers(pp::tests::Harness& h) PP_EXPECT(h, index.value().body.layers[1].stored_order == 1U); PP_EXPECT(h, index.value().body.layers[0].name == "Layer 1"); PP_EXPECT(h, index.value().body.layers[1].name == "Layer 2"); + PP_EXPECT(h, index.value().body.layers[0].opacity == 0.625F); + PP_EXPECT(h, index.value().body.layers[1].opacity == 0.625F); + PP_EXPECT(h, index.value().body.layers[0].blend_mode == 4U); + PP_EXPECT(h, index.value().body.layers[1].blend_mode == 4U); + PP_EXPECT(h, index.value().body.layers[0].alpha_locked); + PP_EXPECT(h, index.value().body.layers[1].alpha_locked); + PP_EXPECT(h, !index.value().body.layers[0].visible); + PP_EXPECT(h, !index.value().body.layers[1].visible); PP_EXPECT(h, index.value().body.layers[0].frames.size() == 2U); PP_EXPECT(h, index.value().body.layers[1].frames.size() == 2U); PP_EXPECT(h, index.value().body.layers[0].frames[1].duration_ms == 111U); @@ -448,6 +463,7 @@ void creates_minimal_project_with_multiple_frames(pp::tests::Harness& h) .width = 256, .height = 128, .layer_name = "Frames", + .layer_metadata = {}, .layer_count = 1, .frame_count = 3, .frame_duration_ms = 111, @@ -482,6 +498,7 @@ void creates_minimal_project_with_dirty_face_payload(pp::tests::Harness& h) .width = 256, .height = 128, .layer_name = "Payload", + .layer_metadata = {}, .layer_count = 1, .frame_count = 1, .frame_duration_ms = 333, @@ -528,6 +545,7 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) .width = 0, .height = 128, .layer_name = "Ink", + .layer_metadata = {}, .layer_count = 1, .frame_count = 1, .frame_duration_ms = 100, @@ -537,6 +555,7 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) .width = 128, .height = 128, .layer_name = "", + .layer_metadata = {}, .layer_count = 1, .frame_count = 1, .frame_duration_ms = 100, @@ -546,6 +565,7 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) .width = 128, .height = 128, .layer_name = "Ink", + .layer_metadata = {}, .layer_count = 1, .frame_count = 1, .frame_duration_ms = 0, @@ -555,6 +575,7 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) .width = 128, .height = 128, .layer_name = "Ink", + .layer_metadata = {}, .layer_count = 1, .frame_count = 0, .frame_duration_ms = 100, @@ -564,15 +585,41 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) .width = 128, .height = 128, .layer_name = "Ink", + .layer_metadata = {}, .layer_count = 0, .frame_count = 1, .frame_duration_ms = 100, .dirty_faces = {}, }); + const auto bad_opacity = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { + .width = 128, + .height = 128, + .layer_name = "Ink", + .layer_metadata = pp::assets::PpiLayerMetadataConfig { + .opacity = 1.25F, + }, + .layer_count = 1, + .frame_count = 1, + .frame_duration_ms = 100, + .dirty_faces = {}, + }); + const auto bad_blend_mode = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { + .width = 128, + .height = 128, + .layer_name = "Ink", + .layer_metadata = pp::assets::PpiLayerMetadataConfig { + .blend_mode = 99, + }, + .layer_count = 1, + .frame_count = 1, + .frame_duration_ms = 100, + .dirty_faces = {}, + }); const auto duplicate_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, @@ -589,6 +636,10 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) PP_EXPECT(h, no_frames.status().code == StatusCode::out_of_range); PP_EXPECT(h, !no_layers.ok()); PP_EXPECT(h, no_layers.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_opacity.ok()); + PP_EXPECT(h, bad_opacity.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_blend_mode.ok()); + 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); } diff --git a/tests/cmake/pano_cli_save_project_roundtrip.cmake b/tests/cmake/pano_cli_save_project_roundtrip.cmake index 75cb761..d2faf0e 100644 --- a/tests/cmake/pano_cli_save_project_roundtrip.cmake +++ b/tests/cmake/pano_cli_save_project_roundtrip.cmake @@ -16,6 +16,10 @@ execute_process( --width 96 --height 48 --layer-name Roundtrip + --layer-opacity 0.625 + --blend-mode 4 + --alpha-locked + --hidden --layers 2 --frames 3 --frame-duration-ms 321 @@ -31,7 +35,18 @@ string(FIND "${save_output}" "\"command\":\"save-project\"" save_command_index) string(FIND "${save_output}" "\"bytes\":65830" save_bytes_index) string(FIND "${save_output}" "\"layers\":2" save_layers_index) string(FIND "${save_output}" "\"frames\":3" save_frames_index) -if(save_command_index LESS 0 OR save_bytes_index LESS 0 OR save_layers_index LESS 0 OR save_frames_index LESS 0) +string(FIND "${save_output}" "\"layerOpacity\":0.625" save_opacity_index) +string(FIND "${save_output}" "\"blendMode\":4" save_blend_index) +string(FIND "${save_output}" "\"alphaLocked\":true" save_alpha_index) +string(FIND "${save_output}" "\"visible\":false" save_visible_index) +if(save_command_index LESS 0 + OR save_bytes_index LESS 0 + OR save_layers_index LESS 0 + OR save_frames_index LESS 0 + OR save_opacity_index LESS 0 + OR save_blend_index LESS 0 + OR save_alpha_index LESS 0 + OR save_visible_index LESS 0) message(FATAL_ERROR "save-project output did not contain expected summary: ${save_output}") endif() @@ -58,6 +73,10 @@ string(FIND "${load_output}" "\"animationDurationMs\":963" load_duration_index) string(FIND "${load_output}" "\"layerNames\":[\"Roundtrip 1\",\"Roundtrip 2\"]" load_layer_index) string(FIND "${load_output}" "\"layerFrameCounts\":[3,3]" load_layer_frames_index) string(FIND "${load_output}" "\"layerDurationsMs\":[963,963]" load_layer_durations_index) +string(FIND "${load_output}" "\"layerOpacities\":[0.625,0.625]" load_layer_opacity_index) +string(FIND "${load_output}" "\"layerBlendModes\":[\"overlay\",\"overlay\"]" load_layer_blend_index) +string(FIND "${load_output}" "\"layerAlphaLocked\":[true,true]" load_layer_alpha_index) +string(FIND "${load_output}" "\"layerVisible\":[false,false]" load_layer_visible_index) if(load_command_index LESS 0 OR load_width_index LESS 0 OR load_height_index LESS 0 @@ -66,6 +85,10 @@ if(load_command_index LESS 0 OR load_duration_index LESS 0 OR load_layer_index LESS 0 OR load_layer_frames_index LESS 0 - OR load_layer_durations_index LESS 0) + OR load_layer_durations_index LESS 0 + OR load_layer_opacity_index LESS 0 + OR load_layer_blend_index LESS 0 + OR load_layer_alpha_index LESS 0 + OR load_layer_visible_index LESS 0) message(FATAL_ERROR "load-project output did not contain expected round-trip summary: ${load_output}") endif() diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index f80486a..c7d2836 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -12,10 +12,12 @@ #include "ui_core/layout_xml.h" #include +#include #include #include #include #include +#include #include #include #include @@ -36,6 +38,10 @@ struct SaveProjectArgs { std::uint32_t width = 0; std::uint32_t height = 0; std::string layer_name = "Ink"; + float layer_opacity = 1.0F; + std::uint32_t blend_mode = 0; + bool alpha_locked = false; + bool visible = true; std::uint32_t layers = 1; std::uint32_t frames = 1; std::uint32_t frame_duration_ms = 100; @@ -174,6 +180,20 @@ std::string json_escape(std::string_view value) return escaped; } +pp::foundation::Result parse_float_arg(std::string_view text) +{ + float value = 0.0F; + const auto* begin = text.data(); + const auto* end = begin + text.size(); + const auto [ptr, ec] = std::from_chars(begin, end, value); + if (ec != std::errc {} || ptr != end) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("invalid floating-point value")); + } + + return pp::foundation::Result::success(value); +} + void print_help() { std::cout @@ -186,7 +206,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] [--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]\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" @@ -306,8 +326,22 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec } else { args.layer_name = argv[++i]; } + } else if (key == "--layer-opacity") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + + const auto value = parse_float_arg(argv[++i]); + if (!value) { + return value.status(); + } + args.layer_opacity = value.value(); + } else if (key == "--alpha-locked") { + args.alpha_locked = true; + } else if (key == "--hidden") { + args.visible = false; } else if (key == "--width" || key == "--height" || key == "--layers" || key == "--frames" - || key == "--frame-duration-ms") { + || key == "--blend-mode" || key == "--frame-duration-ms") { if (i + 1 >= argc) { return pp::foundation::Status::invalid_argument("missing value for option"); } @@ -325,6 +359,8 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec args.layers = value.value(); } else if (key == "--frames") { args.frames = value.value(); + } else if (key == "--blend-mode") { + args.blend_mode = value.value(); } else { args.frame_duration_ms = value.value(); } @@ -347,6 +383,14 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec return pp::foundation::Status::invalid_argument("layer name must not be empty"); } + if (args.layer_opacity < 0.0F || args.layer_opacity > 1.0F) { + return pp::foundation::Status::out_of_range("layer opacity must be in the range [0, 1]"); + } + + if (args.blend_mode > 4U) { + return pp::foundation::Status::out_of_range("blend mode must be in the range [0, 4]"); + } + if (args.layers == 0) { return pp::foundation::Status::invalid_argument("layer count must be greater than zero"); } @@ -386,6 +430,12 @@ int save_project(int argc, char** argv) .width = args.width, .height = args.height, .layer_name = args.layer_name, + .layer_metadata = pp::assets::PpiLayerMetadataConfig { + .opacity = args.layer_opacity, + .blend_mode = args.blend_mode, + .alpha_locked = args.alpha_locked, + .visible = args.visible, + }, .layer_count = args.layers, .frame_count = args.frames, .frame_duration_ms = args.frame_duration_ms, @@ -420,6 +470,10 @@ int save_project(int argc, char** argv) << ",\"layers\":" << args.layers << ",\"frames\":" << args.frames << ",\"layerName\":\"" << json_escape(args.layer_name) << "\"" + << ",\"layerOpacity\":" << args.layer_opacity + << ",\"blendMode\":" << args.blend_mode + << ",\"alphaLocked\":" << (args.alpha_locked ? "true" : "false") + << ",\"visible\":" << (args.visible ? "true" : "false") << ",\"frameDurationMs\":" << args.frame_duration_ms << ",\"facePayloads\":" << (args.include_test_face_payload ? 1 : 0) << "}}\n"; @@ -907,6 +961,34 @@ int load_project(int argc, char** argv) const auto duration = document.layer_animation_duration_ms(layer_index); std::cout << (duration ? duration.value() : 0U); } + std::cout << "],\"layerOpacities\":["; + for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) { + if (layer_index != 0U) { + std::cout << ","; + } + std::cout << document.layers()[layer_index].opacity; + } + std::cout << "],\"layerBlendModes\":["; + for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) { + if (layer_index != 0U) { + std::cout << ","; + } + std::cout << "\"" << pp::paint::blend_mode_name(document.layers()[layer_index].blend_mode) << "\""; + } + std::cout << "],\"layerAlphaLocked\":["; + for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) { + if (layer_index != 0U) { + std::cout << ","; + } + std::cout << (document.layers()[layer_index].alpha_locked ? "true" : "false"); + } + std::cout << "],\"layerVisible\":["; + for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) { + if (layer_index != 0U) { + std::cout << ","; + } + std::cout << (document.layers()[layer_index].visible ? "true" : "false"); + } std::cout << "]}}\n"; return 0; }