Add document selection mask automation

This commit is contained in:
2026-06-02 10:55:12 +02:00
parent 1ab2a9b846
commit ddca24779e
9 changed files with 304 additions and 20 deletions

View File

@@ -3,6 +3,7 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <string>
#include <utility>
namespace pp::document {
@@ -107,37 +108,70 @@ namespace {
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
[[nodiscard]] pp::foundation::Result<std::size_t> byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
std::uint32_t height,
std::uint32_t components,
const char* dimensions_overflow_message,
const char* byte_size_overflow_message,
const char* payload_limit_message,
const char* addressable_memory_message) noexcept
{
const auto width64 = static_cast<std::uint64_t>(width);
const auto height64 = static_cast<std::uint64_t>(height);
if (width64 > std::numeric_limits<std::uint64_t>::max() / height64) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel dimensions overflow"));
pp::foundation::Status::out_of_range(dimensions_overflow_message));
}
const auto pixels = width64 * height64;
if (pixels > std::numeric_limits<std::uint64_t>::max() / rgba8_components) {
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel byte size overflows"));
pp::foundation::Status::out_of_range(byte_size_overflow_message));
}
const auto bytes = pixels * rgba8_components;
const auto bytes = pixels * components;
if (bytes > max_face_pixel_payload_bytes) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel payload exceeds the configured limit"));
pp::foundation::Status::out_of_range(payload_limit_message));
}
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel payload exceeds addressable memory"));
pp::foundation::Status::out_of_range(addressable_memory_message));
}
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
}
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
{
return byte_size(
width,
height,
rgba8_components,
"face pixel dimensions overflow",
"face pixel byte size overflows",
"face pixel payload exceeds the configured limit",
"face pixel payload exceeds addressable memory");
}
[[nodiscard]] pp::foundation::Result<std::size_t> alpha8_byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
{
return byte_size(
width,
height,
alpha8_components,
"selection mask dimensions overflow",
"selection mask byte size overflows",
"selection mask payload exceeds the configured limit",
"selection mask payload exceeds addressable memory");
}
[[nodiscard]] pp::foundation::Status validate_face_pixels(
LayerFacePixels pixels,
std::uint32_t document_width,
@@ -168,6 +202,36 @@ namespace {
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_selection_mask(
SelectionMask mask,
std::uint32_t document_width,
std::uint32_t document_height) noexcept
{
if (mask.face_index >= cube_face_count) {
return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
}
if (mask.width == 0 || mask.height == 0) {
return pp::foundation::Status::invalid_argument("selection mask dimensions must be greater than zero");
}
if (mask.x > document_width || mask.width > document_width - mask.x
|| mask.y > document_height || mask.height > document_height - mask.y) {
return pp::foundation::Status::out_of_range("selection mask rectangle is outside the document");
}
const auto expected_bytes = alpha8_byte_size(mask.width, mask.height);
if (!expected_bytes) {
return expected_bytes.status();
}
if (mask.alpha8.size() != expected_bytes.value()) {
return pp::foundation::Status::invalid_argument("selection mask byte size does not match dimensions");
}
return pp::foundation::Status::success();
}
}
pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
@@ -272,6 +336,14 @@ pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(Docu
document.frames_.push_back(frame_config);
}
for (const auto& mask : config.selection_masks) {
const auto mask_status = validate_selection_mask(mask, document.width_, document.height_);
if (!mask_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(mask_status);
}
document.selection_masks_.push_back(mask);
}
return pp::foundation::Result<CanvasDocument>::success(document);
}
@@ -325,6 +397,11 @@ std::size_t CanvasDocument::face_pixel_payload_count() const noexcept
return count;
}
std::size_t CanvasDocument::selection_mask_payload_count() const noexcept
{
return selection_masks_.size();
}
std::span<const Layer> CanvasDocument::layers() const noexcept
{
return layers_;
@@ -335,6 +412,11 @@ std::span<const AnimationFrame> CanvasDocument::frames() const noexcept
return frames_;
}
std::span<const SelectionMask> CanvasDocument::selection_masks() const noexcept
{
return selection_masks_;
}
pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view name)
{
if (layers_.size() >= max_layer_count) {
@@ -656,6 +738,47 @@ pp::foundation::Status CanvasDocument::set_layer_frame_face_pixels(
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_selection_mask(SelectionMask mask)
{
const auto mask_status = validate_selection_mask(mask, width_, height_);
if (!mask_status.ok()) {
return mask_status;
}
const auto existing = std::find_if(
selection_masks_.begin(),
selection_masks_.end(),
[face_index = mask.face_index](const SelectionMask& candidate) {
return candidate.face_index == face_index;
});
if (existing == selection_masks_.end()) {
selection_masks_.push_back(std::move(mask));
} else {
*existing = std::move(mask);
}
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::clear_selection_mask(std::uint32_t face_index) noexcept
{
if (face_index >= cube_face_count) {
return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
}
const auto existing = std::find_if(
selection_masks_.begin(),
selection_masks_.end(),
[face_index](const SelectionMask& candidate) {
return candidate.face_index == face_index;
});
if (existing == selection_masks_.end()) {
return pp::foundation::Status::out_of_range("selection mask face is not present");
}
selection_masks_.erase(existing);
return pp::foundation::Status::success();
}
pp::foundation::Result<DocumentHistory> DocumentHistory::create(
CanvasDocument initial_document,
std::size_t max_entries)

View File

@@ -20,6 +20,7 @@ constexpr std::size_t max_document_history_entries = 10000;
constexpr std::size_t max_layer_name_length = 128;
constexpr std::uint32_t cube_face_count = 6;
constexpr std::uint32_t rgba8_components = 4;
constexpr std::uint32_t alpha8_components = 1;
constexpr std::uint64_t max_face_pixel_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
struct DocumentConfig {
@@ -37,6 +38,15 @@ struct LayerFacePixels {
std::vector<std::uint8_t> rgba8;
};
struct SelectionMask {
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::vector<std::uint8_t> alpha8;
};
struct AnimationFrame {
std::uint32_t duration_ms = 100;
std::vector<LayerFacePixels> face_pixels;
@@ -65,6 +75,7 @@ struct DocumentSnapshotConfig {
std::uint32_t height = 0;
std::span<const DocumentLayerConfig> layers;
std::span<const AnimationFrame> frames;
std::span<const SelectionMask> selection_masks;
};
class CanvasDocument {
@@ -79,8 +90,10 @@ public:
[[nodiscard]] std::uint64_t animation_duration_ms() const noexcept;
[[nodiscard]] pp::foundation::Result<std::uint64_t> layer_animation_duration_ms(std::size_t index) const noexcept;
[[nodiscard]] std::size_t face_pixel_payload_count() const noexcept;
[[nodiscard]] std::size_t selection_mask_payload_count() const noexcept;
[[nodiscard]] std::span<const Layer> layers() const noexcept;
[[nodiscard]] std::span<const AnimationFrame> frames() const noexcept;
[[nodiscard]] std::span<const SelectionMask> selection_masks() const noexcept;
[[nodiscard]] pp::foundation::Result<std::size_t> add_layer(std::string_view name);
[[nodiscard]] pp::foundation::Status remove_layer(std::size_t index);
@@ -102,6 +115,8 @@ public:
std::size_t layer_index,
std::size_t frame_index,
LayerFacePixels pixels);
[[nodiscard]] pp::foundation::Status set_selection_mask(SelectionMask mask);
[[nodiscard]] pp::foundation::Status clear_selection_mask(std::uint32_t face_index) noexcept;
private:
std::uint32_t width_ = 0;
@@ -110,6 +125,7 @@ private:
std::size_t active_frame_index_ = 0;
std::vector<Layer> layers_;
std::vector<AnimationFrame> frames_;
std::vector<SelectionMask> selection_masks_;
};
class DocumentHistory {

View File

@@ -79,6 +79,7 @@ namespace {
.height = project.body.summary.height,
.layers = layers,
.frames = frames,
.selection_masks = {},
});
}