diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 2430228..ed90370 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -113,6 +113,8 @@ Known local toolchain state: `pano_cli_load_project_metadata_smoke`. - `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 + operations and is covered by `pano_cli_simulate_document_edits_smoke`. - `pano_cli simulate-document-history` exercises pure document history apply/undo/redo behavior and is covered by `pano_cli_simulate_document_history_smoke`. @@ -252,6 +254,9 @@ Known local toolchain state: - `pano_cli simulate-document-history` exposes `pp_document::DocumentHistory` apply/undo/redo state through JSON automation and is covered by `pano_cli_simulate_document_history_smoke`. +- `pano_cli simulate-document-edits` exposes `pp_document` layer metadata, + frame order, active-index, and tiny face-payload state through JSON + automation and is covered by `pano_cli_simulate_document_edits_smoke`. - `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/roadmap.md b/docs/modernization/roadmap.md index ed75cba..3eb57c7 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -339,8 +339,10 @@ asset-level decode coverage, and counts, durations, and decoded face-pixel payload attachment when PPI image payloads are present. `pano_cli create-document` can create simple animation documents with explicit -frame count/duration. `pano_cli simulate-document-history` exercises the pure -`pp_document::DocumentHistory` apply/undo/redo path and emits JSON state +frame count/duration. `pano_cli simulate-document-edits` exercises pure +layer metadata, frame reordering, active-index preservation, and tiny +face-payload attachment. `pano_cli simulate-document-history` exercises the +pure `pp_document::DocumentHistory` apply/undo/redo path and emits JSON state summaries. `pano_cli simulate-stroke` exercises the pure stroke sampler for scripted-stroke automation. `pano_cli simulate-stroke-script` loads stroke script fixtures, parses them through `pp_paint`, and samples every @@ -687,6 +689,9 @@ Results: - `pano_cli_create_document_smoke` passed. - `pano_cli_create_animation_document_smoke` passed and reports animation duration JSON. +- `pano_cli_simulate_document_edits_smoke` passed and reports pure + `pp_document` layer metadata, frame order, active indices, and face-payload + state as JSON. - `pano_cli_simulate_document_history_smoke` passed and reports real `pp_document::DocumentHistory` apply/undo/redo state as JSON. - `pano_cli_inspect_image_rejects_unsupported` passed as an expected failure @@ -753,6 +758,9 @@ Results: - `pano_cli simulate-document-history` exercises pure document history apply/undo/redo behavior and emits JSON layer/frame/history state for agent automation. +- `pano_cli simulate-document-edits` exercises pure document layer/frame edit + operations and emits JSON metadata, frame order, and face-payload state for + agent automation. - 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/tests/CMakeLists.txt b/tests/CMakeLists.txt index 415c9a9..2b907af 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -285,6 +285,12 @@ if(TARGET pano_cli) LABELS "renderer;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"commands\":7.*\"drawCommands\":1") + add_test(NAME pano_cli_simulate_document_edits_smoke + COMMAND pano_cli simulate-document-edits --width 128 --height 64) + set_tests_properties(pano_cli_simulate_document_edits_smoke PROPERTIES + LABELS "document;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"simulate-document-edits\".*\"document\":\\{\"width\":128,\"height\":64,\"layers\":2,\"frames\":3,\"activeLayer\":0,\"activeFrame\":0,\"animationDurationMs\":683,\"facePayloads\":1\\}.*\"activeLayer\":\\{\"name\":\"Ink\",\"visible\":false,\"alphaLocked\":true,\"opacity\":0.625,\"blendMode\":\"overlay\",\"frames\":3\\}.*\"frames\":\\[250,100,333\\].*\"activeLayerFrameDurations\":\\[250,100,333\\]") + add_test(NAME pano_cli_simulate_document_history_smoke COMMAND pano_cli simulate-document-history --width 64 --height 32 --history 4) set_tests_properties(pano_cli_simulate_document_history_smoke PROPERTIES diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index d3521bf..4db52dd 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -53,6 +53,11 @@ struct SimulateStrokeScriptArgs { std::string path; }; +struct SimulateDocumentEditsArgs { + std::uint32_t width = 128; + std::uint32_t height = 64; +}; + struct SimulateDocumentHistoryArgs { std::uint32_t width = 64; std::uint32_t height = 32; @@ -122,6 +127,7 @@ void print_help() << " load-project --path FILE\n" << " parse-layout --path FILE\n" << " record-render [--width N] [--height N]\n" + << " simulate-document-edits [--width N] [--height N]\n" << " simulate-document-history [--width N] [--height N] [--history N]\n" << " simulate-stroke --x1 N --y1 N --x2 N --y2 N [--spacing N]\n" << " simulate-stroke-script --path FILE\n" @@ -638,6 +644,182 @@ int simulate_stroke_script(int argc, char** argv) return 0; } +pp::foundation::Status parse_simulate_document_edits_args( + int argc, + char** argv, + SimulateDocumentEditsArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--width" || key == "--height") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + + const auto value = pp::foundation::parse_u32(argv[++i]); + if (!value) { + return value.status(); + } + + if (key == "--width") { + args.width = value.value(); + } else { + args.height = value.value(); + } + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + if (args.width == 0 || args.height == 0) { + return pp::foundation::Status::invalid_argument("width and height must be greater than zero"); + } + + return pp::foundation::Status::success(); +} + +int simulate_document_edits(int argc, char** argv) +{ + SimulateDocumentEditsArgs args; + const auto status = parse_simulate_document_edits_args(argc, argv, args); + if (!status.ok()) { + print_error("simulate-document-edits", status.message); + return 2; + } + + const auto document_result = pp::document::CanvasDocument::create( + pp::document::DocumentConfig { + .width = args.width, + .height = args.height, + .layer_count = 1, + }); + if (!document_result) { + print_error("simulate-document-edits", document_result.status().message); + return 2; + } + + auto document = document_result.value(); + auto edit_status = document.set_frame_duration(0, 100); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + + const auto layer_result = document.add_layer("Paint"); + if (!layer_result) { + print_error("simulate-document-edits", layer_result.status().message); + return 2; + } + + const std::size_t paint_layer = layer_result.value(); + edit_status = document.rename_layer(paint_layer, "Ink"); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + edit_status = document.set_layer_visible(paint_layer, false); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + edit_status = document.set_layer_alpha_locked(paint_layer, true); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + edit_status = document.set_layer_opacity(paint_layer, 0.625F); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + edit_status = document.set_layer_blend_mode(paint_layer, pp::paint::BlendMode::overlay); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + edit_status = document.set_active_layer(paint_layer); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + + const auto duplicated_frame = document.duplicate_frame(0); + if (!duplicated_frame) { + print_error("simulate-document-edits", duplicated_frame.status().message); + return 2; + } + edit_status = document.set_frame_duration(duplicated_frame.value(), 333); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + + const auto added_frame = document.add_frame(250); + if (!added_frame) { + print_error("simulate-document-edits", added_frame.status().message); + return 2; + } + edit_status = document.move_frame(added_frame.value(), 0); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + edit_status = document.move_layer(paint_layer, 0); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + + edit_status = document.set_layer_frame_face_pixels( + 0, + 0, + pp::document::LayerFacePixels { + .face_index = 2, + .x = 1, + .y = 2, + .width = 1, + .height = 1, + .rgba8 = { 255, 0, 0, 255 }, + }); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + + const auto& active_layer = document.layers()[document.active_layer_index()]; + std::cout << "{\"ok\":true,\"command\":\"simulate-document-edits\"" + << ",\"document\":{\"width\":" << document.width() + << ",\"height\":" << document.height() + << ",\"layers\":" << document.layers().size() + << ",\"frames\":" << document.frames().size() + << ",\"activeLayer\":" << document.active_layer_index() + << ",\"activeFrame\":" << document.active_frame_index() + << ",\"animationDurationMs\":" << document.animation_duration_ms() + << ",\"facePayloads\":" << document.face_pixel_payload_count() + << "},\"activeLayer\":{\"name\":\"" << json_escape(active_layer.name) + << "\",\"visible\":" << (active_layer.visible ? "true" : "false") + << ",\"alphaLocked\":" << (active_layer.alpha_locked ? "true" : "false") + << ",\"opacity\":" << active_layer.opacity + << ",\"blendMode\":\"" << pp::paint::blend_mode_name(active_layer.blend_mode) + << "\",\"frames\":" << active_layer.frames.size() + << "},\"frames\":["; + for (std::size_t frame_index = 0; frame_index < document.frames().size(); ++frame_index) { + if (frame_index != 0U) { + std::cout << ","; + } + std::cout << document.frames()[frame_index].duration_ms; + } + std::cout << "],\"activeLayerFrameDurations\":["; + for (std::size_t frame_index = 0; frame_index < active_layer.frames.size(); ++frame_index) { + if (frame_index != 0U) { + std::cout << ","; + } + std::cout << active_layer.frames[frame_index].duration_ms; + } + std::cout << "]}\n"; + return 0; +} + pp::foundation::Status parse_simulate_document_history_args( int argc, char** argv, @@ -975,6 +1157,10 @@ int main(int argc, char** argv) return simulate_stroke_script(argc, argv); } + if (command == "simulate-document-edits") { + return simulate_document_edits(argc, argv); + } + if (command == "simulate-document-history") { return simulate_document_history(argc, argv); }