463 lines
14 KiB
C++
463 lines
14 KiB
C++
#include "document/document.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace pp::document {
|
|
|
|
namespace {
|
|
|
|
[[nodiscard]] pp::foundation::Status validate_config(DocumentConfig config) noexcept
|
|
{
|
|
if (config.width == 0 || config.height == 0) {
|
|
return pp::foundation::Status::invalid_argument("document dimensions must be greater than zero");
|
|
}
|
|
|
|
if (config.width > max_canvas_dimension || config.height > max_canvas_dimension) {
|
|
return pp::foundation::Status::out_of_range("document dimensions exceed the configured limit");
|
|
}
|
|
|
|
if (config.layer_count == 0) {
|
|
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
|
|
}
|
|
|
|
if (config.layer_count > max_layer_count) {
|
|
return pp::foundation::Status::out_of_range("document layer count exceeds the configured limit");
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
[[nodiscard]] std::string default_layer_name(std::size_t index)
|
|
{
|
|
return "Layer " + std::to_string(index + 1U);
|
|
}
|
|
|
|
[[nodiscard]] pp::foundation::Status validate_layer_name(std::string_view name) noexcept
|
|
{
|
|
if (name.empty()) {
|
|
return pp::foundation::Status::invalid_argument("layer name must not be empty");
|
|
}
|
|
|
|
if (name.size() > max_layer_name_length) {
|
|
return pp::foundation::Status::out_of_range("layer name length exceeds the configured limit");
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
[[nodiscard]] pp::foundation::Status validate_layer_index(std::size_t index, std::size_t layer_count) noexcept
|
|
{
|
|
if (index >= layer_count) {
|
|
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
[[nodiscard]] pp::foundation::Status validate_frame_index(std::size_t index, std::size_t frame_count) noexcept
|
|
{
|
|
if (index >= frame_count) {
|
|
return pp::foundation::Status::out_of_range("frame index is outside the document");
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
}
|
|
|
|
pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
|
|
{
|
|
const auto status = validate_config(config);
|
|
if (!status.ok()) {
|
|
return pp::foundation::Result<CanvasDocument>::failure(status);
|
|
}
|
|
|
|
CanvasDocument document;
|
|
document.width_ = config.width;
|
|
document.height_ = config.height;
|
|
document.layers_.reserve(config.layer_count);
|
|
for (std::uint32_t i = 0; i < config.layer_count; ++i) {
|
|
document.layers_.push_back(Layer { .name = default_layer_name(i) });
|
|
}
|
|
document.frames_.push_back(AnimationFrame {});
|
|
|
|
return pp::foundation::Result<CanvasDocument>::success(document);
|
|
}
|
|
|
|
std::uint32_t CanvasDocument::width() const noexcept
|
|
{
|
|
return width_;
|
|
}
|
|
|
|
std::uint32_t CanvasDocument::height() const noexcept
|
|
{
|
|
return height_;
|
|
}
|
|
|
|
std::size_t CanvasDocument::active_layer_index() const noexcept
|
|
{
|
|
return active_layer_index_;
|
|
}
|
|
|
|
std::size_t CanvasDocument::active_frame_index() const noexcept
|
|
{
|
|
return active_frame_index_;
|
|
}
|
|
|
|
std::uint64_t CanvasDocument::animation_duration_ms() const noexcept
|
|
{
|
|
std::uint64_t duration = 0;
|
|
for (const auto& frame : frames_) {
|
|
duration += frame.duration_ms;
|
|
}
|
|
return duration;
|
|
}
|
|
|
|
std::span<const Layer> CanvasDocument::layers() const noexcept
|
|
{
|
|
return layers_;
|
|
}
|
|
|
|
std::span<const AnimationFrame> CanvasDocument::frames() const noexcept
|
|
{
|
|
return frames_;
|
|
}
|
|
|
|
pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view name)
|
|
{
|
|
if (layers_.size() >= max_layer_count) {
|
|
return pp::foundation::Result<std::size_t>::failure(
|
|
pp::foundation::Status::out_of_range("document layer count exceeds the configured limit"));
|
|
}
|
|
|
|
Layer layer;
|
|
if (name.empty()) {
|
|
layer.name = default_layer_name(layers_.size());
|
|
} else {
|
|
const auto name_status = validate_layer_name(name);
|
|
if (!name_status.ok()) {
|
|
return pp::foundation::Result<std::size_t>::failure(name_status);
|
|
}
|
|
layer.name = std::string(name);
|
|
}
|
|
layers_.push_back(layer);
|
|
active_layer_index_ = layers_.size() - 1U;
|
|
return pp::foundation::Result<std::size_t>::success(active_layer_index_);
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::remove_layer(std::size_t index)
|
|
{
|
|
if (index >= layers_.size()) {
|
|
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
|
}
|
|
|
|
if (layers_.size() == 1U) {
|
|
return pp::foundation::Status::invalid_argument("document must keep at least one layer");
|
|
}
|
|
|
|
layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(index));
|
|
if (active_layer_index_ >= layers_.size()) {
|
|
active_layer_index_ = layers_.size() - 1U;
|
|
} else if (active_layer_index_ > index) {
|
|
--active_layer_index_;
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::move_layer(std::size_t from, std::size_t to)
|
|
{
|
|
if (from >= layers_.size() || to >= layers_.size()) {
|
|
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
|
}
|
|
|
|
if (from == to) {
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
auto layer = layers_[from];
|
|
layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(from));
|
|
layers_.insert(layers_.begin() + static_cast<std::ptrdiff_t>(to), layer);
|
|
|
|
if (active_layer_index_ == from) {
|
|
active_layer_index_ = to;
|
|
} else if (from < active_layer_index_ && active_layer_index_ <= to) {
|
|
--active_layer_index_;
|
|
} else if (to <= active_layer_index_ && active_layer_index_ < from) {
|
|
++active_layer_index_;
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::set_active_layer(std::size_t index) noexcept
|
|
{
|
|
const auto index_status = validate_layer_index(index, layers_.size());
|
|
if (!index_status.ok()) {
|
|
return index_status;
|
|
}
|
|
|
|
active_layer_index_ = index;
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::rename_layer(std::size_t index, std::string_view name)
|
|
{
|
|
const auto index_status = validate_layer_index(index, layers_.size());
|
|
if (!index_status.ok()) {
|
|
return index_status;
|
|
}
|
|
|
|
const auto name_status = validate_layer_name(name);
|
|
if (!name_status.ok()) {
|
|
return name_status;
|
|
}
|
|
|
|
layers_[index].name = std::string(name);
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::set_layer_visible(std::size_t index, bool visible) noexcept
|
|
{
|
|
const auto index_status = validate_layer_index(index, layers_.size());
|
|
if (!index_status.ok()) {
|
|
return index_status;
|
|
}
|
|
|
|
layers_[index].visible = visible;
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::set_layer_opacity(std::size_t index, float opacity) noexcept
|
|
{
|
|
const auto index_status = validate_layer_index(index, layers_.size());
|
|
if (!index_status.ok()) {
|
|
return index_status;
|
|
}
|
|
|
|
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
|
|
return pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1");
|
|
}
|
|
|
|
layers_[index].opacity = opacity;
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept
|
|
{
|
|
const auto index_status = validate_layer_index(index, layers_.size());
|
|
if (!index_status.ok()) {
|
|
return index_status;
|
|
}
|
|
|
|
switch (blend_mode) {
|
|
case pp::paint::BlendMode::normal:
|
|
case pp::paint::BlendMode::multiply:
|
|
case pp::paint::BlendMode::screen:
|
|
case pp::paint::BlendMode::color_dodge:
|
|
case pp::paint::BlendMode::overlay:
|
|
layers_[index].blend_mode = blend_mode;
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
return pp::foundation::Status::invalid_argument("layer blend mode is not supported");
|
|
}
|
|
|
|
pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t duration_ms)
|
|
{
|
|
if (frames_.size() >= max_frame_count) {
|
|
return pp::foundation::Result<std::size_t>::failure(
|
|
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
|
}
|
|
|
|
if (duration_ms < min_frame_duration_ms) {
|
|
return pp::foundation::Result<std::size_t>::failure(
|
|
pp::foundation::Status::invalid_argument("frame duration must be greater than zero"));
|
|
}
|
|
|
|
frames_.push_back(AnimationFrame { .duration_ms = duration_ms });
|
|
active_frame_index_ = frames_.size() - 1U;
|
|
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
|
|
}
|
|
|
|
pp::foundation::Result<std::size_t> CanvasDocument::duplicate_frame(std::size_t index)
|
|
{
|
|
const auto index_status = validate_frame_index(index, frames_.size());
|
|
if (!index_status.ok()) {
|
|
return pp::foundation::Result<std::size_t>::failure(
|
|
index_status);
|
|
}
|
|
|
|
if (frames_.size() >= max_frame_count) {
|
|
return pp::foundation::Result<std::size_t>::failure(
|
|
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
|
}
|
|
|
|
const auto insert_at = index + 1U;
|
|
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(insert_at), frames_[index]);
|
|
active_frame_index_ = insert_at;
|
|
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::remove_frame(std::size_t index)
|
|
{
|
|
const auto index_status = validate_frame_index(index, frames_.size());
|
|
if (!index_status.ok()) {
|
|
return index_status;
|
|
}
|
|
|
|
if (frames_.size() == 1U) {
|
|
return pp::foundation::Status::invalid_argument("document must keep at least one frame");
|
|
}
|
|
|
|
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(index));
|
|
if (active_frame_index_ >= frames_.size()) {
|
|
active_frame_index_ = frames_.size() - 1U;
|
|
} else if (active_frame_index_ > index) {
|
|
--active_frame_index_;
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::move_frame(std::size_t from, std::size_t to)
|
|
{
|
|
if (from >= frames_.size() || to >= frames_.size()) {
|
|
return pp::foundation::Status::out_of_range("frame index is outside the document");
|
|
}
|
|
|
|
if (from == to) {
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
const auto frame = frames_[from];
|
|
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(from));
|
|
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(to), frame);
|
|
|
|
if (active_frame_index_ == from) {
|
|
active_frame_index_ = to;
|
|
} else if (from < active_frame_index_ && active_frame_index_ <= to) {
|
|
--active_frame_index_;
|
|
} else if (to <= active_frame_index_ && active_frame_index_ < from) {
|
|
++active_frame_index_;
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept
|
|
{
|
|
const auto index_status = validate_frame_index(index, frames_.size());
|
|
if (!index_status.ok()) {
|
|
return index_status;
|
|
}
|
|
|
|
if (duration_ms < min_frame_duration_ms) {
|
|
return pp::foundation::Status::invalid_argument("frame duration must be greater than zero");
|
|
}
|
|
|
|
frames_[index].duration_ms = duration_ms;
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status CanvasDocument::set_active_frame(std::size_t index) noexcept
|
|
{
|
|
const auto index_status = validate_frame_index(index, frames_.size());
|
|
if (!index_status.ok()) {
|
|
return index_status;
|
|
}
|
|
|
|
active_frame_index_ = index;
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Result<DocumentHistory> DocumentHistory::create(
|
|
CanvasDocument initial_document,
|
|
std::size_t max_entries)
|
|
{
|
|
if (max_entries < min_document_history_entries) {
|
|
return pp::foundation::Result<DocumentHistory>::failure(
|
|
pp::foundation::Status::invalid_argument("document history must keep at least two entries"));
|
|
}
|
|
|
|
if (max_entries > max_document_history_entries) {
|
|
return pp::foundation::Result<DocumentHistory>::failure(
|
|
pp::foundation::Status::out_of_range("document history entry limit exceeds the configured limit"));
|
|
}
|
|
|
|
DocumentHistory history;
|
|
history.max_entries_ = max_entries;
|
|
history.entries_.reserve(max_entries);
|
|
history.entries_.push_back(initial_document);
|
|
return pp::foundation::Result<DocumentHistory>::success(history);
|
|
}
|
|
|
|
const CanvasDocument& DocumentHistory::current() const noexcept
|
|
{
|
|
return entries_[current_index_];
|
|
}
|
|
|
|
std::size_t DocumentHistory::size() const noexcept
|
|
{
|
|
return entries_.size();
|
|
}
|
|
|
|
std::size_t DocumentHistory::current_index() const noexcept
|
|
{
|
|
return current_index_;
|
|
}
|
|
|
|
bool DocumentHistory::can_undo() const noexcept
|
|
{
|
|
return current_index_ > 0;
|
|
}
|
|
|
|
bool DocumentHistory::can_redo() const noexcept
|
|
{
|
|
return current_index_ + 1U < entries_.size();
|
|
}
|
|
|
|
pp::foundation::Status DocumentHistory::apply(CanvasDocument next_document)
|
|
{
|
|
if (entries_.empty()) {
|
|
return pp::foundation::Status::invalid_argument("document history is not initialized");
|
|
}
|
|
|
|
if (can_redo()) {
|
|
entries_.erase(entries_.begin() + static_cast<std::ptrdiff_t>(current_index_ + 1U), entries_.end());
|
|
}
|
|
|
|
entries_.push_back(next_document);
|
|
if (entries_.size() > max_entries_) {
|
|
entries_.erase(entries_.begin());
|
|
} else {
|
|
++current_index_;
|
|
}
|
|
|
|
current_index_ = entries_.size() - 1U;
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status DocumentHistory::undo() noexcept
|
|
{
|
|
if (!can_undo()) {
|
|
return pp::foundation::Status::out_of_range("document history has no undo entry");
|
|
}
|
|
|
|
--current_index_;
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
pp::foundation::Status DocumentHistory::redo() noexcept
|
|
{
|
|
if (!can_redo()) {
|
|
return pp::foundation::Status::out_of_range("document history has no redo entry");
|
|
}
|
|
|
|
++current_index_;
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
}
|