350 lines
12 KiB
C++
350 lines
12 KiB
C++
#pragma once
|
|
|
|
#include "document/document.h"
|
|
#include "document/ppi_export.h"
|
|
#include "foundation/result.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <span>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace pp::app {
|
|
|
|
struct DocumentCanvasClearPlan {
|
|
float r = 0.0F;
|
|
float g = 0.0F;
|
|
float b = 0.0F;
|
|
float a = 0.0F;
|
|
bool clears_canvas = false;
|
|
bool records_undo = false;
|
|
bool marks_unsaved = false;
|
|
bool no_op = true;
|
|
};
|
|
|
|
struct DocumentCanvasFacePayloadInput {
|
|
std::uint32_t frame_index = 0;
|
|
std::uint32_t face_index = 0;
|
|
std::uint32_t x = 0;
|
|
std::uint32_t y = 0;
|
|
std::uint32_t width = 0;
|
|
std::uint32_t height = 0;
|
|
std::span<const std::uint8_t> rgba8;
|
|
};
|
|
|
|
struct DocumentCanvasLayerSnapshotInput {
|
|
std::string_view name;
|
|
bool visible = true;
|
|
bool alpha_locked = false;
|
|
float opacity = 1.0F;
|
|
int blend_mode = 0;
|
|
std::span<const std::uint32_t> frame_durations_ms;
|
|
std::size_t pending_face_payloads = 0;
|
|
std::span<const DocumentCanvasFacePayloadInput> captured_face_payloads;
|
|
};
|
|
|
|
struct DocumentCanvasSnapshotInput {
|
|
bool has_canvas = true;
|
|
std::uint32_t width = 0;
|
|
std::uint32_t height = 0;
|
|
std::size_t active_layer_index = 0;
|
|
std::size_t active_frame_index = 0;
|
|
std::span<const DocumentCanvasLayerSnapshotInput> layers;
|
|
};
|
|
|
|
struct DocumentCanvasSnapshotResult {
|
|
pp::document::CanvasDocument document;
|
|
std::size_t layer_count = 0;
|
|
std::size_t frame_count = 0;
|
|
std::size_t pending_face_payloads = 0;
|
|
std::size_t captured_face_payloads = 0;
|
|
bool metadata_only = false;
|
|
bool requires_renderer_payload_readback = false;
|
|
};
|
|
|
|
struct DocumentCanvasSaveSnapshotReport {
|
|
std::uint32_t width = 0;
|
|
std::uint32_t height = 0;
|
|
std::size_t layer_count = 0;
|
|
std::size_t frame_count = 0;
|
|
std::size_t captured_face_payloads = 0;
|
|
std::size_t pending_face_payloads = 0;
|
|
bool payload_complete = false;
|
|
bool can_export_ppi = false;
|
|
};
|
|
|
|
enum class DocumentCanvasSaveWriterAction {
|
|
use_document_ppi_writer,
|
|
use_legacy_project_save,
|
|
};
|
|
|
|
struct DocumentCanvasSaveWriterRoutePlan {
|
|
DocumentCanvasSaveWriterAction action = DocumentCanvasSaveWriterAction::use_legacy_project_save;
|
|
bool payload_complete = false;
|
|
bool can_export_ppi = false;
|
|
bool uses_document_ppi_writer = false;
|
|
std::string_view fallback_reason;
|
|
};
|
|
|
|
struct DocumentCanvasPpiExportResult {
|
|
DocumentCanvasSaveSnapshotReport report;
|
|
std::vector<std::byte> bytes;
|
|
};
|
|
|
|
class DocumentCanvasClearServices {
|
|
public:
|
|
virtual ~DocumentCanvasClearServices() = default;
|
|
|
|
virtual void clear_current_canvas(float r, float g, float b, float a) = 0;
|
|
};
|
|
|
|
[[nodiscard]] inline pp::foundation::Status validate_clear_color_channel(float value) noexcept
|
|
{
|
|
if (!std::isfinite(value)) {
|
|
return pp::foundation::Status::invalid_argument("clear color channel must be finite");
|
|
}
|
|
if (value < 0.0F || value > 1.0F) {
|
|
return pp::foundation::Status::out_of_range("clear color channel must be within 0..1");
|
|
}
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasSnapshotResult> plan_document_canvas_snapshot(
|
|
DocumentCanvasSnapshotInput input)
|
|
{
|
|
if (!input.has_canvas) {
|
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
|
pp::foundation::Status::invalid_argument("document canvas snapshot requires a canvas"));
|
|
}
|
|
|
|
if (input.layers.empty()) {
|
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
|
pp::foundation::Status::invalid_argument("document canvas snapshot requires at least one layer"));
|
|
}
|
|
|
|
std::size_t frame_count = 1U;
|
|
std::size_t pending_face_payloads = 0U;
|
|
std::size_t captured_face_payloads = 0U;
|
|
for (const auto& layer : input.layers) {
|
|
frame_count = std::max(frame_count, layer.frame_durations_ms.size());
|
|
pending_face_payloads += layer.pending_face_payloads;
|
|
captured_face_payloads += layer.captured_face_payloads.size();
|
|
}
|
|
|
|
if (input.active_layer_index >= input.layers.size()) {
|
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
|
pp::foundation::Status::out_of_range("active canvas layer is outside the document snapshot"));
|
|
}
|
|
|
|
if (input.active_frame_index >= frame_count) {
|
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
|
pp::foundation::Status::out_of_range("active canvas frame is outside the document snapshot"));
|
|
}
|
|
|
|
std::vector<pp::document::AnimationFrame> root_frames;
|
|
root_frames.reserve(frame_count);
|
|
for (std::size_t frame_index = 0; frame_index < frame_count; ++frame_index) {
|
|
std::uint32_t duration_ms = 100U;
|
|
for (const auto& layer : input.layers) {
|
|
if (frame_index < layer.frame_durations_ms.size()) {
|
|
duration_ms = layer.frame_durations_ms[frame_index];
|
|
break;
|
|
}
|
|
}
|
|
root_frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
|
|
}
|
|
|
|
std::vector<std::string> layer_names;
|
|
std::vector<std::vector<pp::document::AnimationFrame>> layer_frames;
|
|
std::vector<pp::document::DocumentLayerConfig> layer_configs;
|
|
layer_names.reserve(input.layers.size());
|
|
layer_frames.reserve(input.layers.size());
|
|
layer_configs.reserve(input.layers.size());
|
|
|
|
for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
|
|
const auto& layer = input.layers[layer_index];
|
|
if (layer.name.empty()) {
|
|
layer_names.push_back("Layer " + std::to_string(layer_index + 1U));
|
|
} else {
|
|
layer_names.push_back(std::string(layer.name));
|
|
}
|
|
|
|
layer_frames.push_back({});
|
|
auto& frames = layer_frames.back();
|
|
frames.reserve(layer.frame_durations_ms.empty() ? root_frames.size() : layer.frame_durations_ms.size());
|
|
if (layer.frame_durations_ms.empty()) {
|
|
frames = root_frames;
|
|
} else {
|
|
for (const auto duration_ms : layer.frame_durations_ms) {
|
|
frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
|
|
}
|
|
}
|
|
|
|
layer_configs.push_back(pp::document::DocumentLayerConfig {
|
|
.name = layer_names.back(),
|
|
.visible = layer.visible,
|
|
.alpha_locked = layer.alpha_locked,
|
|
.opacity = layer.opacity,
|
|
.blend_mode = static_cast<pp::paint::BlendMode>(layer.blend_mode),
|
|
.frames = std::span<const pp::document::AnimationFrame>(frames),
|
|
});
|
|
}
|
|
|
|
auto document = pp::document::CanvasDocument::create_from_snapshot(pp::document::DocumentSnapshotConfig {
|
|
.width = input.width,
|
|
.height = input.height,
|
|
.layers = std::span<const pp::document::DocumentLayerConfig>(layer_configs),
|
|
.frames = std::span<const pp::document::AnimationFrame>(root_frames),
|
|
});
|
|
if (!document) {
|
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(document.status());
|
|
}
|
|
|
|
for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
|
|
for (const auto& payload : input.layers[layer_index].captured_face_payloads) {
|
|
pp::document::LayerFacePixels pixels {
|
|
.face_index = payload.face_index,
|
|
.x = payload.x,
|
|
.y = payload.y,
|
|
.width = payload.width,
|
|
.height = payload.height,
|
|
.rgba8 = std::vector<std::uint8_t>(payload.rgba8.begin(), payload.rgba8.end()),
|
|
};
|
|
const auto payload_status = document.value().set_layer_frame_face_pixels(
|
|
layer_index,
|
|
payload.frame_index,
|
|
std::move(pixels));
|
|
if (!payload_status.ok()) {
|
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(payload_status);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto active_status = document.value().set_active_layer(input.active_layer_index);
|
|
if (!active_status.ok()) {
|
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
|
|
}
|
|
|
|
active_status = document.value().set_active_frame(input.active_frame_index);
|
|
if (!active_status.ok()) {
|
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
|
|
}
|
|
|
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::success(DocumentCanvasSnapshotResult {
|
|
.document = std::move(document.value()),
|
|
.layer_count = input.layers.size(),
|
|
.frame_count = frame_count,
|
|
.pending_face_payloads = pending_face_payloads,
|
|
.captured_face_payloads = captured_face_payloads,
|
|
.metadata_only = captured_face_payloads == 0U,
|
|
.requires_renderer_payload_readback = pending_face_payloads > captured_face_payloads,
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] inline DocumentCanvasSaveSnapshotReport make_document_canvas_save_snapshot_report(
|
|
const DocumentCanvasSnapshotResult& snapshot) noexcept
|
|
{
|
|
return DocumentCanvasSaveSnapshotReport {
|
|
.width = snapshot.document.width(),
|
|
.height = snapshot.document.height(),
|
|
.layer_count = snapshot.layer_count,
|
|
.frame_count = snapshot.frame_count,
|
|
.captured_face_payloads = snapshot.captured_face_payloads,
|
|
.pending_face_payloads = snapshot.pending_face_payloads,
|
|
.payload_complete = !snapshot.requires_renderer_payload_readback,
|
|
.can_export_ppi = !snapshot.requires_renderer_payload_readback,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] constexpr DocumentCanvasSaveWriterRoutePlan plan_document_canvas_save_writer_route(
|
|
DocumentCanvasSaveSnapshotReport report) noexcept
|
|
{
|
|
DocumentCanvasSaveWriterRoutePlan plan;
|
|
plan.payload_complete = report.payload_complete;
|
|
plan.can_export_ppi = report.can_export_ppi;
|
|
|
|
if (!report.payload_complete || !report.can_export_ppi) {
|
|
plan.fallback_reason = "canvas document snapshot still requires renderer payload readback";
|
|
return plan;
|
|
}
|
|
|
|
plan.action = DocumentCanvasSaveWriterAction::use_document_ppi_writer;
|
|
plan.uses_document_ppi_writer = true;
|
|
return plan;
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasPpiExportResult>
|
|
export_document_canvas_save_snapshot_to_ppi(const DocumentCanvasSnapshotResult& snapshot)
|
|
{
|
|
const auto report = make_document_canvas_save_snapshot_report(snapshot);
|
|
const auto route = plan_document_canvas_save_writer_route(report);
|
|
if (!route.uses_document_ppi_writer) {
|
|
return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(
|
|
pp::foundation::Status::invalid_argument(route.fallback_reason.data()));
|
|
}
|
|
|
|
auto bytes = pp::document::export_ppi_project_document(snapshot.document);
|
|
if (!bytes) {
|
|
return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(bytes.status());
|
|
}
|
|
|
|
return pp::foundation::Result<DocumentCanvasPpiExportResult>::success(DocumentCanvasPpiExportResult {
|
|
.report = report,
|
|
.bytes = std::move(bytes.value()),
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
|
|
bool has_canvas,
|
|
float r = 0.0F,
|
|
float g = 0.0F,
|
|
float b = 0.0F,
|
|
float a = 0.0F) noexcept
|
|
{
|
|
const float channels[] { r, g, b, a };
|
|
for (const float channel : channels) {
|
|
const auto status = validate_clear_color_channel(channel);
|
|
if (!status.ok()) {
|
|
return pp::foundation::Result<DocumentCanvasClearPlan>::failure(status);
|
|
}
|
|
}
|
|
|
|
DocumentCanvasClearPlan plan;
|
|
plan.r = r;
|
|
plan.g = g;
|
|
plan.b = b;
|
|
plan.a = a;
|
|
plan.clears_canvas = has_canvas;
|
|
plan.records_undo = has_canvas;
|
|
plan.marks_unsaved = has_canvas;
|
|
plan.no_op = !has_canvas;
|
|
return pp::foundation::Result<DocumentCanvasClearPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Status execute_document_canvas_clear_plan(
|
|
const DocumentCanvasClearPlan& plan,
|
|
DocumentCanvasClearServices& services)
|
|
{
|
|
const float channels[] { plan.r, plan.g, plan.b, plan.a };
|
|
for (const float channel : channels) {
|
|
const auto status = validate_clear_color_channel(channel);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
}
|
|
|
|
if (plan.no_op || !plan.clears_canvas) {
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
services.clear_current_canvas(plan.r, plan.g, plan.b, plan.a);
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
} // namespace pp::app
|