Export layer collections through paint renderer
This commit is contained in:
@@ -258,7 +258,9 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p
|
|||||||
pure six-face PNG export bytes encoded through `pp_assets`. The compositor
|
pure six-face PNG export bytes encoded through `pp_assets`. The compositor
|
||||||
tests now also cover equirectangular PNG export from that same composited
|
tests now also cover equirectangular PNG export from that same composited
|
||||||
document frame using the shader-equivalent cube sampling policy and
|
document frame using the shader-equivalent cube sampling policy and
|
||||||
`pp_assets` PNG encoding.
|
`pp_assets` PNG encoding, independent layer equirectangular PNG export for
|
||||||
|
layer collections, and merged animation-frame equirectangular PNG export for
|
||||||
|
frame collections.
|
||||||
- `pano_cli simulate-document-export` exposes the same pure document-to-PPI
|
- `pano_cli simulate-document-export` exposes the same pure document-to-PPI
|
||||||
export, asset-level decode, and document reimport path through JSON
|
export, asset-level decode, and document reimport path through JSON
|
||||||
automation and is covered by `pano_cli_simulate_document_export_smoke`.
|
automation and is covered by `pano_cli_simulate_document_export_smoke`.
|
||||||
@@ -289,7 +291,10 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p
|
|||||||
falls back to `Canvas::export_cube_faces` if snapshot capture, PNG generation,
|
falls back to `Canvas::export_cube_faces` if snapshot capture, PNG generation,
|
||||||
or file writing fails. PNG equirectangular export writes a pure
|
or file writing fails. PNG equirectangular export writes a pure
|
||||||
document/paint-renderer equirectangular PNG before falling back to the
|
document/paint-renderer equirectangular PNG before falling back to the
|
||||||
retained writer; JPEG/XMP equirectangular export, layer, animation-frame,
|
retained writer. Payload-complete layer and animation-frame PNG collections
|
||||||
|
write pure document/paint-renderer equirectangular PNG sequences through the
|
||||||
|
app-core collection write/publish executor before retained fallback; JPEG/XMP
|
||||||
|
equirectangular export, Web handoff, incomplete-readback collection cases,
|
||||||
depth, and video export remain on retained writer paths.
|
depth, and video export remain on retained writer paths.
|
||||||
- `pano_cli save-document-project` writes that pure document export to a PPI
|
- `pano_cli save-document-project` writes that pure document export to a PPI
|
||||||
file and is covered by `pano_cli_save_document_project_roundtrip_smoke`,
|
file and is covered by `pano_cli_save_document_project_roundtrip_smoke`,
|
||||||
@@ -1134,8 +1139,11 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p
|
|||||||
app-core planned legacy work-directory face paths through the app-core
|
app-core planned legacy work-directory face paths through the app-core
|
||||||
write/publish executor before falling back to the retained writer, PNG
|
write/publish executor before falling back to the retained writer, PNG
|
||||||
equirectangular export writes the pure document/paint-renderer
|
equirectangular export writes the pure document/paint-renderer
|
||||||
equirectangular PNG before retained fallback, and JPEG/XMP, collection, and
|
equirectangular PNG before retained fallback, payload-complete layer and
|
||||||
depth exports remain on older retained paths. It
|
animation-frame collections write pure document/paint-renderer PNG sequences
|
||||||
|
through the app-core collection write/publish executor, and JPEG/XMP,
|
||||||
|
Web/incomplete-readback collection cases, and depth exports remain on older
|
||||||
|
retained paths. It
|
||||||
also bridges timelapse and animation MP4 export picker-selected paths while
|
also bridges timelapse and animation MP4 export picker-selected paths while
|
||||||
preserving desktop worker-thread timelapse behavior, mobile/Web save
|
preserving desktop worker-thread timelapse behavior, mobile/Web save
|
||||||
callbacks, `App::rec_export`, animation `Canvas::export_anim_mp4`, and
|
callbacks, `App::rec_export`, animation `Canvas::export_anim_mp4`, and
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ and validation command.
|
|||||||
| Capability | Current Area | Target Owner | Required Tests |
|
| Capability | Current Area | Target Owner | Required Tests |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
|
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
|
||||||
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests, live export-adapter document snapshot readiness through the shared paint-renderer export report, pure cube-face PNG writer, pure equirectangular PNG writer, JPEG/XMP parity still retained |
|
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests, live export-adapter document snapshot readiness through the shared paint-renderer export report, pure cube-face PNG writer, pure equirectangular PNG writer, pure layer/frame collection PNG writers, app-core collection write executor, JPEG/XMP parity still retained |
|
||||||
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests, live export-adapter renderer-upload/face-PNG readiness report, pure document-frame equirectangular PNG export and live PNG writer fallback, JPEG/XMP retained until metadata parity is owned |
|
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests, live export-adapter renderer-upload/face-PNG readiness report, pure document-frame equirectangular PNG export and live PNG writer fallback, pure layer/frame equirectangular PNG collection export, JPEG/XMP retained until metadata parity is owned |
|
||||||
| Cube face export | `Canvas` fallback | `pp_paint_renderer`, `pp_app_core` | Pure six-face document frame composite, renderer texture-upload bridge, shared export-readiness report, app-core face filename planning and write/publish service execution, payload-complete canvas-snapshot renderer-upload and face-PNG automation, live document/renderer face-PNG writer with retained Canvas fallback, OpenGL command-plan coverage, six-face golden set |
|
| Cube face export | `Canvas` fallback | `pp_paint_renderer`, `pp_app_core` | Pure six-face document frame composite, renderer texture-upload bridge, shared export-readiness report, app-core face filename planning and write/publish service execution, payload-complete canvas-snapshot renderer-upload and face-PNG automation, live document/renderer face-PNG writer with retained Canvas fallback, OpenGL command-plan coverage, six-face golden set |
|
||||||
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |
|
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -692,9 +692,12 @@ executor using the app-core-planned legacy face filenames when available and
|
|||||||
falls back to retained `Canvas::export_cube_faces` on snapshot/write failure.
|
falls back to retained `Canvas::export_cube_faces` on snapshot/write failure.
|
||||||
PNG equirectangular export now uses the same document/composite payload to
|
PNG equirectangular export now uses the same document/composite payload to
|
||||||
generate an equirectangular PNG through `pp_paint_renderer` before the retained
|
generate an equirectangular PNG through `pp_paint_renderer` before the retained
|
||||||
fallback. JPEG/XMP equirectangular export, layer collections, animation-frame
|
fallback. Payload-complete layer and animation-frame PNG collections now use
|
||||||
collections, depth, and video still delegate to retained `Canvas` writers after
|
pure `pp_paint_renderer` equirectangular PNG generation plus app-core
|
||||||
readiness reporting.
|
collection write/publish execution before retained fallback. JPEG/XMP
|
||||||
|
equirectangular export, Web handoff, depth, video, and incomplete-readback
|
||||||
|
collection cases still delegate to retained `Canvas` writers after readiness
|
||||||
|
reporting.
|
||||||
`pano_cli plan-image-import` exposes app-core planning for File > Import image
|
`pano_cli plan-image-import` exposes app-core planning for File > Import image
|
||||||
route decisions, including wide equirectangular images, legacy vertical cube
|
route decisions, including wide equirectangular images, legacy vertical cube
|
||||||
strips, regular transform-placement images, and invalid image dimensions; live
|
strips, regular transform-placement images, and invalid image dimensions; live
|
||||||
@@ -2536,10 +2539,13 @@ Results:
|
|||||||
write/publish service executor before falling back to retained `Canvas`
|
write/publish service executor before falling back to retained `Canvas`
|
||||||
execution on failure. PNG equirectangular export now writes a
|
execution on failure. PNG equirectangular export now writes a
|
||||||
`pp_paint_renderer` equirectangular PNG from the same composited document
|
`pp_paint_renderer` equirectangular PNG from the same composited document
|
||||||
frame before falling back to retained `Canvas` execution; JPEG/XMP,
|
frame before falling back to retained `Canvas` execution; payload-complete
|
||||||
layer, animation-frame, depth, and video export remain on their prior
|
layer and animation-frame PNG collections now write pure
|
||||||
retained writer paths. Actual broader writer replacement remains tracked
|
`pp_paint_renderer` equirectangular PNG sequences through a tested app-core
|
||||||
under export debt.
|
collection write/publish executor before retained fallback. JPEG/XMP,
|
||||||
|
Web prepared-file handoff, depth, video, and incomplete-readback collection
|
||||||
|
cases remain on their prior retained writer paths. Actual broader writer
|
||||||
|
replacement remains tracked under export debt.
|
||||||
- Snapshot creation now rejects invalid embedded RGBA8 face payloads before
|
- Snapshot creation now rejects invalid embedded RGBA8 face payloads before
|
||||||
document export or history can persist malformed state.
|
document export or history can persist malformed state.
|
||||||
- Package-smoke wrappers validate the Windows CMake app executable/runtime
|
- Package-smoke wrappers validate the Windows CMake app executable/runtime
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ struct DocumentCubeFaceExportPayload {
|
|||||||
std::span<const std::byte> bytes;
|
std::span<const std::byte> bytes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct DocumentExportCollectionPngPayload {
|
||||||
|
std::string path_suffix;
|
||||||
|
std::span<const std::byte> bytes;
|
||||||
|
};
|
||||||
|
|
||||||
struct DocumentExportSuggestedName {
|
struct DocumentExportSuggestedName {
|
||||||
std::string name;
|
std::string name;
|
||||||
};
|
};
|
||||||
@@ -190,6 +195,16 @@ public:
|
|||||||
virtual void publish_exported_image(std::string_view path) = 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<const std::byte> bytes) = 0;
|
||||||
|
virtual void publish_exported_image(std::string_view path) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
|
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
|
||||||
bool requires_license,
|
bool requires_license,
|
||||||
bool license_valid,
|
bool license_valid,
|
||||||
@@ -608,6 +623,41 @@ document_cube_face_export_names() noexcept
|
|||||||
return pp::foundation::Result<DocumentCubeFaceExportTarget>::success(std::move(target));
|
return pp::foundation::Result<DocumentCubeFaceExportTarget>::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<DocumentExportSuggestedName> make_document_export_suggested_name(
|
[[nodiscard]] inline pp::foundation::Result<DocumentExportSuggestedName> make_document_export_suggested_name(
|
||||||
std::string_view document_name,
|
std::string_view document_name,
|
||||||
std::string_view suffix)
|
std::string_view suffix)
|
||||||
@@ -656,6 +706,41 @@ document_cube_face_export_names() noexcept
|
|||||||
return pp::foundation::Status::success();
|
return pp::foundation::Status::success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Status execute_document_export_collection_write(
|
||||||
|
const DocumentExportCollectionTarget& target,
|
||||||
|
std::span<const DocumentExportCollectionPngPayload> 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(
|
[[nodiscard]] inline pp::foundation::Status execute_document_export_file(
|
||||||
const DocumentExportFileTarget& target,
|
const DocumentExportFileTarget& target,
|
||||||
DocumentExportServices& services)
|
DocumentExportServices& services)
|
||||||
|
|||||||
@@ -65,9 +65,11 @@ pp::foundation::Status write_export_binary_file(std::string_view path, std::span
|
|||||||
return pp::foundation::Status::success();
|
return pp::foundation::Status::success();
|
||||||
}
|
}
|
||||||
|
|
||||||
class LegacyCubeFaceExportWriteServices final : public pp::app::DocumentCubeFaceExportWriteServices {
|
class LegacyExportWriteServices final
|
||||||
|
: public pp::app::DocumentCubeFaceExportWriteServices
|
||||||
|
, public pp::app::DocumentExportCollectionWriteServices {
|
||||||
public:
|
public:
|
||||||
explicit LegacyCubeFaceExportWriteServices(App& app) noexcept
|
explicit LegacyExportWriteServices(App& app) noexcept
|
||||||
: app_(app)
|
: app_(app)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -183,10 +185,90 @@ pp::foundation::Status export_cube_faces_from_document_snapshot(
|
|||||||
payloads[face_index].bytes = std::span<const std::byte>(reports.face_pngs.face_pngs[face_index]);
|
payloads[face_index].bytes = std::span<const std::byte>(reports.face_pngs.face_pngs[face_index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
LegacyCubeFaceExportWriteServices services(app);
|
LegacyExportWriteServices services(app);
|
||||||
return pp::app::execute_document_cube_face_export_write(target.value(), payloads, services);
|
return pp::app::execute_document_cube_face_export_write(target.value(), payloads, services);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status export_layers_from_document_snapshot(
|
||||||
|
App& app,
|
||||||
|
const pp::app::DocumentExportCollectionTarget& target,
|
||||||
|
const LegacyDocumentExportSnapshotReports& reports)
|
||||||
|
{
|
||||||
|
const auto report = pp::app::make_document_canvas_save_snapshot_report(reports.snapshot);
|
||||||
|
if (!report.payload_complete) {
|
||||||
|
return pp::foundation::Status::invalid_argument(
|
||||||
|
"document snapshot layer export still requires renderer payload readback");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto exported = pp::paint_renderer::export_document_layers_equirectangular_pngs(
|
||||||
|
pp::paint_renderer::DocumentLayerEquirectangularPngExportRequest {
|
||||||
|
.document = &reports.snapshot.document,
|
||||||
|
.frame_index = reports.snapshot.document.active_frame_index(),
|
||||||
|
.clear_color = {},
|
||||||
|
});
|
||||||
|
if (!exported) {
|
||||||
|
return exported.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto exported_value = std::move(exported.value());
|
||||||
|
LOG(
|
||||||
|
"export-layers document export PNG writer: layers=%zu bytes=%llu activeFrame=%zu",
|
||||||
|
exported_value.layer_count,
|
||||||
|
static_cast<unsigned long long>(exported_value.encoded_bytes),
|
||||||
|
reports.snapshot.document.active_frame_index());
|
||||||
|
|
||||||
|
std::vector<pp::app::DocumentExportCollectionPngPayload> payloads;
|
||||||
|
payloads.reserve(exported_value.layers.size());
|
||||||
|
for (const auto& layer : exported_value.layers) {
|
||||||
|
payloads.push_back(pp::app::DocumentExportCollectionPngPayload {
|
||||||
|
.path_suffix = pp::app::make_document_layer_export_path_suffix(layer.layer_index, layer.layer_name),
|
||||||
|
.bytes = std::span<const std::byte>(layer.png.data(), layer.png.size()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacyExportWriteServices services(app);
|
||||||
|
return pp::app::execute_document_export_collection_write(target, payloads, services);
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status export_animation_frames_from_document_snapshot(
|
||||||
|
App& app,
|
||||||
|
const pp::app::DocumentExportCollectionTarget& target,
|
||||||
|
const LegacyDocumentExportSnapshotReports& reports)
|
||||||
|
{
|
||||||
|
const auto report = pp::app::make_document_canvas_save_snapshot_report(reports.snapshot);
|
||||||
|
if (!report.payload_complete) {
|
||||||
|
return pp::foundation::Status::invalid_argument(
|
||||||
|
"document snapshot animation-frame export still requires renderer payload readback");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto exported = pp::paint_renderer::export_document_animation_frames_equirectangular_pngs(
|
||||||
|
pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {
|
||||||
|
.document = &reports.snapshot.document,
|
||||||
|
.clear_color = {},
|
||||||
|
});
|
||||||
|
if (!exported) {
|
||||||
|
return exported.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto exported_value = std::move(exported.value());
|
||||||
|
LOG(
|
||||||
|
"export-animation-frames document export PNG writer: frames=%zu bytes=%llu",
|
||||||
|
exported_value.frame_count,
|
||||||
|
static_cast<unsigned long long>(exported_value.encoded_bytes));
|
||||||
|
|
||||||
|
std::vector<pp::app::DocumentExportCollectionPngPayload> payloads;
|
||||||
|
payloads.reserve(exported_value.frames.size());
|
||||||
|
for (const auto& frame : exported_value.frames) {
|
||||||
|
payloads.push_back(pp::app::DocumentExportCollectionPngPayload {
|
||||||
|
.path_suffix = pp::app::make_document_animation_frame_export_path_suffix(frame.frame_index),
|
||||||
|
.bytes = std::span<const std::byte>(frame.png.data(), frame.png.size()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacyExportWriteServices services(app);
|
||||||
|
return pp::app::execute_document_export_collection_write(target, payloads, services);
|
||||||
|
}
|
||||||
|
|
||||||
pp::foundation::Status export_equirectangular_png_from_document_snapshot(
|
pp::foundation::Status export_equirectangular_png_from_document_snapshot(
|
||||||
App& app,
|
App& app,
|
||||||
const pp::app::DocumentExportFileTarget& target,
|
const pp::app::DocumentExportFileTarget& target,
|
||||||
@@ -284,7 +366,32 @@ public:
|
|||||||
void export_layers_to_stem(const pp::app::DocumentExportStemTarget& target) override
|
void export_layers_to_stem(const pp::app::DocumentExportStemTarget& target) override
|
||||||
{
|
{
|
||||||
auto* app = &app_;
|
auto* app = &app_;
|
||||||
|
#if !__WEB__
|
||||||
|
const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-layers");
|
||||||
|
if (prepared) {
|
||||||
|
const auto collection_target = pp::app::DocumentExportCollectionTarget {
|
||||||
|
.stem_path = target.stem_path,
|
||||||
|
};
|
||||||
|
const auto exported = export_layers_from_document_snapshot(app_, collection_target, prepared.value());
|
||||||
|
if (exported.ok()) {
|
||||||
|
show_export_success_dialog(
|
||||||
|
app_,
|
||||||
|
pp::app::plan_document_export_success_dialog(
|
||||||
|
pp::app::DocumentExportSuccessKind::layers,
|
||||||
|
pp::app::DocumentExportSuccessDestination::path,
|
||||||
|
target.stem_path));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG("export-layers document export writer retained legacy export after failure: %s", exported.message);
|
||||||
|
} else {
|
||||||
|
LOG(
|
||||||
|
"export-layers document export snapshot bridge retained legacy export after failure: %s",
|
||||||
|
prepared.status().message);
|
||||||
|
}
|
||||||
|
#else
|
||||||
prepare_legacy_document_export_snapshot_or_continue(app_, "export-layers");
|
prepare_legacy_document_export_snapshot_or_continue(app_, "export-layers");
|
||||||
|
#endif
|
||||||
app_.canvas->m_canvas->export_layers(target.stem_path, [app, target] {
|
app_.canvas->m_canvas->export_layers(target.stem_path, [app, target] {
|
||||||
show_export_success_dialog(
|
show_export_success_dialog(
|
||||||
*app,
|
*app,
|
||||||
@@ -298,7 +405,28 @@ public:
|
|||||||
void export_layers_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
|
void export_layers_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
|
||||||
{
|
{
|
||||||
auto* app = &app_;
|
auto* app = &app_;
|
||||||
|
#if !__WEB__
|
||||||
|
const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-layers");
|
||||||
|
if (prepared) {
|
||||||
|
const auto exported = export_layers_from_document_snapshot(app_, target, prepared.value());
|
||||||
|
if (exported.ok()) {
|
||||||
|
show_export_success_dialog(
|
||||||
|
app_,
|
||||||
|
pp::app::plan_document_export_success_dialog(
|
||||||
|
pp::app::DocumentExportSuccessKind::layers,
|
||||||
|
pp::app::DocumentExportSuccessDestination::files_panopainter));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG("export-layers document export writer retained legacy export after failure: %s", exported.message);
|
||||||
|
} else {
|
||||||
|
LOG(
|
||||||
|
"export-layers document export snapshot bridge retained legacy export after failure: %s",
|
||||||
|
prepared.status().message);
|
||||||
|
}
|
||||||
|
#else
|
||||||
prepare_legacy_document_export_snapshot_or_continue(app_, "export-layers");
|
prepare_legacy_document_export_snapshot_or_continue(app_, "export-layers");
|
||||||
|
#endif
|
||||||
app_.canvas->m_canvas->export_layers(target.stem_path, [app] {
|
app_.canvas->m_canvas->export_layers(target.stem_path, [app] {
|
||||||
show_export_success_dialog(
|
show_export_success_dialog(
|
||||||
*app,
|
*app,
|
||||||
@@ -311,7 +439,37 @@ public:
|
|||||||
void export_animation_frames_to_stem(const pp::app::DocumentExportStemTarget& target) override
|
void export_animation_frames_to_stem(const pp::app::DocumentExportStemTarget& target) override
|
||||||
{
|
{
|
||||||
auto* app = &app_;
|
auto* app = &app_;
|
||||||
|
#if !__WEB__
|
||||||
|
const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-animation-frames");
|
||||||
|
if (prepared) {
|
||||||
|
const auto collection_target = pp::app::DocumentExportCollectionTarget {
|
||||||
|
.stem_path = target.stem_path,
|
||||||
|
};
|
||||||
|
const auto exported = export_animation_frames_from_document_snapshot(
|
||||||
|
app_,
|
||||||
|
collection_target,
|
||||||
|
prepared.value());
|
||||||
|
if (exported.ok()) {
|
||||||
|
show_export_success_dialog(
|
||||||
|
app_,
|
||||||
|
pp::app::plan_document_export_success_dialog(
|
||||||
|
pp::app::DocumentExportSuccessKind::animation_frames,
|
||||||
|
pp::app::DocumentExportSuccessDestination::path,
|
||||||
|
target.stem_path));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG(
|
||||||
|
"export-animation-frames document export writer retained legacy export after failure: %s",
|
||||||
|
exported.message);
|
||||||
|
} else {
|
||||||
|
LOG(
|
||||||
|
"export-animation-frames document export snapshot bridge retained legacy export after failure: %s",
|
||||||
|
prepared.status().message);
|
||||||
|
}
|
||||||
|
#else
|
||||||
prepare_legacy_document_export_snapshot_or_continue(app_, "export-animation-frames");
|
prepare_legacy_document_export_snapshot_or_continue(app_, "export-animation-frames");
|
||||||
|
#endif
|
||||||
app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app, target] {
|
app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app, target] {
|
||||||
show_export_success_dialog(
|
show_export_success_dialog(
|
||||||
*app,
|
*app,
|
||||||
@@ -325,7 +483,30 @@ public:
|
|||||||
void export_animation_frames_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
|
void export_animation_frames_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
|
||||||
{
|
{
|
||||||
auto* app = &app_;
|
auto* app = &app_;
|
||||||
|
#if !__WEB__
|
||||||
|
const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-animation-frames");
|
||||||
|
if (prepared) {
|
||||||
|
const auto exported = export_animation_frames_from_document_snapshot(app_, target, prepared.value());
|
||||||
|
if (exported.ok()) {
|
||||||
|
show_export_success_dialog(
|
||||||
|
app_,
|
||||||
|
pp::app::plan_document_export_success_dialog(
|
||||||
|
pp::app::DocumentExportSuccessKind::animation_frames,
|
||||||
|
pp::app::DocumentExportSuccessDestination::files_panopainter));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG(
|
||||||
|
"export-animation-frames document export writer retained legacy export after failure: %s",
|
||||||
|
exported.message);
|
||||||
|
} else {
|
||||||
|
LOG(
|
||||||
|
"export-animation-frames document export snapshot bridge retained legacy export after failure: %s",
|
||||||
|
prepared.status().message);
|
||||||
|
}
|
||||||
|
#else
|
||||||
prepare_legacy_document_export_snapshot_or_continue(app_, "export-animation-frames");
|
prepare_legacy_document_export_snapshot_or_continue(app_, "export-animation-frames");
|
||||||
|
#endif
|
||||||
app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app] {
|
app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app] {
|
||||||
show_export_success_dialog(
|
show_export_success_dialog(
|
||||||
*app,
|
*app,
|
||||||
|
|||||||
@@ -408,6 +408,77 @@ pp::foundation::Result<DocumentFrameCompositeResult> composite_document_frame(
|
|||||||
return pp::foundation::Result<DocumentFrameCompositeResult>::success(std::move(result));
|
return pp::foundation::Result<DocumentFrameCompositeResult>::success(std::move(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<DocumentFrameCompositeResult> composite_document_layer_frame(
|
||||||
|
const pp::document::CanvasDocument& document,
|
||||||
|
std::size_t layer_index,
|
||||||
|
std::size_t frame_index,
|
||||||
|
pp::paint::Rgba clear_color)
|
||||||
|
{
|
||||||
|
if (layer_index >= document.layers().size()) {
|
||||||
|
return pp::foundation::Result<DocumentFrameCompositeResult>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document layer export index is outside the document"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame_index >= document.frames().size()) {
|
||||||
|
return pp::foundation::Result<DocumentFrameCompositeResult>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document layer export frame index is outside the document"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pp::renderer::Extent2D extent {
|
||||||
|
.width = document.width(),
|
||||||
|
.height = document.height(),
|
||||||
|
};
|
||||||
|
const auto pixel_count = expected_pixel_count(extent);
|
||||||
|
if (!pixel_count) {
|
||||||
|
return pp::foundation::Result<DocumentFrameCompositeResult>::failure(pixel_count.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& source_layer = document.layers()[layer_index];
|
||||||
|
auto export_layer = source_layer;
|
||||||
|
export_layer.visible = true;
|
||||||
|
export_layer.opacity = 1.0F;
|
||||||
|
export_layer.blend_mode = pp::paint::BlendMode::normal;
|
||||||
|
|
||||||
|
DocumentFrameCompositeResult result;
|
||||||
|
result.extent = extent;
|
||||||
|
result.visited_layer_count = 1U;
|
||||||
|
|
||||||
|
for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) {
|
||||||
|
DocumentFaceCompositeResult face;
|
||||||
|
face.extent = extent;
|
||||||
|
face.pixels.assign(pixel_count.value(), clear_color);
|
||||||
|
face.visited_layer_count = 1U;
|
||||||
|
|
||||||
|
if (frame_index < source_layer.frames.size()) {
|
||||||
|
bool composited_face = false;
|
||||||
|
const auto& frame = source_layer.frames[frame_index];
|
||||||
|
for (const auto& payload : frame.face_pixels) {
|
||||||
|
if (payload.face_index != face_index) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto status = composite_face_payload(face.pixels, extent, payload, export_layer);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentFrameCompositeResult>::failure(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
composited_face = true;
|
||||||
|
++face.face_payload_count;
|
||||||
|
++result.face_payload_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (composited_face) {
|
||||||
|
face.composited_layer_count = 1U;
|
||||||
|
++result.composited_layer_face_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.faces[face_index] = std::move(face);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<DocumentFrameCompositeResult>::success(std::move(result));
|
||||||
|
}
|
||||||
|
|
||||||
pp::foundation::Result<DocumentFrameUploadResult> upload_document_frame_faces(
|
pp::foundation::Result<DocumentFrameUploadResult> upload_document_frame_faces(
|
||||||
pp::renderer::IRenderDevice& device,
|
pp::renderer::IRenderDevice& device,
|
||||||
DocumentFrameUploadRequest request)
|
DocumentFrameUploadRequest request)
|
||||||
@@ -612,6 +683,91 @@ export_document_frame_equirectangular_png(DocumentFrameCompositeRequest request)
|
|||||||
return export_document_frame_equirectangular_png(composite.value());
|
return export_document_frame_equirectangular_png(composite.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>
|
||||||
|
export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request)
|
||||||
|
{
|
||||||
|
if (request.document == nullptr) {
|
||||||
|
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document layer export request requires a document"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.frame_index >= request.document->frames().size()) {
|
||||||
|
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("document layer export frame index is outside the document"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerEquirectangularPngExportResult result;
|
||||||
|
result.layers.reserve(request.document->layers().size());
|
||||||
|
for (std::size_t layer_index = 0; layer_index < request.document->layers().size(); ++layer_index) {
|
||||||
|
auto composite = composite_document_layer_frame(
|
||||||
|
*request.document,
|
||||||
|
layer_index,
|
||||||
|
request.frame_index,
|
||||||
|
request.clear_color);
|
||||||
|
if (!composite) {
|
||||||
|
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(composite.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto exported = export_document_frame_equirectangular_png(composite.value());
|
||||||
|
if (!exported) {
|
||||||
|
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(exported.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentLayerEquirectangularPng layer;
|
||||||
|
layer.layer_index = layer_index;
|
||||||
|
layer.layer_name = request.document->layers()[layer_index].name;
|
||||||
|
layer.face_extent = exported.value().face_extent;
|
||||||
|
layer.equirectangular_extent = exported.value().equirectangular_extent;
|
||||||
|
layer.encoded_bytes = exported.value().encoded_bytes;
|
||||||
|
layer.face_payload_count = exported.value().face_payload_count;
|
||||||
|
layer.composited_layer_face_count = exported.value().composited_layer_face_count;
|
||||||
|
layer.png = std::move(exported.value().png);
|
||||||
|
result.encoded_bytes += layer.encoded_bytes;
|
||||||
|
result.layers.push_back(std::move(layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.layer_count = result.layers.size();
|
||||||
|
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::success(std::move(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>
|
||||||
|
export_document_animation_frames_equirectangular_pngs(
|
||||||
|
DocumentAnimationFrameEquirectangularPngExportRequest request)
|
||||||
|
{
|
||||||
|
if (request.document == nullptr) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document animation-frame export request requires a document"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationFrameEquirectangularPngExportResult result;
|
||||||
|
result.frames.reserve(request.document->frames().size());
|
||||||
|
for (std::size_t frame_index = 0; frame_index < request.document->frames().size(); ++frame_index) {
|
||||||
|
auto exported = export_document_frame_equirectangular_png(DocumentFrameCompositeRequest {
|
||||||
|
.document = request.document,
|
||||||
|
.frame_index = frame_index,
|
||||||
|
.clear_color = request.clear_color,
|
||||||
|
});
|
||||||
|
if (!exported) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::failure(
|
||||||
|
exported.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationFrameEquirectangularPng frame;
|
||||||
|
frame.frame_index = frame_index;
|
||||||
|
frame.face_extent = exported.value().face_extent;
|
||||||
|
frame.equirectangular_extent = exported.value().equirectangular_extent;
|
||||||
|
frame.encoded_bytes = exported.value().encoded_bytes;
|
||||||
|
frame.face_payload_count = exported.value().face_payload_count;
|
||||||
|
frame.composited_layer_face_count = exported.value().composited_layer_face_count;
|
||||||
|
frame.png = std::move(exported.value().png);
|
||||||
|
result.encoded_bytes += frame.encoded_bytes;
|
||||||
|
result.frames.push_back(std::move(frame));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.frame_count = result.frames.size();
|
||||||
|
return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::success(std::move(result));
|
||||||
|
}
|
||||||
|
|
||||||
pp::foundation::Result<DocumentFrameExportReadinessResult> prepare_document_frame_export_readiness(
|
pp::foundation::Result<DocumentFrameExportReadinessResult> prepare_document_frame_export_readiness(
|
||||||
DocumentFrameCompositeRequest request)
|
DocumentFrameCompositeRequest request)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace pp::paint_renderer {
|
namespace pp::paint_renderer {
|
||||||
@@ -155,6 +156,50 @@ struct DocumentFrameEquirectangularPngExportResult {
|
|||||||
std::size_t composited_layer_face_count = 0;
|
std::size_t composited_layer_face_count = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct DocumentLayerEquirectangularPngExportRequest {
|
||||||
|
const pp::document::CanvasDocument* document = nullptr;
|
||||||
|
std::size_t frame_index = 0;
|
||||||
|
pp::paint::Rgba clear_color {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentLayerEquirectangularPng {
|
||||||
|
std::size_t layer_index = 0;
|
||||||
|
std::string layer_name;
|
||||||
|
pp::renderer::Extent2D face_extent {};
|
||||||
|
pp::renderer::Extent2D equirectangular_extent {};
|
||||||
|
std::vector<std::byte> png;
|
||||||
|
std::uint64_t encoded_bytes = 0;
|
||||||
|
std::size_t face_payload_count = 0;
|
||||||
|
std::size_t composited_layer_face_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentLayerEquirectangularPngExportResult {
|
||||||
|
std::vector<DocumentLayerEquirectangularPng> layers;
|
||||||
|
std::uint64_t encoded_bytes = 0;
|
||||||
|
std::size_t layer_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentAnimationFrameEquirectangularPngExportRequest {
|
||||||
|
const pp::document::CanvasDocument* document = nullptr;
|
||||||
|
pp::paint::Rgba clear_color {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentAnimationFrameEquirectangularPng {
|
||||||
|
std::size_t frame_index = 0;
|
||||||
|
pp::renderer::Extent2D face_extent {};
|
||||||
|
pp::renderer::Extent2D equirectangular_extent {};
|
||||||
|
std::vector<std::byte> png;
|
||||||
|
std::uint64_t encoded_bytes = 0;
|
||||||
|
std::size_t face_payload_count = 0;
|
||||||
|
std::size_t composited_layer_face_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentAnimationFrameEquirectangularPngExportResult {
|
||||||
|
std::vector<DocumentAnimationFrameEquirectangularPng> frames;
|
||||||
|
std::uint64_t encoded_bytes = 0;
|
||||||
|
std::size_t frame_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct DocumentFrameExportReadinessResult {
|
struct DocumentFrameExportReadinessResult {
|
||||||
RecordedDocumentFrameUploadResult recorded_upload {};
|
RecordedDocumentFrameUploadResult recorded_upload {};
|
||||||
DocumentFrameFacePngExportResult face_pngs {};
|
DocumentFrameFacePngExportResult face_pngs {};
|
||||||
@@ -187,6 +232,13 @@ export_document_frame_equirectangular_png(const DocumentFrameCompositeResult& co
|
|||||||
[[nodiscard]] pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>
|
[[nodiscard]] pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>
|
||||||
export_document_frame_equirectangular_png(DocumentFrameCompositeRequest request);
|
export_document_frame_equirectangular_png(DocumentFrameCompositeRequest request);
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>
|
||||||
|
export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request);
|
||||||
|
|
||||||
|
[[nodiscard]] pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>
|
||||||
|
export_document_animation_frames_equirectangular_pngs(
|
||||||
|
DocumentAnimationFrameEquirectangularPngExportRequest request);
|
||||||
|
|
||||||
[[nodiscard]] pp::foundation::Result<DocumentFrameExportReadinessResult> prepare_document_frame_export_readiness(
|
[[nodiscard]] pp::foundation::Result<DocumentFrameExportReadinessResult> prepare_document_frame_export_readiness(
|
||||||
DocumentFrameCompositeRequest request);
|
DocumentFrameCompositeRequest request);
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,9 @@ public:
|
|||||||
std::string call_order;
|
std::string call_order;
|
||||||
};
|
};
|
||||||
|
|
||||||
class FakeDocumentCubeFaceExportWriteServices final : public pp::app::DocumentCubeFaceExportWriteServices {
|
class FakeDocumentCubeFaceExportWriteServices final
|
||||||
|
: public pp::app::DocumentCubeFaceExportWriteServices
|
||||||
|
, public pp::app::DocumentExportCollectionWriteServices {
|
||||||
public:
|
public:
|
||||||
pp::foundation::Status write_binary_file(
|
pp::foundation::Status write_binary_file(
|
||||||
std::string_view path,
|
std::string_view path,
|
||||||
@@ -347,6 +349,96 @@ void cube_face_export_writer_rejects_malformed_inputs(pp::tests::Harness& harnes
|
|||||||
PP_EXPECT(harness, services.publish_calls == 0);
|
PP_EXPECT(harness, services.publish_calls == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void collection_export_builds_legacy_path_suffixes(pp::tests::Harness& harness)
|
||||||
|
{
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
pp::app::make_document_layer_export_path_suffix(0, "Base") == "-layer00-Base.png");
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
pp::app::make_document_layer_export_path_suffix(12, "Paint") == "-layer12-Paint.png");
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
pp::app::make_document_animation_frame_export_path_suffix(0) == "-00.png");
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
pp::app::make_document_animation_frame_export_path_suffix(125) == "-125.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
void collection_export_writer_writes_and_publishes_payloads_in_order(pp::tests::Harness& harness)
|
||||||
|
{
|
||||||
|
const auto target = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_layers");
|
||||||
|
const std::array<std::byte, 2> base_bytes {};
|
||||||
|
const std::array<std::byte, 3> paint_bytes {};
|
||||||
|
const pp::app::DocumentExportCollectionPngPayload payloads[] {
|
||||||
|
{
|
||||||
|
.path_suffix = pp::app::make_document_layer_export_path_suffix(0, "Base"),
|
||||||
|
.bytes = std::span<const std::byte>(base_bytes),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.path_suffix = pp::app::make_document_layer_export_path_suffix(1, "Paint"),
|
||||||
|
.bytes = std::span<const std::byte>(paint_bytes),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
FakeDocumentCubeFaceExportWriteServices services;
|
||||||
|
|
||||||
|
PP_EXPECT(harness, target);
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
pp::app::execute_document_export_collection_write(target.value(), payloads, services).ok());
|
||||||
|
PP_EXPECT(harness, services.write_calls == 2);
|
||||||
|
PP_EXPECT(harness, services.publish_calls == 2);
|
||||||
|
PP_EXPECT(harness, services.total_bytes == 5U);
|
||||||
|
PP_EXPECT(harness, services.last_path == "D:/Paint/demo_layers/demo-layer01-Paint.png");
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
services.call_order
|
||||||
|
== "write:D:/Paint/demo_layers/demo-layer00-Base.png;"
|
||||||
|
"publish:D:/Paint/demo_layers/demo-layer00-Base.png;"
|
||||||
|
"write:D:/Paint/demo_layers/demo-layer01-Paint.png;"
|
||||||
|
"publish:D:/Paint/demo_layers/demo-layer01-Paint.png;");
|
||||||
|
}
|
||||||
|
|
||||||
|
void collection_export_writer_rejects_malformed_inputs(pp::tests::Harness& harness)
|
||||||
|
{
|
||||||
|
const auto target = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_frames");
|
||||||
|
const std::array<std::byte, 2> bytes {};
|
||||||
|
pp::app::DocumentExportCollectionPngPayload payloads[] {
|
||||||
|
{
|
||||||
|
.path_suffix = pp::app::make_document_animation_frame_export_path_suffix(0),
|
||||||
|
.bytes = std::span<const std::byte>(bytes),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
FakeDocumentCubeFaceExportWriteServices services;
|
||||||
|
|
||||||
|
PP_EXPECT(harness, target);
|
||||||
|
auto empty_stem = target.value();
|
||||||
|
empty_stem.stem_path.clear();
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
!pp::app::execute_document_export_collection_write(empty_stem, payloads, services).ok());
|
||||||
|
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
!pp::app::execute_document_export_collection_write(
|
||||||
|
target.value(),
|
||||||
|
std::span<const pp::app::DocumentExportCollectionPngPayload>(),
|
||||||
|
services)
|
||||||
|
.ok());
|
||||||
|
|
||||||
|
payloads[0].path_suffix.clear();
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
!pp::app::execute_document_export_collection_write(target.value(), payloads, services).ok());
|
||||||
|
payloads[0].path_suffix = pp::app::make_document_animation_frame_export_path_suffix(0);
|
||||||
|
payloads[0].bytes = {};
|
||||||
|
PP_EXPECT(
|
||||||
|
harness,
|
||||||
|
!pp::app::execute_document_export_collection_write(target.value(), payloads, services).ok());
|
||||||
|
PP_EXPECT(harness, services.write_calls == 0);
|
||||||
|
PP_EXPECT(harness, services.publish_calls == 0);
|
||||||
|
}
|
||||||
|
|
||||||
void video_export_builds_suggested_name(pp::tests::Harness& harness)
|
void video_export_builds_suggested_name(pp::tests::Harness& harness)
|
||||||
{
|
{
|
||||||
const auto timelapse = pp::app::make_document_export_suggested_name("demo", "-timelapse");
|
const auto timelapse = pp::app::make_document_export_suggested_name("demo", "-timelapse");
|
||||||
@@ -891,6 +983,11 @@ int main()
|
|||||||
"cube face export writer stops before publish on write failure",
|
"cube face export writer stops before publish on write failure",
|
||||||
cube_face_export_writer_stops_before_publish_on_write_failure);
|
cube_face_export_writer_stops_before_publish_on_write_failure);
|
||||||
harness.run("cube face export writer rejects malformed inputs", cube_face_export_writer_rejects_malformed_inputs);
|
harness.run("cube face export writer rejects malformed inputs", cube_face_export_writer_rejects_malformed_inputs);
|
||||||
|
harness.run("collection export builds legacy path suffixes", collection_export_builds_legacy_path_suffixes);
|
||||||
|
harness.run(
|
||||||
|
"collection export writer writes and publishes payloads in order",
|
||||||
|
collection_export_writer_writes_and_publishes_payloads_in_order);
|
||||||
|
harness.run("collection export writer rejects malformed inputs", collection_export_writer_rejects_malformed_inputs);
|
||||||
harness.run("video export builds suggested name", video_export_builds_suggested_name);
|
harness.run("video export builds suggested name", video_export_builds_suggested_name);
|
||||||
harness.run("collection export target plan selects platform destination", collection_export_target_plan_selects_platform_destination);
|
harness.run("collection export target plan selects platform destination", collection_export_target_plan_selects_platform_destination);
|
||||||
harness.run("export success dialog plans image destinations", export_success_dialog_plans_image_destinations);
|
harness.run("export success dialog plans image destinations", export_success_dialog_plans_image_destinations);
|
||||||
|
|||||||
@@ -46,6 +46,60 @@ bool near(float a, float b)
|
|||||||
return std::fabs(a - b) < 0.0001F;
|
return std::fabs(a - b) < 0.0001F;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> solid_rgba8(
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height,
|
||||||
|
std::uint8_t r,
|
||||||
|
std::uint8_t g,
|
||||||
|
std::uint8_t b,
|
||||||
|
std::uint8_t a)
|
||||||
|
{
|
||||||
|
std::vector<std::uint8_t> pixels(
|
||||||
|
static_cast<std::size_t>(width) * height * pp::document::rgba8_components);
|
||||||
|
for (std::size_t i = 0; i < pixels.size(); i += pp::document::rgba8_components) {
|
||||||
|
pixels[i] = r;
|
||||||
|
pixels[i + 1U] = g;
|
||||||
|
pixels[i + 2U] = b;
|
||||||
|
pixels[i + 3U] = a;
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerFacePixels solid_face_payload(
|
||||||
|
std::uint32_t face_index,
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height,
|
||||||
|
std::uint8_t r,
|
||||||
|
std::uint8_t g,
|
||||||
|
std::uint8_t b,
|
||||||
|
std::uint8_t a)
|
||||||
|
{
|
||||||
|
return LayerFacePixels {
|
||||||
|
.face_index = face_index,
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.rgba8 = solid_rgba8(width, height, r, g, b, a),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<LayerFacePixels> solid_cube_faces(
|
||||||
|
std::uint32_t width,
|
||||||
|
std::uint32_t height,
|
||||||
|
std::uint8_t r,
|
||||||
|
std::uint8_t g,
|
||||||
|
std::uint8_t b,
|
||||||
|
std::uint8_t a)
|
||||||
|
{
|
||||||
|
std::vector<LayerFacePixels> faces;
|
||||||
|
faces.reserve(pp::document::cube_face_count);
|
||||||
|
for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) {
|
||||||
|
faces.push_back(solid_face_payload(face_index, width, height, r, g, b, a));
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
void composites_visible_layer_with_opacity(pp::tests::Harness& h)
|
void composites_visible_layer_with_opacity(pp::tests::Harness& h)
|
||||||
{
|
{
|
||||||
std::vector<Rgba> destination {
|
std::vector<Rgba> destination {
|
||||||
@@ -848,6 +902,140 @@ void exports_document_frame_as_equirectangular_png(pp::tests::Harness& h)
|
|||||||
PP_EXPECT(h, decoded.value().pixels[bottom + 2U] == 255U);
|
PP_EXPECT(h, decoded.value().pixels[bottom + 2U] == 255U);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void exports_document_layers_as_equirectangular_pngs(pp::tests::Harness& h)
|
||||||
|
{
|
||||||
|
const AnimationFrame root_frames[] {
|
||||||
|
{ .duration_ms = 100, .face_pixels = {} },
|
||||||
|
};
|
||||||
|
const AnimationFrame base_frames[] {
|
||||||
|
{
|
||||||
|
.duration_ms = 100,
|
||||||
|
.face_pixels = solid_cube_faces(1, 4, 255, 0, 0, 255),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const AnimationFrame hidden_frames[] {
|
||||||
|
{
|
||||||
|
.duration_ms = 100,
|
||||||
|
.face_pixels = solid_cube_faces(1, 4, 0, 0, 255, 255),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const DocumentLayerConfig layers[] {
|
||||||
|
{
|
||||||
|
.name = "Base",
|
||||||
|
.frames = std::span<const AnimationFrame>(base_frames, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "HiddenPaint",
|
||||||
|
.visible = false,
|
||||||
|
.opacity = 0.0F,
|
||||||
|
.frames = std::span<const AnimationFrame>(hidden_frames, 1),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||||
|
.width = 1,
|
||||||
|
.height = 4,
|
||||||
|
.layers = std::span<const DocumentLayerConfig>(layers, 2),
|
||||||
|
.frames = std::span<const AnimationFrame>(root_frames, 1),
|
||||||
|
.selection_masks = {},
|
||||||
|
});
|
||||||
|
PP_EXPECT(h, document);
|
||||||
|
if (!document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto exported = pp::paint_renderer::export_document_layers_equirectangular_pngs(
|
||||||
|
pp::paint_renderer::DocumentLayerEquirectangularPngExportRequest {
|
||||||
|
.document = &document.value(),
|
||||||
|
.frame_index = 0,
|
||||||
|
});
|
||||||
|
PP_EXPECT(h, exported);
|
||||||
|
if (!exported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PP_EXPECT(h, exported.value().layer_count == 2U);
|
||||||
|
PP_EXPECT(h, exported.value().layers.size() == 2U);
|
||||||
|
PP_EXPECT(h, exported.value().layers[0].layer_name == "Base");
|
||||||
|
PP_EXPECT(h, exported.value().layers[1].layer_name == "HiddenPaint");
|
||||||
|
PP_EXPECT(h, exported.value().layers[0].face_payload_count == pp::document::cube_face_count);
|
||||||
|
PP_EXPECT(h, exported.value().layers[1].face_payload_count == pp::document::cube_face_count);
|
||||||
|
PP_EXPECT(h, exported.value().encoded_bytes > 0U);
|
||||||
|
|
||||||
|
const auto decoded = pp::assets::decode_png_rgba8(exported.value().layers[1].png);
|
||||||
|
PP_EXPECT(h, decoded);
|
||||||
|
if (!decoded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PP_EXPECT(h, decoded.value().width == 4U);
|
||||||
|
PP_EXPECT(h, decoded.value().height == 8U);
|
||||||
|
PP_EXPECT(h, decoded.value().pixels[0] == 0U);
|
||||||
|
PP_EXPECT(h, decoded.value().pixels[1] == 0U);
|
||||||
|
PP_EXPECT(h, decoded.value().pixels[2] == 255U);
|
||||||
|
}
|
||||||
|
|
||||||
|
void exports_document_animation_frames_as_equirectangular_pngs(pp::tests::Harness& h)
|
||||||
|
{
|
||||||
|
const AnimationFrame root_frames[] {
|
||||||
|
{ .duration_ms = 100, .face_pixels = {} },
|
||||||
|
{ .duration_ms = 100, .face_pixels = {} },
|
||||||
|
};
|
||||||
|
const AnimationFrame layer_frames[] {
|
||||||
|
{
|
||||||
|
.duration_ms = 100,
|
||||||
|
.face_pixels = solid_cube_faces(1, 4, 255, 0, 0, 255),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.duration_ms = 100,
|
||||||
|
.face_pixels = solid_cube_faces(1, 4, 0, 255, 0, 255),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const DocumentLayerConfig layers[] {
|
||||||
|
{
|
||||||
|
.name = "Paint",
|
||||||
|
.frames = std::span<const AnimationFrame>(layer_frames, 2),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||||
|
.width = 1,
|
||||||
|
.height = 4,
|
||||||
|
.layers = std::span<const DocumentLayerConfig>(layers, 1),
|
||||||
|
.frames = std::span<const AnimationFrame>(root_frames, 2),
|
||||||
|
.selection_masks = {},
|
||||||
|
});
|
||||||
|
PP_EXPECT(h, document);
|
||||||
|
if (!document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto exported = pp::paint_renderer::export_document_animation_frames_equirectangular_pngs(
|
||||||
|
pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {
|
||||||
|
.document = &document.value(),
|
||||||
|
});
|
||||||
|
PP_EXPECT(h, exported);
|
||||||
|
if (!exported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PP_EXPECT(h, exported.value().frame_count == 2U);
|
||||||
|
PP_EXPECT(h, exported.value().frames.size() == 2U);
|
||||||
|
PP_EXPECT(h, exported.value().frames[0].frame_index == 0U);
|
||||||
|
PP_EXPECT(h, exported.value().frames[1].frame_index == 1U);
|
||||||
|
PP_EXPECT(h, exported.value().encoded_bytes > 0U);
|
||||||
|
|
||||||
|
const auto decoded = pp::assets::decode_png_rgba8(exported.value().frames[1].png);
|
||||||
|
PP_EXPECT(h, decoded);
|
||||||
|
if (!decoded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PP_EXPECT(h, decoded.value().width == 4U);
|
||||||
|
PP_EXPECT(h, decoded.value().height == 8U);
|
||||||
|
PP_EXPECT(h, decoded.value().pixels[0] == 0U);
|
||||||
|
PP_EXPECT(h, decoded.value().pixels[1] == 255U);
|
||||||
|
PP_EXPECT(h, decoded.value().pixels[2] == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h)
|
void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h)
|
||||||
{
|
{
|
||||||
RecordingRenderDevice device;
|
RecordingRenderDevice device;
|
||||||
@@ -858,6 +1046,10 @@ void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h)
|
|||||||
DocumentFrameCompositeRequest {});
|
DocumentFrameCompositeRequest {});
|
||||||
const auto no_document_equirect = pp::paint_renderer::export_document_frame_equirectangular_png(
|
const auto no_document_equirect = pp::paint_renderer::export_document_frame_equirectangular_png(
|
||||||
DocumentFrameCompositeRequest {});
|
DocumentFrameCompositeRequest {});
|
||||||
|
const auto no_document_layers = pp::paint_renderer::export_document_layers_equirectangular_pngs(
|
||||||
|
pp::paint_renderer::DocumentLayerEquirectangularPngExportRequest {});
|
||||||
|
const auto no_document_frames = pp::paint_renderer::export_document_animation_frames_equirectangular_pngs(
|
||||||
|
pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {});
|
||||||
|
|
||||||
const AnimationFrame root_frames[] {
|
const AnimationFrame root_frames[] {
|
||||||
{ .duration_ms = 100, .face_pixels = {} },
|
{ .duration_ms = 100, .face_pixels = {} },
|
||||||
@@ -892,6 +1084,10 @@ void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h)
|
|||||||
PP_EXPECT(h, no_document_readiness.status().code == StatusCode::invalid_argument);
|
PP_EXPECT(h, no_document_readiness.status().code == StatusCode::invalid_argument);
|
||||||
PP_EXPECT(h, !no_document_equirect.ok());
|
PP_EXPECT(h, !no_document_equirect.ok());
|
||||||
PP_EXPECT(h, no_document_equirect.status().code == StatusCode::invalid_argument);
|
PP_EXPECT(h, no_document_equirect.status().code == StatusCode::invalid_argument);
|
||||||
|
PP_EXPECT(h, !no_document_layers.ok());
|
||||||
|
PP_EXPECT(h, no_document_layers.status().code == StatusCode::invalid_argument);
|
||||||
|
PP_EXPECT(h, !no_document_frames.ok());
|
||||||
|
PP_EXPECT(h, no_document_frames.status().code == StatusCode::invalid_argument);
|
||||||
PP_EXPECT(h, !bad_frame.ok());
|
PP_EXPECT(h, !bad_frame.ok());
|
||||||
PP_EXPECT(h, bad_frame.status().code == StatusCode::out_of_range);
|
PP_EXPECT(h, bad_frame.status().code == StatusCode::out_of_range);
|
||||||
PP_EXPECT(h, !bad_frame_readiness.ok());
|
PP_EXPECT(h, !bad_frame_readiness.ok());
|
||||||
@@ -1248,6 +1444,10 @@ int main()
|
|||||||
harness.run("exports_document_frame_faces_as_pngs", exports_document_frame_faces_as_pngs);
|
harness.run("exports_document_frame_faces_as_pngs", exports_document_frame_faces_as_pngs);
|
||||||
harness.run("prepares_document_frame_export_readiness_report", prepares_document_frame_export_readiness_report);
|
harness.run("prepares_document_frame_export_readiness_report", prepares_document_frame_export_readiness_report);
|
||||||
harness.run("exports_document_frame_as_equirectangular_png", exports_document_frame_as_equirectangular_png);
|
harness.run("exports_document_frame_as_equirectangular_png", exports_document_frame_as_equirectangular_png);
|
||||||
|
harness.run("exports_document_layers_as_equirectangular_pngs", exports_document_layers_as_equirectangular_pngs);
|
||||||
|
harness.run(
|
||||||
|
"exports_document_animation_frames_as_equirectangular_pngs",
|
||||||
|
exports_document_animation_frames_as_equirectangular_pngs);
|
||||||
harness.run("document_frame_upload_rejects_invalid_requests", document_frame_upload_rejects_invalid_requests);
|
harness.run("document_frame_upload_rejects_invalid_requests", document_frame_upload_rejects_invalid_requests);
|
||||||
harness.run("detects_feedback_requirements", detects_feedback_requirements);
|
harness.run("detects_feedback_requirements", detects_feedback_requirements);
|
||||||
harness.run("plans_stroke_composite_paths", plans_stroke_composite_paths);
|
harness.run("plans_stroke_composite_paths", plans_stroke_composite_paths);
|
||||||
|
|||||||
Reference in New Issue
Block a user