#pragma once #include "document/document.h" #include "document/ppi_export.h" #include "foundation/result.h" #include #include #include #include #include #include #include #include #include namespace pp::app { struct DocumentCanvasClearPlan { float r = 0.0F; float g = 0.0F; float b = 0.0F; float a = 0.0F; bool clears_canvas = false; bool records_undo = false; bool marks_unsaved = false; bool no_op = true; }; struct DocumentCanvasFacePayloadInput { std::uint32_t frame_index = 0; std::uint32_t face_index = 0; std::uint32_t x = 0; std::uint32_t y = 0; std::uint32_t width = 0; std::uint32_t height = 0; std::span rgba8; }; struct DocumentCanvasLayerSnapshotInput { std::string_view name; bool visible = true; bool alpha_locked = false; float opacity = 1.0F; int blend_mode = 0; std::span frame_durations_ms; std::size_t pending_face_payloads = 0; std::span captured_face_payloads; }; struct DocumentCanvasSnapshotInput { bool has_canvas = true; std::uint32_t width = 0; std::uint32_t height = 0; std::size_t active_layer_index = 0; std::size_t active_frame_index = 0; std::span layers; }; struct DocumentCanvasSnapshotResult { pp::document::CanvasDocument document; std::size_t layer_count = 0; std::size_t frame_count = 0; std::size_t pending_face_payloads = 0; std::size_t captured_face_payloads = 0; bool metadata_only = false; bool requires_renderer_payload_readback = false; }; struct DocumentCanvasSaveSnapshotReport { std::uint32_t width = 0; std::uint32_t height = 0; std::size_t layer_count = 0; std::size_t frame_count = 0; std::size_t captured_face_payloads = 0; std::size_t pending_face_payloads = 0; bool payload_complete = false; bool can_export_ppi = false; }; enum class DocumentCanvasSaveWriterAction { use_document_ppi_writer, use_legacy_project_save, }; struct DocumentCanvasSaveWriterRoutePlan { DocumentCanvasSaveWriterAction action = DocumentCanvasSaveWriterAction::use_legacy_project_save; bool payload_complete = false; bool can_export_ppi = false; bool uses_document_ppi_writer = false; std::string_view fallback_reason; }; struct DocumentCanvasPpiExportResult { DocumentCanvasSaveSnapshotReport report; std::vector bytes; }; struct DocumentCanvasProjectSaveTargetPlan { std::string target_path; std::string file_name; std::string temporary_path; std::string timelapse_path; }; enum class DocumentCanvasProjectSaveWriteAction { write_direct_to_target, write_temporary_then_swap, }; struct DocumentCanvasProjectSaveWritePlan { DocumentCanvasProjectSaveWriteAction action = DocumentCanvasProjectSaveWriteAction::write_direct_to_target; std::string write_path; std::string target_path; std::string temporary_path; bool target_exists = false; bool uses_temporary = false; bool falls_back_to_direct_on_temporary_open_failure = false; }; struct DocumentCanvasProjectSaveCommitInput { bool used_temporary = false; bool target_remove_attempted = false; bool target_remove_succeeded = false; bool temporary_rename_attempted = false; bool temporary_rename_succeeded = false; }; struct DocumentCanvasProjectSaveCommitPlan { bool saved = false; bool used_temporary = false; bool target_removed = false; bool temporary_renamed = false; bool target_may_be_missing = false; 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; virtual void clear_current_canvas(float r, float g, float b, float a) = 0; }; [[nodiscard]] inline pp::foundation::Status validate_clear_color_channel(float value) noexcept { if (!std::isfinite(value)) { return pp::foundation::Status::invalid_argument("clear color channel must be finite"); } if (value < 0.0F || value > 1.0F) { return pp::foundation::Status::out_of_range("clear color channel must be within 0..1"); } return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Result plan_document_canvas_snapshot( DocumentCanvasSnapshotInput input) { if (!input.has_canvas) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document canvas snapshot requires a canvas")); } if (input.layers.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document canvas snapshot requires at least one layer")); } std::size_t frame_count = 1U; std::size_t pending_face_payloads = 0U; std::size_t captured_face_payloads = 0U; for (const auto& layer : input.layers) { frame_count = std::max(frame_count, layer.frame_durations_ms.size()); pending_face_payloads += layer.pending_face_payloads; captured_face_payloads += layer.captured_face_payloads.size(); } if (input.active_layer_index >= input.layers.size()) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("active canvas layer is outside the document snapshot")); } if (input.active_frame_index >= frame_count) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("active canvas frame is outside the document snapshot")); } std::vector root_frames; root_frames.reserve(frame_count); for (std::size_t frame_index = 0; frame_index < frame_count; ++frame_index) { std::uint32_t duration_ms = 100U; for (const auto& layer : input.layers) { if (frame_index < layer.frame_durations_ms.size()) { duration_ms = layer.frame_durations_ms[frame_index]; break; } } root_frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms }); } std::vector layer_names; std::vector> layer_frames; std::vector layer_configs; layer_names.reserve(input.layers.size()); layer_frames.reserve(input.layers.size()); layer_configs.reserve(input.layers.size()); for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) { const auto& layer = input.layers[layer_index]; if (layer.name.empty()) { layer_names.push_back("Layer " + std::to_string(layer_index + 1U)); } else { layer_names.push_back(std::string(layer.name)); } layer_frames.push_back({}); auto& frames = layer_frames.back(); frames.reserve(layer.frame_durations_ms.empty() ? root_frames.size() : layer.frame_durations_ms.size()); if (layer.frame_durations_ms.empty()) { frames = root_frames; } else { for (const auto duration_ms : layer.frame_durations_ms) { frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms }); } } layer_configs.push_back(pp::document::DocumentLayerConfig { .name = layer_names.back(), .visible = layer.visible, .alpha_locked = layer.alpha_locked, .opacity = layer.opacity, .blend_mode = static_cast(layer.blend_mode), .frames = std::span(frames), }); } auto document = pp::document::CanvasDocument::create_from_snapshot(pp::document::DocumentSnapshotConfig { .width = input.width, .height = input.height, .layers = std::span(layer_configs), .frames = std::span(root_frames), }); if (!document) { return pp::foundation::Result::failure(document.status()); } for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) { for (const auto& payload : input.layers[layer_index].captured_face_payloads) { pp::document::LayerFacePixels pixels { .face_index = payload.face_index, .x = payload.x, .y = payload.y, .width = payload.width, .height = payload.height, .rgba8 = std::vector(payload.rgba8.begin(), payload.rgba8.end()), }; const auto payload_status = document.value().set_layer_frame_face_pixels( layer_index, payload.frame_index, std::move(pixels)); if (!payload_status.ok()) { return pp::foundation::Result::failure(payload_status); } } } auto active_status = document.value().set_active_layer(input.active_layer_index); if (!active_status.ok()) { return pp::foundation::Result::failure(active_status); } active_status = document.value().set_active_frame(input.active_frame_index); if (!active_status.ok()) { return pp::foundation::Result::failure(active_status); } return pp::foundation::Result::success(DocumentCanvasSnapshotResult { .document = std::move(document.value()), .layer_count = input.layers.size(), .frame_count = frame_count, .pending_face_payloads = pending_face_payloads, .captured_face_payloads = captured_face_payloads, .metadata_only = captured_face_payloads == 0U, .requires_renderer_payload_readback = pending_face_payloads > captured_face_payloads, }); } [[nodiscard]] inline DocumentCanvasSaveSnapshotReport make_document_canvas_save_snapshot_report( const DocumentCanvasSnapshotResult& snapshot) noexcept { return DocumentCanvasSaveSnapshotReport { .width = snapshot.document.width(), .height = snapshot.document.height(), .layer_count = snapshot.layer_count, .frame_count = snapshot.frame_count, .captured_face_payloads = snapshot.captured_face_payloads, .pending_face_payloads = snapshot.pending_face_payloads, .payload_complete = !snapshot.requires_renderer_payload_readback, .can_export_ppi = !snapshot.requires_renderer_payload_readback, }; } [[nodiscard]] constexpr DocumentCanvasSaveWriterRoutePlan plan_document_canvas_save_writer_route( DocumentCanvasSaveSnapshotReport report) noexcept { DocumentCanvasSaveWriterRoutePlan plan; plan.payload_complete = report.payload_complete; plan.can_export_ppi = report.can_export_ppi; if (!report.payload_complete || !report.can_export_ppi) { plan.fallback_reason = "canvas document snapshot still requires renderer payload readback"; return plan; } plan.action = DocumentCanvasSaveWriterAction::use_document_ppi_writer; plan.uses_document_ppi_writer = true; return plan; } [[nodiscard]] inline pp::foundation::Result export_document_canvas_save_snapshot_to_ppi(const DocumentCanvasSnapshotResult& snapshot) { const auto report = make_document_canvas_save_snapshot_report(snapshot); const auto route = plan_document_canvas_save_writer_route(report); if (!route.uses_document_ppi_writer) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument(route.fallback_reason.data())); } auto bytes = pp::document::export_ppi_project_document(snapshot.document); if (!bytes) { return pp::foundation::Result::failure(bytes.status()); } return pp::foundation::Result::success(DocumentCanvasPpiExportResult { .report = report, .bytes = std::move(bytes.value()), }); } [[nodiscard]] inline pp::foundation::Result plan_document_canvas_project_save_target( std::string_view data_directory, std::string_view target_path) { if (data_directory.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("project save data directory must not be empty")); } if (target_path.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("project save target path must not be empty")); } const auto basename_start = target_path.find_last_of("/\\"); const auto file_name_start = basename_start == std::string_view::npos ? 0U : basename_start + 1U; auto file_name = target_path.substr(file_name_start); if (file_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("project save target file name must not be empty")); } constexpr std::string_view ppi_extension = ".ppi"; if (file_name.size() > ppi_extension.size() && file_name.substr(file_name.size() - ppi_extension.size()) == ppi_extension) { file_name.remove_suffix(ppi_extension.size()); } DocumentCanvasProjectSaveTargetPlan plan; plan.target_path = std::string(target_path); plan.file_name = std::string(file_name); plan.temporary_path.reserve(data_directory.size() + plan.file_name.size() + 10U); plan.temporary_path += data_directory; plan.temporary_path += "/"; plan.temporary_path += plan.file_name; plan.temporary_path += ".tmp.ppi"; plan.timelapse_path.reserve(data_directory.size() + plan.file_name.size() + 6U); plan.timelapse_path += data_directory; plan.timelapse_path += "/"; plan.timelapse_path += plan.file_name; plan.timelapse_path += ".pptl"; return pp::foundation::Result::success(std::move(plan)); } [[nodiscard]] inline pp::foundation::Result plan_document_canvas_project_save_write( const DocumentCanvasProjectSaveTargetPlan& target, bool target_exists) { if (target.target_path.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("project save write target path must not be empty")); } DocumentCanvasProjectSaveWritePlan plan; plan.target_exists = target_exists; plan.target_path = target.target_path; plan.temporary_path = target.temporary_path; if (!target_exists) { plan.write_path = target.target_path; return pp::foundation::Result::success(std::move(plan)); } if (target.temporary_path.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("project save temporary path must not be empty")); } plan.action = DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap; plan.write_path = target.temporary_path; plan.uses_temporary = true; plan.falls_back_to_direct_on_temporary_open_failure = true; return pp::foundation::Result::success(std::move(plan)); } [[nodiscard]] constexpr DocumentCanvasProjectSaveCommitPlan plan_document_canvas_project_save_commit( DocumentCanvasProjectSaveCommitInput input) noexcept { DocumentCanvasProjectSaveCommitPlan plan; plan.used_temporary = input.used_temporary; if (!input.used_temporary) { plan.saved = true; plan.log_message = "project saved to target"; return plan; } if (!input.target_remove_attempted || !input.target_remove_succeeded) { plan.log_message = "could not remove target project before temporary swap"; return plan; } plan.target_removed = true; if (!input.temporary_rename_attempted || !input.temporary_rename_succeeded) { plan.target_may_be_missing = true; plan.log_message = "temporary project not swapped after original removal"; return plan; } plan.saved = true; plan.temporary_renamed = true; plan.log_message = "temporary project swapped successfully"; 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, float g = 0.0F, float b = 0.0F, float a = 0.0F) noexcept { const float channels[] { r, g, b, a }; for (const float channel : channels) { const auto status = validate_clear_color_channel(channel); if (!status.ok()) { return pp::foundation::Result::failure(status); } } DocumentCanvasClearPlan plan; plan.r = r; plan.g = g; plan.b = b; plan.a = a; plan.clears_canvas = has_canvas; plan.records_undo = has_canvas; plan.marks_unsaved = has_canvas; plan.no_op = !has_canvas; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Status execute_document_canvas_clear_plan( const DocumentCanvasClearPlan& plan, DocumentCanvasClearServices& services) { const float channels[] { plan.r, plan.g, plan.b, plan.a }; for (const float channel : channels) { const auto status = validate_clear_color_channel(channel); if (!status.ok()) { return status; } } if (plan.no_op || !plan.clears_canvas) { return pp::foundation::Status::success(); } services.clear_current_canvas(plan.r, plan.g, plan.b, plan.a); return pp::foundation::Status::success(); } } // namespace pp::app