diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 36e5a13..0a3e63f 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -307,7 +307,10 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p the app-core commit plan for direct saves, successful temporary swaps, target-remove failures, and rename-after-remove failures. It is covered by forward-slash, Windows backslash, existing-target, remove-failure, - rename-failure, and invalid-path smokes. + rename-failure, and invalid-path smokes. The same command now reports the + app-core post-commit side-effect plan for clean/new-document metadata + updates, timelapse sidecar gating, platform flush, progress cleanup, and + title refresh. - Live equirectangular, layer, animation-frame, and cube-face export adapters now prepare and log the same payload-bearing canvas document snapshot plus shared paint-renderer export-readiness report. @@ -1191,7 +1194,10 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p delegating to retained `Canvas::project_save`. Direct save, successful temporary swap, remove-failure, and rename-after-remove commit outcomes are now classified by the same app-core planner before retained success metadata - mutation. + mutation. Post-commit clean/new-document metadata, timelapse sidecar, + platform flush, progress cleanup, and title-refresh decisions also come from + `pp_app_core`, while retained code still performs the mutations and sidecar + serialization. Retained legacy UI/canvas execution and actual live save serialization remain tracked by `DEBT-0040`, `DEBT-0041`, and `DEBT-0042`; the pure snapshot-to-PPI export handoff is diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 4bad80d..43a6548 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -559,6 +559,15 @@ agent or engineer to remove them without reconstructing context from chat. Actual PPI serialization, filesystem remove/rename execution, progress/threading, timelapse sidecar serialization, and app metadata mutation remain retained. +- 2026-06-06: DEBT-0040/DEBT-0042 were narrowed again. `pp_app_core` now owns + the retained project-save post-commit side-effect policy: successful saves + mark the document clean, commit new-document state, flush platform storage, + optionally write a timelapse sidecar when an encoder exists, and always + dismiss visible progress UI plus refresh the title. Live + `Canvas::project_save_thread` consumes that plan, and + `pano_cli plan-canvas-project-save-target` reports it in JSON. Actual state + mutation, progress UI destruction, platform flush execution, title update, + and timelapse sidecar serialization remain retained. - 2026-06-05: DEBT-0010/DEBT-0013 were narrowed again. `pp_app_core` now exports payload-complete or metadata-only canvas document snapshots through the pure `pp_document` PPI writer and rejects snapshots that still require diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 88f77ee..7d2b30d 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -715,8 +715,11 @@ and preserving the legacy direct-write fallback when the temporary file cannot be opened. The same app-core boundary now also classifies the post-write commit result for direct writes, successful temporary swaps, remove failures, and rename-after-remove failures before retained save metadata mutation -continues. The same automation now feeds payload-complete snapshots through the -shared +continues. Post-save side effects are now planned there too: mark-clean, +new-document commitment, timelapse sidecar gating, platform flush, progress +cleanup, and title refresh are reported before retained execution performs the +actual mutations and sidecar serialization. The same automation now feeds +payload-complete snapshots through the shared `pp_paint_renderer::prepare_document_frame_export_readiness` report, which records renderer-neutral six-face texture upload commands and encodes the active document frame's six composited faces to PNG bytes. This gives CLI @@ -2326,6 +2329,9 @@ Results: The command now reports the app-core commit plan for direct saves, successful temporary swaps, target-remove failures, and rename-after-remove failures, including whether the target may be missing after a failed swap. + It also reports the app-core post-commit side-effect plan, including + clean/new-document metadata updates, timelapse sidecar gating, platform + flush, progress cleanup, and title refresh. - The same payload-complete snapshot automation now uploads the active document frame through `pp_paint_renderer::upload_document_frame_faces` and the `RecordingRenderDevice`, emitting `rendererUpload` JSON with texture, diff --git a/src/app_core/document_canvas.h b/src/app_core/document_canvas.h index 2a1fda8..bd31298 100644 --- a/src/app_core/document_canvas.h +++ b/src/app_core/document_canvas.h @@ -135,6 +135,21 @@ struct DocumentCanvasProjectSaveCommitPlan { std::string_view log_message; }; +struct DocumentCanvasProjectSavePostCommitInput { + bool save_succeeded = false; + bool timelapse_encoder_available = false; + bool progress_ui_visible = false; +}; + +struct DocumentCanvasProjectSavePostCommitPlan { + bool marks_document_clean = false; + bool marks_new_document_committed = false; + bool saves_timelapse_sidecar = false; + bool flushes_platform_storage = false; + bool dismisses_progress_ui = false; + bool updates_title = true; +}; + class DocumentCanvasClearServices { public: virtual ~DocumentCanvasClearServices() = default; @@ -444,6 +459,23 @@ plan_document_canvas_project_save_write( return plan; } +[[nodiscard]] constexpr DocumentCanvasProjectSavePostCommitPlan plan_document_canvas_project_save_post_commit( + DocumentCanvasProjectSavePostCommitInput input) noexcept +{ + DocumentCanvasProjectSavePostCommitPlan plan; + plan.dismisses_progress_ui = input.progress_ui_visible; + + if (!input.save_succeeded) { + return plan; + } + + plan.marks_document_clean = true; + plan.marks_new_document_committed = true; + plan.saves_timelapse_sidecar = input.timelapse_encoder_available; + plan.flushes_platform_storage = true; + return plan; +} + [[nodiscard]] inline pp::foundation::Result plan_document_canvas_clear( bool has_canvas, float r = 0.0F, diff --git a/src/canvas.cpp b/src/canvas.cpp index e77bea2..e45e8d2 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -2577,32 +2577,50 @@ bool Canvas::project_save_thread(std::string file_path, bool show_progress) LOG("project saved to %s", file_path.c_str()); } - if (success) + const auto post_commit_plan = pp::app::plan_document_canvas_project_save_post_commit( + pp::app::DocumentCanvasProjectSavePostCommitInput { + .save_succeeded = success, + .timelapse_encoder_available = Canvas::I->m_encoder != nullptr, + .progress_ui_visible = show_progress, + }); + + if (post_commit_plan.marks_document_clean) { m_unsaved = false; + } + if (post_commit_plan.marks_new_document_committed) + { m_newdoc = false; + } - // save timelapse - if (Canvas::I->m_encoder) - { - BinaryStreamWriter sw; - sw.init(BinaryStream::ByteOrder::LittleEndian); - Serializer::Descriptor info; - info.class_id = "tracks-info"; - info.name = L"Timelapse Tracks"; - info.props["has-track-360"] = std::make_shared(true); - info.props["version"] = std::make_shared(1); - sw << info; - sw << *Canvas::I->m_encoder; - if (!sw.save(lapse_path)) - LOG("cannot save timelase to %s", lapse_path.c_str()); - } + if (post_commit_plan.saves_timelapse_sidecar) + { + BinaryStreamWriter sw; + sw.init(BinaryStream::ByteOrder::LittleEndian); + Serializer::Descriptor info; + info.class_id = "tracks-info"; + info.name = L"Timelapse Tracks"; + info.props["has-track-360"] = std::make_shared(true); + info.props["version"] = std::make_shared(1); + sw << info; + sw << *Canvas::I->m_encoder; + if (!sw.save(lapse_path)) + LOG("cannot save timelase to %s", lapse_path.c_str()); + } + + if (post_commit_plan.flushes_platform_storage) + { App::I->flush_platform_storage(); } - if (show_progress) + if (post_commit_plan.dismisses_progress_ui) + { pb->destroy(); - App::I->title_update(); + } + if (post_commit_plan.updates_title) + { + App::I->title_update(); + } return success; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ddd3c07..598e99b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1530,7 +1530,7 @@ if(TARGET pano_cli) COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path D:/Paint/projects/demo.ppi) set_tests_properties(pano_cli_plan_canvas_project_save_target_smoke PROPERTIES LABELS "app;document;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"dataDirectory\":\"D:/Paint/data\".*\"targetPath\":\"D:/Paint/projects/demo.ppi\".*\"fileName\":\"demo\".*\"temporaryPath\":\"D:/Paint/data/demo.tmp.ppi\".*\"timelapsePath\":\"D:/Paint/data/demo.pptl\".*\"writePlan\":\\{\"action\":\"write-direct-to-target\",\"targetExists\":false,\"usesTemporary\":false,\"writePath\":\"D:/Paint/projects/demo.ppi\",\"fallbackDirectOnTemporaryOpenFailure\":false\\}.*\"commitPlan\":\\{\"saved\":true,\"usedTemporary\":false,\"targetRemoved\":false,\"temporaryRenamed\":false,\"targetMayBeMissing\":false,\"message\":\"project saved to target\"\\}") + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"dataDirectory\":\"D:/Paint/data\".*\"targetPath\":\"D:/Paint/projects/demo.ppi\".*\"fileName\":\"demo\".*\"temporaryPath\":\"D:/Paint/data/demo.tmp.ppi\".*\"timelapsePath\":\"D:/Paint/data/demo.pptl\".*\"writePlan\":\\{\"action\":\"write-direct-to-target\",\"targetExists\":false,\"usesTemporary\":false,\"writePath\":\"D:/Paint/projects/demo.ppi\",\"fallbackDirectOnTemporaryOpenFailure\":false\\}.*\"commitPlan\":\\{\"saved\":true,\"usedTemporary\":false,\"targetRemoved\":false,\"temporaryRenamed\":false,\"targetMayBeMissing\":false,\"message\":\"project saved to target\"\\}.*\"postCommitPlan\":\\{\"marksDocumentClean\":true,\"marksNewDocumentCommitted\":true,\"savesTimelapseSidecar\":false,\"flushesPlatformStorage\":true,\"dismissesProgressUi\":false,\"updatesTitle\":true\\}") add_test(NAME pano_cli_plan_canvas_project_save_target_backslash_smoke COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path "D:\\Paint\\projects\\demo.ppi") @@ -1539,10 +1539,10 @@ if(TARGET pano_cli) PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"fileName\":\"demo\".*\"temporaryPath\":\"D:/Paint/data/demo.tmp.ppi\".*\"timelapsePath\":\"D:/Paint/data/demo.pptl\"") add_test(NAME pano_cli_plan_canvas_project_save_target_existing_smoke - COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path D:/Paint/projects/demo.ppi --target-exists) + COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path D:/Paint/projects/demo.ppi --target-exists --encoder --progress) set_tests_properties(pano_cli_plan_canvas_project_save_target_existing_smoke PROPERTIES LABELS "app;document;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"targetPath\":\"D:/Paint/projects/demo.ppi\".*\"writePlan\":\\{\"action\":\"write-temporary-then-swap\",\"targetExists\":true,\"usesTemporary\":true,\"writePath\":\"D:/Paint/data/demo.tmp.ppi\",\"fallbackDirectOnTemporaryOpenFailure\":true\\}.*\"commitPlan\":\\{\"saved\":true,\"usedTemporary\":true,\"targetRemoved\":true,\"temporaryRenamed\":true,\"targetMayBeMissing\":false,\"message\":\"temporary project swapped successfully\"\\}") + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"targetPath\":\"D:/Paint/projects/demo.ppi\".*\"writePlan\":\\{\"action\":\"write-temporary-then-swap\",\"targetExists\":true,\"usesTemporary\":true,\"writePath\":\"D:/Paint/data/demo.tmp.ppi\",\"fallbackDirectOnTemporaryOpenFailure\":true\\}.*\"commitPlan\":\\{\"saved\":true,\"usedTemporary\":true,\"targetRemoved\":true,\"temporaryRenamed\":true,\"targetMayBeMissing\":false,\"message\":\"temporary project swapped successfully\"\\}.*\"postCommitPlan\":\\{\"marksDocumentClean\":true,\"marksNewDocumentCommitted\":true,\"savesTimelapseSidecar\":true,\"flushesPlatformStorage\":true,\"dismissesProgressUi\":true,\"updatesTitle\":true\\}") add_test(NAME pano_cli_plan_canvas_project_save_target_remove_failure_smoke COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path D:/Paint/projects/demo.ppi --target-exists --remove-fails) @@ -1554,7 +1554,7 @@ if(TARGET pano_cli) COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path D:/Paint/projects/demo.ppi --target-exists --rename-fails) set_tests_properties(pano_cli_plan_canvas_project_save_target_rename_failure_smoke PROPERTIES LABELS "app;document;integration;desktop-fast;fuzz" - PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"commitPlan\":\\{\"saved\":false,\"usedTemporary\":true,\"targetRemoved\":true,\"temporaryRenamed\":false,\"targetMayBeMissing\":true,\"message\":\"temporary project not swapped after original removal\"\\}") + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"commitPlan\":\\{\"saved\":false,\"usedTemporary\":true,\"targetRemoved\":true,\"temporaryRenamed\":false,\"targetMayBeMissing\":true,\"message\":\"temporary project not swapped after original removal\"\\}.*\"postCommitPlan\":\\{\"marksDocumentClean\":false,\"marksNewDocumentCommitted\":false,\"savesTimelapseSidecar\":false,\"flushesPlatformStorage\":false,\"dismissesProgressUi\":false,\"updatesTitle\":true\\}") add_test(NAME pano_cli_plan_canvas_project_save_target_rejects_empty_path COMMAND pano_cli plan-canvas-project-save-target --path "") diff --git a/tests/app_core/document_canvas_tests.cpp b/tests/app_core/document_canvas_tests.cpp index 4cd9585..f27be9f 100644 --- a/tests/app_core/document_canvas_tests.cpp +++ b/tests/app_core/document_canvas_tests.cpp @@ -425,6 +425,57 @@ void project_save_commit_plan_flags_missing_target_after_rename_failure(pp::test PP_EXPECT(harness, plan.log_message == "temporary project not swapped after original removal"); } +void project_save_post_commit_plan_records_success_side_effects(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_document_canvas_project_save_post_commit( + pp::app::DocumentCanvasProjectSavePostCommitInput { + .save_succeeded = true, + .timelapse_encoder_available = true, + .progress_ui_visible = true, + }); + + PP_EXPECT(harness, plan.marks_document_clean); + PP_EXPECT(harness, plan.marks_new_document_committed); + PP_EXPECT(harness, plan.saves_timelapse_sidecar); + PP_EXPECT(harness, plan.flushes_platform_storage); + PP_EXPECT(harness, plan.dismisses_progress_ui); + PP_EXPECT(harness, plan.updates_title); +} + +void project_save_post_commit_plan_skips_timelapse_without_encoder(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_document_canvas_project_save_post_commit( + pp::app::DocumentCanvasProjectSavePostCommitInput { + .save_succeeded = true, + .timelapse_encoder_available = false, + .progress_ui_visible = false, + }); + + PP_EXPECT(harness, plan.marks_document_clean); + PP_EXPECT(harness, plan.marks_new_document_committed); + PP_EXPECT(harness, !plan.saves_timelapse_sidecar); + PP_EXPECT(harness, plan.flushes_platform_storage); + PP_EXPECT(harness, !plan.dismisses_progress_ui); + PP_EXPECT(harness, plan.updates_title); +} + +void project_save_post_commit_plan_preserves_state_after_failure(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_document_canvas_project_save_post_commit( + pp::app::DocumentCanvasProjectSavePostCommitInput { + .save_succeeded = false, + .timelapse_encoder_available = true, + .progress_ui_visible = true, + }); + + PP_EXPECT(harness, !plan.marks_document_clean); + PP_EXPECT(harness, !plan.marks_new_document_committed); + PP_EXPECT(harness, !plan.saves_timelapse_sidecar); + PP_EXPECT(harness, !plan.flushes_platform_storage); + PP_EXPECT(harness, plan.dismisses_progress_ui); + PP_EXPECT(harness, plan.updates_title); +} + void snapshot_plan_rejects_invalid_canvas_state(pp::tests::Harness& harness) { const std::uint32_t frames[] { 100U }; @@ -616,6 +667,9 @@ int main() harness.run("project save commit plan succeeds for temporary swap", project_save_commit_plan_succeeds_for_temporary_swap); harness.run("project save commit plan fails when target remove fails", project_save_commit_plan_fails_when_target_remove_fails); harness.run("project save commit plan flags missing target after rename failure", project_save_commit_plan_flags_missing_target_after_rename_failure); + harness.run("project save post commit plan records success side effects", project_save_post_commit_plan_records_success_side_effects); + harness.run("project save post commit plan skips timelapse without encoder", project_save_post_commit_plan_skips_timelapse_without_encoder); + harness.run("project save post commit plan preserves state after failure", project_save_post_commit_plan_preserves_state_after_failure); harness.run("snapshot plan rejects invalid canvas state", snapshot_plan_rejects_invalid_canvas_state); harness.run("clear plan records legacy canvas effects", clear_plan_records_legacy_canvas_effects); harness.run("clear plan noops without canvas", clear_plan_noops_without_canvas); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 3967d4b..8b7bb18 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -416,6 +416,8 @@ struct PlanCanvasProjectSaveTargetArgs { bool target_exists = false; bool remove_fails = false; bool rename_fails = false; + bool encoder_available = false; + bool progress_visible = false; }; struct PlanCanvasDocumentSnapshotArgs { @@ -2562,7 +2564,7 @@ void print_help() << " plan-canvas-view-density [--density N] [--bad-float]\n" << " plan-canvas-view-cursor-mode [--mode N]\n" << " plan-canvas-cursor [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--visibility never|small-brush|not-painting|always] [--brush-size N] [--no-brush] [--drawing] [--alt] [--resizing] [--picking] [--bad-size]\n" - << " plan-canvas-project-save-target [--data-dir DIR] [--path FILE] [--target-exists] [--remove-fails] [--rename-fails]\n" + << " plan-canvas-project-save-target [--data-dir DIR] [--path FILE] [--target-exists] [--remove-fails] [--rename-fails] [--encoder] [--progress]\n" << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" << " plan-history-operation --kind undo|redo|clear [--undo-count N] [--redo-count N] [--memory-bytes N]\n" << " plan-main-toolbar --command open|save|undo|redo|clear-history|clear-canvas|message-box|settings [--undo-count N] [--redo-count N] [--memory-bytes N] [--no-canvas]\n" @@ -6097,6 +6099,10 @@ pp::foundation::Status parse_plan_canvas_project_save_target_args( args.remove_fails = true; } else if (key == "--rename-fails") { args.rename_fails = true; + } else if (key == "--encoder") { + args.encoder_available = true; + } else if (key == "--progress") { + args.progress_visible = true; } else { return pp::foundation::Status::invalid_argument("unknown option"); } @@ -6144,6 +6150,12 @@ int plan_canvas_project_save_target(int argc, char** argv) .temporary_rename_attempted = temporary_rename_attempted, .temporary_rename_succeeded = temporary_rename_succeeded, }); + const auto post_commit_plan = pp::app::plan_document_canvas_project_save_post_commit( + pp::app::DocumentCanvasProjectSavePostCommitInput { + .save_succeeded = commit_plan.saved, + .timelapse_encoder_available = args.encoder_available, + .progress_ui_visible = args.progress_visible, + }); std::cout << "{\"ok\":true,\"command\":\"plan-canvas-project-save-target\"" << ",\"dataDirectory\":\"" << json_escape(args.data_directory) << "\",\"targetPath\":\"" << json_escape(value.target_path) @@ -6163,7 +6175,18 @@ int plan_canvas_project_save_target(int argc, char** argv) << ",\"temporaryRenamed\":" << json_bool(commit_plan.temporary_renamed) << ",\"targetMayBeMissing\":" << json_bool(commit_plan.target_may_be_missing) << ",\"message\":\"" << json_escape(std::string(commit_plan.log_message)) - << "\"}" + << "\"},\"postCommitPlan\":{\"marksDocumentClean\":" + << json_bool(post_commit_plan.marks_document_clean) + << ",\"marksNewDocumentCommitted\":" + << json_bool(post_commit_plan.marks_new_document_committed) + << ",\"savesTimelapseSidecar\":" + << json_bool(post_commit_plan.saves_timelapse_sidecar) + << ",\"flushesPlatformStorage\":" + << json_bool(post_commit_plan.flushes_platform_storage) + << ",\"dismissesProgressUi\":" + << json_bool(post_commit_plan.dismisses_progress_ui) + << ",\"updatesTitle\":" << json_bool(post_commit_plan.updates_title) + << "}" << "}\n"; return 0; }