#pragma once #include "app_core/app_dialog.h" #include "app_core/document_canvas.h" #include "document/document.h" #include "foundation/result.h" #include #include #include #include #include #include namespace pp::app { struct DocumentExportFileTarget { std::string path; std::string suggested_name; }; struct DocumentExportCollectionTarget { std::string directory; std::string stem_path; }; struct DocumentExportStemTarget { std::string stem_path; }; struct DocumentCubeFaceExportFileTarget { std::string face_name; std::string path; }; struct DocumentCubeFaceExportTarget { std::array faces; std::size_t face_count = 0; }; struct DocumentDepthExportTarget { std::string image_path; std::string depth_path; }; struct DocumentCubeFaceExportPayload { std::span bytes; }; struct DocumentDepthExportPayload { std::span image_bytes; std::span depth_bytes; }; struct DocumentExportFilePayload { std::span bytes; }; struct DocumentExportCollectionPngPayload { std::string path_suffix; std::span bytes; }; struct DocumentExportSuggestedName { std::string name; }; enum class DocumentExportCollectionDestination { work_directory_collection, picked_directory_stem, }; enum class DocumentExportStartDecision { start_now, show_license_disabled, unavailable_no_canvas, }; enum class DocumentExportMenuKind { jpeg, png, layers, cube_faces, depth, animation_frames, animation_mp4, timelapse, }; enum class DocumentExportMenuAction { show_jpeg_dialog, show_png_dialog, show_layers_dialog, show_cube_faces_dialog, show_depth_dialog, show_animation_frames_dialog, show_animation_mp4_dialog, show_timelapse_dialog, show_license_disabled, unavailable_no_canvas, }; enum class DocumentExportCollectionKind { layers, animation_frames, }; enum class DocumentVideoExportKind { animation_mp4, timelapse, }; enum class DocumentExportSuccessKind { equirectangular, layers, animation_frames, depth, cube_faces, animation_mp4, timelapse, }; enum class DocumentExportSuccessDestination { suppressed, photos, pictures_panopainter, files_panopainter, work_directory, path, generic_success, }; enum class DocumentExportExecutionKind { equirectangular_file, layers_collection, layers_stem, animation_frames_collection, animation_frames_stem, depth, cube_faces, animation_mp4, timelapse, }; enum class DocumentExportSnapshotRouteAction { use_document_snapshot_writer, use_legacy_export, }; struct DocumentExportMenuPlan { DocumentExportMenuKind kind = DocumentExportMenuKind::jpeg; DocumentExportMenuAction action = DocumentExportMenuAction::show_jpeg_dialog; bool opens_dialog = true; }; struct DocumentExportCollectionTargetPlan { DocumentExportCollectionKind kind = DocumentExportCollectionKind::layers; DocumentExportCollectionDestination destination = DocumentExportCollectionDestination::picked_directory_stem; std::string_view suffix; }; struct DocumentExportSuccessDialogPlan { DocumentExportSuccessKind kind = DocumentExportSuccessKind::equirectangular; DocumentExportSuccessDestination destination = DocumentExportSuccessDestination::suppressed; AppMessageDialogPlan dialog; bool show_dialog = false; }; struct DocumentExportSnapshotRoutePlan { DocumentExportExecutionKind kind = DocumentExportExecutionKind::equirectangular_file; DocumentExportSnapshotRouteAction action = DocumentExportSnapshotRouteAction::use_legacy_export; bool payload_complete = false; bool target_supported = false; bool platform_supported = false; bool uses_document_snapshot_writer = false; std::string_view fallback_reason; }; class DocumentExportMenuServices { public: virtual ~DocumentExportMenuServices() = default; virtual void show_jpeg_dialog() = 0; virtual void show_png_dialog() = 0; virtual void show_layers_dialog() = 0; virtual void show_cube_faces_dialog() = 0; virtual void show_depth_dialog() = 0; virtual void show_animation_frames_dialog() = 0; virtual void show_animation_mp4_dialog() = 0; virtual void show_timelapse_dialog() = 0; virtual void show_license_disabled() = 0; }; class DocumentExportServices { public: virtual ~DocumentExportServices() = default; virtual bool create_directory(std::string_view directory) = 0; virtual void export_equirectangular(const DocumentExportFileTarget& target) = 0; virtual void export_layers_to_stem(const DocumentExportStemTarget& target) = 0; virtual void export_layers_to_collection(const DocumentExportCollectionTarget& target) = 0; virtual void export_animation_frames_to_stem(const DocumentExportStemTarget& target) = 0; virtual void export_animation_frames_to_collection(const DocumentExportCollectionTarget& target) = 0; virtual void export_depth(std::string_view document_name) = 0; virtual void export_cube_faces(std::string_view document_name) = 0; }; class DocumentVideoExportServices { public: virtual ~DocumentVideoExportServices() = default; virtual void export_animation_mp4(std::string_view path) = 0; virtual void export_timelapse_mp4(std::string_view path) = 0; virtual void show_animation_export_success(std::string_view path) = 0; virtual void show_timelapse_export_success(std::string_view path) = 0; }; class DocumentCubeFaceExportWriteServices { public: virtual ~DocumentCubeFaceExportWriteServices() = default; virtual pp::foundation::Status write_binary_file( std::string_view path, std::span bytes) = 0; virtual void publish_exported_image(std::string_view path) = 0; }; class DocumentDepthExportWriteServices { public: virtual ~DocumentDepthExportWriteServices() = default; virtual pp::foundation::Status write_binary_file( std::string_view path, std::span bytes) = 0; virtual void publish_exported_image(std::string_view path) = 0; }; class DocumentExportFileWriteServices { public: virtual ~DocumentExportFileWriteServices() = default; virtual pp::foundation::Status write_binary_file( std::string_view path, std::span bytes) = 0; virtual void publish_exported_image(std::string_view path) = 0; }; class DocumentExportCollectionWriteServices { public: virtual ~DocumentExportCollectionWriteServices() = default; virtual pp::foundation::Status write_binary_file( std::string_view path, std::span bytes) = 0; virtual void publish_exported_image(std::string_view path) = 0; }; [[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start( bool requires_license, bool license_valid, bool has_canvas) noexcept { if (requires_license && !license_valid) { return DocumentExportStartDecision::show_license_disabled; } return has_canvas ? DocumentExportStartDecision::start_now : DocumentExportStartDecision::unavailable_no_canvas; } [[nodiscard]] constexpr bool document_export_menu_requires_license( DocumentExportMenuKind kind) noexcept { switch (kind) { case DocumentExportMenuKind::animation_mp4: case DocumentExportMenuKind::timelapse: return true; case DocumentExportMenuKind::jpeg: case DocumentExportMenuKind::png: case DocumentExportMenuKind::layers: case DocumentExportMenuKind::cube_faces: case DocumentExportMenuKind::depth: case DocumentExportMenuKind::animation_frames: return false; } return false; } [[nodiscard]] constexpr DocumentExportMenuAction document_export_menu_dialog_action( DocumentExportMenuKind kind) noexcept { switch (kind) { case DocumentExportMenuKind::jpeg: return DocumentExportMenuAction::show_jpeg_dialog; case DocumentExportMenuKind::png: return DocumentExportMenuAction::show_png_dialog; case DocumentExportMenuKind::layers: return DocumentExportMenuAction::show_layers_dialog; case DocumentExportMenuKind::cube_faces: return DocumentExportMenuAction::show_cube_faces_dialog; case DocumentExportMenuKind::depth: return DocumentExportMenuAction::show_depth_dialog; case DocumentExportMenuKind::animation_frames: return DocumentExportMenuAction::show_animation_frames_dialog; case DocumentExportMenuKind::animation_mp4: return DocumentExportMenuAction::show_animation_mp4_dialog; case DocumentExportMenuKind::timelapse: return DocumentExportMenuAction::show_timelapse_dialog; } return DocumentExportMenuAction::show_jpeg_dialog; } [[nodiscard]] constexpr DocumentExportSuccessDestination document_export_equirectangular_platform_destination() noexcept { #if defined(__IOS__) return DocumentExportSuccessDestination::photos; #elif defined(__OSX__) return DocumentExportSuccessDestination::pictures_panopainter; #elif defined(_WIN32) return DocumentExportSuccessDestination::work_directory; #else return DocumentExportSuccessDestination::suppressed; #endif } [[nodiscard]] constexpr DocumentExportSuccessDestination document_export_media_platform_destination() noexcept { #if defined(__IOS__) return DocumentExportSuccessDestination::files_panopainter; #elif defined(__OSX__) return DocumentExportSuccessDestination::pictures_panopainter; #elif defined(_WIN32) return DocumentExportSuccessDestination::work_directory; #else return DocumentExportSuccessDestination::suppressed; #endif } [[nodiscard]] constexpr DocumentExportMenuPlan plan_document_export_menu_action( DocumentExportMenuKind kind, bool has_canvas, bool license_valid) noexcept { DocumentExportMenuPlan plan; plan.kind = kind; plan.action = document_export_menu_dialog_action(kind); const auto start = plan_document_export_start( document_export_menu_requires_license(kind), license_valid, has_canvas); if (start == DocumentExportStartDecision::show_license_disabled) { plan.action = DocumentExportMenuAction::show_license_disabled; plan.opens_dialog = false; } else if (start == DocumentExportStartDecision::unavailable_no_canvas) { plan.action = DocumentExportMenuAction::unavailable_no_canvas; plan.opens_dialog = false; } return plan; } [[nodiscard]] constexpr std::string_view document_export_collection_suffix( DocumentExportCollectionKind kind) noexcept { switch (kind) { case DocumentExportCollectionKind::layers: return "_layers"; case DocumentExportCollectionKind::animation_frames: return "_frames"; } return {}; } [[nodiscard]] constexpr DocumentExportSuccessKind document_export_collection_success_kind( DocumentExportCollectionKind kind) noexcept { switch (kind) { case DocumentExportCollectionKind::layers: return DocumentExportSuccessKind::layers; case DocumentExportCollectionKind::animation_frames: return DocumentExportSuccessKind::animation_frames; } return DocumentExportSuccessKind::layers; } [[nodiscard]] constexpr const char* document_export_failure_dialog_title( DocumentExportSuccessKind kind) noexcept { switch (kind) { case DocumentExportSuccessKind::equirectangular: return "Export Equirectangular"; case DocumentExportSuccessKind::layers: case DocumentExportSuccessKind::animation_frames: return "Export Layers"; case DocumentExportSuccessKind::depth: return "Export 3D View + Depth"; case DocumentExportSuccessKind::cube_faces: return "Export Cube Faces"; case DocumentExportSuccessKind::animation_mp4: return "Export Animation"; case DocumentExportSuccessKind::timelapse: return "Export Timelapse"; } return "Export"; } [[nodiscard]] constexpr const char* document_export_execution_log_message( DocumentExportExecutionKind kind) noexcept { switch (kind) { case DocumentExportExecutionKind::equirectangular_file: return "Document export file action failed"; case DocumentExportExecutionKind::layers_collection: return "Document layer collection export failed"; case DocumentExportExecutionKind::layers_stem: return "Document layer stem export failed"; case DocumentExportExecutionKind::animation_frames_collection: return "Document animation frame collection export failed"; case DocumentExportExecutionKind::animation_frames_stem: return "Document animation frame stem export failed"; case DocumentExportExecutionKind::depth: return "Document depth export failed"; case DocumentExportExecutionKind::cube_faces: return "Document cube-face export failed"; case DocumentExportExecutionKind::animation_mp4: return "Document animation export failed"; case DocumentExportExecutionKind::timelapse: return "Document timelapse export failed"; } return "Document export failed"; } [[nodiscard]] constexpr bool ascii_iequals(std::string_view left, std::string_view right) noexcept { if (left.size() != right.size()) { return false; } for (std::size_t i = 0; i < left.size(); ++i) { auto lhs = left[i]; if (lhs >= 'A' && lhs <= 'Z') { lhs = static_cast(lhs - 'A' + 'a'); } auto rhs = right[i]; if (rhs >= 'A' && rhs <= 'Z') { rhs = static_cast(rhs - 'A' + 'a'); } if (lhs != rhs) { return false; } } return true; } [[nodiscard]] constexpr bool document_export_path_has_extension( std::string_view path, std::string_view extension) noexcept { return path.size() >= extension.size() && ascii_iequals(path.substr(path.size() - extension.size()), extension); } [[nodiscard]] constexpr bool document_export_path_is_png_target(std::string_view path) noexcept { return document_export_path_has_extension(path, ".png"); } [[nodiscard]] constexpr bool document_export_path_is_jpeg_target(std::string_view path) noexcept { return document_export_path_has_extension(path, ".jpg") || document_export_path_has_extension(path, ".jpeg"); } [[nodiscard]] constexpr bool document_export_snapshot_target_supported( DocumentExportExecutionKind kind, std::string_view target_path = {}) noexcept { switch (kind) { case DocumentExportExecutionKind::equirectangular_file: return document_export_path_is_png_target(target_path) || document_export_path_is_jpeg_target(target_path); case DocumentExportExecutionKind::layers_collection: case DocumentExportExecutionKind::layers_stem: case DocumentExportExecutionKind::animation_frames_collection: case DocumentExportExecutionKind::animation_frames_stem: case DocumentExportExecutionKind::depth: case DocumentExportExecutionKind::cube_faces: return true; case DocumentExportExecutionKind::animation_mp4: case DocumentExportExecutionKind::timelapse: return false; } return false; } [[nodiscard]] constexpr bool document_export_snapshot_platform_supported() noexcept { #if __WEB__ return false; #else return true; #endif } [[nodiscard]] constexpr DocumentExportSnapshotRoutePlan plan_document_export_snapshot_route( DocumentExportExecutionKind kind, DocumentCanvasSaveSnapshotReport report, bool target_supported, bool platform_supported) noexcept { DocumentExportSnapshotRoutePlan plan; plan.kind = kind; plan.payload_complete = report.payload_complete; plan.target_supported = target_supported; plan.platform_supported = platform_supported; if (!platform_supported) { plan.fallback_reason = "document snapshot export is disabled on this platform"; return plan; } if (!target_supported) { plan.fallback_reason = "document snapshot export does not support this target"; return plan; } if (!report.payload_complete) { plan.fallback_reason = "document snapshot still requires renderer payload readback"; return plan; } plan.action = DocumentExportSnapshotRouteAction::use_document_snapshot_writer; plan.uses_document_snapshot_writer = true; return plan; } [[nodiscard]] constexpr DocumentExportSnapshotRoutePlan plan_document_export_snapshot_route_for_target( DocumentExportExecutionKind kind, DocumentCanvasSaveSnapshotReport report, std::string_view target_path, bool platform_supported) noexcept { return plan_document_export_snapshot_route( kind, report, document_export_snapshot_target_supported(kind, target_path), platform_supported); } [[nodiscard]] constexpr DocumentExportSnapshotRoutePlan plan_document_export_snapshot_route_for_current_platform( DocumentExportExecutionKind kind, DocumentCanvasSaveSnapshotReport report, std::string_view target_path = {}) noexcept { return plan_document_export_snapshot_route_for_target( kind, report, target_path, document_export_snapshot_platform_supported()); } [[nodiscard]] constexpr DocumentExportCollectionTargetPlan plan_document_export_collection_target( DocumentExportCollectionKind kind, bool use_work_directory_collection) noexcept { return { kind, use_work_directory_collection ? DocumentExportCollectionDestination::work_directory_collection : DocumentExportCollectionDestination::picked_directory_stem, document_export_collection_suffix(kind), }; } [[nodiscard]] inline AppMessageDialogPlan plan_document_export_failure_dialog( DocumentExportSuccessKind kind, std::string_view status_message) { return plan_app_message_dialog( document_export_failure_dialog_title(kind), status_message, false); } [[nodiscard]] inline AppMessageDialogPlan plan_document_export_license_disabled_dialog() { return plan_app_message_dialog( "License", "This function is disabled in demo mode.", false); } [[nodiscard]] inline DocumentExportSuccessDialogPlan plan_document_export_success_dialog( DocumentExportSuccessKind kind, DocumentExportSuccessDestination destination, std::string_view detail = {}) { DocumentExportSuccessDialogPlan plan; plan.kind = kind; plan.destination = destination; if (destination == DocumentExportSuccessDestination::suppressed) { return plan; } std::string message; switch (kind) { case DocumentExportSuccessKind::equirectangular: plan.dialog.title = "Export Equirectangular"; switch (destination) { case DocumentExportSuccessDestination::photos: message = "Image exported to Photos"; break; case DocumentExportSuccessDestination::pictures_panopainter: message = "Image exported to Pictures/PanoPainter folder"; break; case DocumentExportSuccessDestination::work_directory: message = "Image exported to "; message += detail; break; case DocumentExportSuccessDestination::suppressed: case DocumentExportSuccessDestination::files_panopainter: case DocumentExportSuccessDestination::path: case DocumentExportSuccessDestination::generic_success: break; } break; case DocumentExportSuccessKind::layers: case DocumentExportSuccessKind::animation_frames: plan.dialog.title = "Export Layers"; if (destination == DocumentExportSuccessDestination::files_panopainter) { message = "Image layers exported to Files/PanoPainter"; } else if (destination == DocumentExportSuccessDestination::path) { message = "Layers exported to: "; message += detail; } break; case DocumentExportSuccessKind::depth: plan.dialog.title = "Export 3D View + Depth"; if (destination == DocumentExportSuccessDestination::files_panopainter) { message = "Image and depth exported to Files/PanoPainter"; } else if (destination == DocumentExportSuccessDestination::pictures_panopainter) { message = "Image and depth exported to Pictures/PanoPainter folder"; } else if (destination == DocumentExportSuccessDestination::work_directory) { message = "Image and depth exported to "; message += detail; } break; case DocumentExportSuccessKind::cube_faces: plan.dialog.title = "Export Cube Faces"; if (destination == DocumentExportSuccessDestination::files_panopainter) { message = "Image and depth exported to Files/PanoPainter"; } else if (destination == DocumentExportSuccessDestination::pictures_panopainter) { message = "Image and depth exported to Pictures/PanoPainter folder"; } else if (destination == DocumentExportSuccessDestination::work_directory) { message = "Image and depth exported to "; message += detail; } break; case DocumentExportSuccessKind::animation_mp4: plan.dialog.title = "Export Animation"; if (destination == DocumentExportSuccessDestination::path) { message = "Animation exported to: "; message += detail; } else if (destination == DocumentExportSuccessDestination::generic_success) { message = "Animation exported successfully."; } break; case DocumentExportSuccessKind::timelapse: plan.dialog.title = "Export Timelapse"; if (destination == DocumentExportSuccessDestination::path) { message = "Timelapse exported to: "; message += detail; } else if (destination == DocumentExportSuccessDestination::generic_success) { message = "Timelapse exported successfully."; } break; } if (!plan.dialog.title.empty() && !message.empty()) { plan.dialog.message = std::move(message); plan.show_dialog = true; } return plan; } [[nodiscard]] inline pp::foundation::Result make_document_export_file_target( std::string_view work_directory, std::string_view document_name, std::string_view extension) { if (document_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document name must not be empty")); } if (extension.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("extension must not be empty")); } DocumentExportFileTarget target; target.suggested_name.reserve(document_name.size() + extension.size()); target.suggested_name += document_name; target.suggested_name += extension; target.path.reserve(work_directory.size() + target.suggested_name.size() + 1); target.path += work_directory; target.path += "/"; target.path += target.suggested_name; return pp::foundation::Result::success(std::move(target)); } [[nodiscard]] inline pp::foundation::Result make_document_export_collection_target( std::string_view work_directory, std::string_view document_name, std::string_view suffix) { if (document_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document name must not be empty")); } DocumentExportCollectionTarget target; target.directory.reserve(work_directory.size() + document_name.size() + suffix.size() + 1); target.directory += work_directory; target.directory += "/"; target.directory += document_name; target.directory += suffix; target.stem_path.reserve(target.directory.size() + document_name.size() + 1); target.stem_path += target.directory; target.stem_path += "/"; target.stem_path += document_name; return pp::foundation::Result::success(std::move(target)); } [[nodiscard]] inline pp::foundation::Result make_document_export_stem_target( std::string_view directory, std::string_view document_name) { if (document_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document name must not be empty")); } DocumentExportStemTarget target; target.stem_path.reserve(directory.size() + document_name.size() + 1); target.stem_path += directory; target.stem_path += "/"; target.stem_path += document_name; return pp::foundation::Result::success(std::move(target)); } [[nodiscard]] constexpr std::array document_cube_face_export_names() noexcept { return { "front", "right", "back", "left", "top", "bottom", }; } [[nodiscard]] inline pp::foundation::Result make_document_cube_face_export_target( std::string_view work_directory, std::string_view document_name) { if (work_directory.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("work directory must not be empty")); } if (document_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document name must not be empty")); } DocumentCubeFaceExportTarget target; const auto face_names = document_cube_face_export_names(); target.face_count = face_names.size(); for (std::size_t face_index = 0; face_index < face_names.size(); ++face_index) { auto& face = target.faces[face_index]; face.face_name = face_names[face_index]; face.path.reserve(work_directory.size() + document_name.size() + face_names[face_index].size() + 6); face.path += work_directory; face.path += "/"; face.path += document_name; face.path += "-"; face.path += face_names[face_index]; face.path += ".png"; } return pp::foundation::Result::success(std::move(target)); } [[nodiscard]] inline pp::foundation::Result make_document_depth_export_target( std::string_view work_directory, std::string_view document_name) { if (work_directory.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("work directory must not be empty")); } if (document_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document name must not be empty")); } DocumentDepthExportTarget target; target.image_path.reserve(work_directory.size() + document_name.size() + 5U); target.image_path += work_directory; target.image_path += "/"; target.image_path += document_name; target.image_path += ".png"; target.depth_path.reserve(work_directory.size() + document_name.size() + 11U); target.depth_path += work_directory; target.depth_path += "/"; target.depth_path += document_name; target.depth_path += "_depth.png"; return pp::foundation::Result::success(std::move(target)); } [[nodiscard]] inline std::string document_export_two_digit_index(std::size_t index) { auto value = std::to_string(index); if (value.size() < 2U) { value.insert(value.begin(), '0'); } return value; } [[nodiscard]] inline std::string make_document_layer_export_path_suffix( std::size_t layer_index, std::string_view layer_name) { std::string suffix; const auto index = document_export_two_digit_index(layer_index); suffix.reserve(10U + index.size() + layer_name.size()); suffix += "-layer"; suffix += index; suffix += "-"; suffix += layer_name; suffix += ".png"; return suffix; } [[nodiscard]] inline std::string make_document_animation_frame_export_path_suffix(std::size_t frame_index) { std::string suffix; const auto index = document_export_two_digit_index(frame_index); suffix.reserve(5U + index.size()); suffix += "-"; suffix += index; suffix += ".png"; return suffix; } [[nodiscard]] inline pp::foundation::Result make_document_export_suggested_name( std::string_view document_name, std::string_view suffix) { if (document_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document name must not be empty")); } DocumentExportSuggestedName target; target.name.reserve(document_name.size() + suffix.size()); target.name += document_name; target.name += suffix; return pp::foundation::Result::success(std::move(target)); } [[nodiscard]] inline pp::foundation::Status execute_document_cube_face_export_write( const DocumentCubeFaceExportTarget& target, std::span face_payloads, DocumentCubeFaceExportWriteServices& services) { if (target.face_count != pp::document::cube_face_count) { return pp::foundation::Status::invalid_argument("cube face export target must contain all cube faces"); } if (face_payloads.size() != target.face_count) { return pp::foundation::Status::invalid_argument("cube face export payload count must match target face count"); } for (std::size_t face_index = 0; face_index < target.face_count; ++face_index) { const auto& face = target.faces[face_index]; if (face.path.empty()) { return pp::foundation::Status::invalid_argument("cube face export path must not be empty"); } if (face_payloads[face_index].bytes.empty()) { return pp::foundation::Status::invalid_argument("cube face export payload must not be empty"); } const auto write_status = services.write_binary_file(face.path, face_payloads[face_index].bytes); if (!write_status.ok()) { return write_status; } services.publish_exported_image(face.path); } return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status execute_document_depth_export_write( const DocumentDepthExportTarget& target, DocumentDepthExportPayload payload, DocumentDepthExportWriteServices& services) { if (target.image_path.empty() || target.depth_path.empty()) { return pp::foundation::Status::invalid_argument("depth export target requires image and depth paths"); } if (payload.image_bytes.empty() || payload.depth_bytes.empty()) { return pp::foundation::Status::invalid_argument("depth export payload requires image and depth bytes"); } const auto image_status = services.write_binary_file(target.image_path, payload.image_bytes); if (!image_status.ok()) { return image_status; } services.publish_exported_image(target.image_path); const auto depth_status = services.write_binary_file(target.depth_path, payload.depth_bytes); if (!depth_status.ok()) { return depth_status; } services.publish_exported_image(target.depth_path); return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status execute_document_export_file_write( const DocumentExportFileTarget& target, DocumentExportFilePayload payload, DocumentExportFileWriteServices& services) { if (target.path.empty()) { return pp::foundation::Status::invalid_argument("export file target requires a path"); } if (payload.bytes.empty()) { return pp::foundation::Status::invalid_argument("export file payload must not be empty"); } const auto write_status = services.write_binary_file(target.path, payload.bytes); if (!write_status.ok()) { return write_status; } services.publish_exported_image(target.path); return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status execute_document_export_collection_write( const DocumentExportCollectionTarget& target, std::span payloads, DocumentExportCollectionWriteServices& services) { if (target.stem_path.empty()) { return pp::foundation::Status::invalid_argument("export collection target requires a stem path"); } if (payloads.empty()) { return pp::foundation::Status::invalid_argument("export collection payloads must not be empty"); } for (const auto& payload : payloads) { if (payload.path_suffix.empty()) { return pp::foundation::Status::invalid_argument("export collection payload suffix must not be empty"); } if (payload.bytes.empty()) { return pp::foundation::Status::invalid_argument("export collection payload must not be empty"); } std::string path; path.reserve(target.stem_path.size() + payload.path_suffix.size()); path += target.stem_path; path += payload.path_suffix; const auto write_status = services.write_binary_file(path, payload.bytes); if (!write_status.ok()) { return write_status; } services.publish_exported_image(path); } return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status execute_document_export_file( const DocumentExportFileTarget& target, DocumentExportServices& services) { if (target.path.empty() || target.suggested_name.empty()) { return pp::foundation::Status::invalid_argument("export file target requires a path and suggested name"); } services.export_equirectangular(target); return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status execute_document_export_stem( DocumentExportCollectionKind kind, const DocumentExportStemTarget& target, DocumentExportServices& services) { if (target.stem_path.empty()) { return pp::foundation::Status::invalid_argument("export stem target requires a stem path"); } switch (kind) { case DocumentExportCollectionKind::layers: services.export_layers_to_stem(target); return pp::foundation::Status::success(); case DocumentExportCollectionKind::animation_frames: services.export_animation_frames_to_stem(target); return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("unknown document export collection kind"); } [[nodiscard]] inline pp::foundation::Status execute_document_export_collection( DocumentExportCollectionKind kind, const DocumentExportCollectionTarget& target, DocumentExportServices& services) { if (target.directory.empty() || target.stem_path.empty()) { return pp::foundation::Status::invalid_argument("export collection target requires a directory and stem path"); } if (!services.create_directory(target.directory)) { return pp::foundation::Status::success(); } switch (kind) { case DocumentExportCollectionKind::layers: services.export_layers_to_collection(target); return pp::foundation::Status::success(); case DocumentExportCollectionKind::animation_frames: services.export_animation_frames_to_collection(target); return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("unknown document export collection kind"); } [[nodiscard]] inline pp::foundation::Status execute_document_export_depth( std::string_view document_name, DocumentExportServices& services) { if (document_name.empty()) { return pp::foundation::Status::invalid_argument("document name must not be empty"); } services.export_depth(document_name); return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status execute_document_export_cube_faces( std::string_view document_name, DocumentExportServices& services) { if (document_name.empty()) { return pp::foundation::Status::invalid_argument("document name must not be empty"); } services.export_cube_faces(document_name); return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status execute_document_video_export( DocumentVideoExportKind kind, std::string_view path, DocumentVideoExportServices& services) { if (path.empty()) { return pp::foundation::Status::invalid_argument("video export path must not be empty"); } switch (kind) { case DocumentVideoExportKind::animation_mp4: services.export_animation_mp4(path); services.show_animation_export_success(path); return pp::foundation::Status::success(); case DocumentVideoExportKind::timelapse: services.export_timelapse_mp4(path); services.show_timelapse_export_success(path); return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("unknown document video export kind"); } [[nodiscard]] inline pp::foundation::Status execute_document_export_menu_plan( const DocumentExportMenuPlan& plan, DocumentExportMenuServices& services) { switch (plan.action) { case DocumentExportMenuAction::show_jpeg_dialog: services.show_jpeg_dialog(); return pp::foundation::Status::success(); case DocumentExportMenuAction::show_png_dialog: services.show_png_dialog(); return pp::foundation::Status::success(); case DocumentExportMenuAction::show_layers_dialog: services.show_layers_dialog(); return pp::foundation::Status::success(); case DocumentExportMenuAction::show_cube_faces_dialog: services.show_cube_faces_dialog(); return pp::foundation::Status::success(); case DocumentExportMenuAction::show_depth_dialog: services.show_depth_dialog(); return pp::foundation::Status::success(); case DocumentExportMenuAction::show_animation_frames_dialog: services.show_animation_frames_dialog(); return pp::foundation::Status::success(); case DocumentExportMenuAction::show_animation_mp4_dialog: services.show_animation_mp4_dialog(); return pp::foundation::Status::success(); case DocumentExportMenuAction::show_timelapse_dialog: services.show_timelapse_dialog(); return pp::foundation::Status::success(); case DocumentExportMenuAction::show_license_disabled: services.show_license_disabled(); return pp::foundation::Status::success(); case DocumentExportMenuAction::unavailable_no_canvas: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("unknown document export menu action"); } }