Files
panopainter/src/document/document.cpp

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();
}
}