#include "document/document.h" #include #include 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::create(DocumentConfig config) { const auto status = validate_config(config); if (!status.ok()) { return pp::foundation::Result::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::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 CanvasDocument::layers() const noexcept { return layers_; } std::span CanvasDocument::frames() const noexcept { return frames_; } pp::foundation::Result CanvasDocument::add_layer(std::string_view name) { if (layers_.size() >= max_layer_count) { return pp::foundation::Result::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::failure(name_status); } layer.name = std::string(name); } layers_.push_back(layer); active_layer_index_ = layers_.size() - 1U; return pp::foundation::Result::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(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(from)); layers_.insert(layers_.begin() + static_cast(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 CanvasDocument::add_frame(std::uint32_t duration_ms) { if (frames_.size() >= max_frame_count) { return pp::foundation::Result::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::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::success(active_frame_index_); } pp::foundation::Result 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::failure( index_status); } if (frames_.size() >= max_frame_count) { return pp::foundation::Result::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(insert_at), frames_[index]); active_frame_index_ = insert_at; return pp::foundation::Result::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(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(from)); frames_.insert(frames_.begin() + static_cast(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::create( CanvasDocument initial_document, std::size_t max_entries) { if (max_entries < min_document_history_entries) { return pp::foundation::Result::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::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::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(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(); } }