Extract layer operation planning
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -10,18 +12,79 @@
|
||||
namespace pp::app {
|
||||
|
||||
inline constexpr std::size_t document_layer_name_max_length = 128;
|
||||
inline constexpr int document_layer_legacy_blend_mode_count = 5;
|
||||
|
||||
enum class DocumentLayerRenameAction {
|
||||
no_op_same_name,
|
||||
rename_and_record_undo,
|
||||
};
|
||||
|
||||
enum class DocumentLayerOperation {
|
||||
add,
|
||||
duplicate,
|
||||
select,
|
||||
reorder,
|
||||
remove,
|
||||
set_opacity,
|
||||
set_visibility,
|
||||
set_alpha_lock,
|
||||
set_blend_mode,
|
||||
set_highlight,
|
||||
};
|
||||
|
||||
struct DocumentLayerRenamePlan {
|
||||
std::string old_name;
|
||||
std::string new_name;
|
||||
DocumentLayerRenameAction action = DocumentLayerRenameAction::no_op_same_name;
|
||||
};
|
||||
|
||||
struct DocumentLayerOperationPlan {
|
||||
DocumentLayerOperation operation = DocumentLayerOperation::select;
|
||||
int index = 0;
|
||||
int from_index = 0;
|
||||
int to_index = 0;
|
||||
int insert_index = 0;
|
||||
int source_index = 0;
|
||||
std::string name;
|
||||
float opacity = 1.0F;
|
||||
bool flag = false;
|
||||
int blend_mode = 0;
|
||||
bool mutates_document = false;
|
||||
bool marks_unsaved = false;
|
||||
bool reloads_animation_layers = false;
|
||||
bool updates_title = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_layer_index(
|
||||
int layer_count,
|
||||
int index) noexcept
|
||||
{
|
||||
if (layer_count <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
|
||||
}
|
||||
|
||||
if (index < 0 || index >= layer_count) {
|
||||
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_layer_insert_index(
|
||||
int layer_count,
|
||||
int index) noexcept
|
||||
{
|
||||
if (layer_count < 0) {
|
||||
return pp::foundation::Status::invalid_argument("layer count must not be negative");
|
||||
}
|
||||
|
||||
if (index < 0 || index > layer_count) {
|
||||
return pp::foundation::Status::out_of_range("layer insert index is outside the document");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerRenamePlan> plan_document_layer_rename(
|
||||
std::string_view old_name,
|
||||
std::string_view requested_name)
|
||||
@@ -45,4 +108,224 @@ struct DocumentLayerRenamePlan {
|
||||
return pp::foundation::Result<DocumentLayerRenamePlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_add(
|
||||
int layer_count,
|
||||
int insert_index,
|
||||
std::string_view name)
|
||||
{
|
||||
const auto index_status = validate_layer_insert_index(layer_count, insert_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
const auto rename = plan_document_layer_rename({}, name);
|
||||
if (!rename) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(rename.status());
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::add;
|
||||
plan.insert_index = insert_index;
|
||||
plan.name = std::string(name);
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_duplicate(
|
||||
int layer_count,
|
||||
int source_index)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, source_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::duplicate;
|
||||
plan.source_index = source_index;
|
||||
plan.insert_index = source_index + 1;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_select(
|
||||
int layer_count,
|
||||
int index)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::select;
|
||||
plan.index = index;
|
||||
plan.reloads_animation_layers = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_reorder(
|
||||
int layer_count,
|
||||
int from_index,
|
||||
int to_index)
|
||||
{
|
||||
auto index_status = validate_layer_index(layer_count, from_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
index_status = validate_layer_index(layer_count, to_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::reorder;
|
||||
plan.from_index = from_index;
|
||||
plan.to_index = to_index;
|
||||
plan.mutates_document = from_index != to_index;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
plan.reloads_animation_layers = plan.mutates_document;
|
||||
plan.updates_title = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_remove(
|
||||
int layer_count,
|
||||
int index)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (layer_count <= 1) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("document must keep at least one layer"));
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::remove;
|
||||
plan.index = index;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_opacity(
|
||||
int layer_count,
|
||||
int index,
|
||||
float opacity)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_opacity;
|
||||
plan.index = index;
|
||||
plan.opacity = opacity;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_visibility(
|
||||
int layer_count,
|
||||
int index,
|
||||
bool visible)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_visibility;
|
||||
plan.index = index;
|
||||
plan.flag = visible;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_alpha_lock(
|
||||
int layer_count,
|
||||
int index,
|
||||
bool locked)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_alpha_lock;
|
||||
plan.index = index;
|
||||
plan.flag = locked;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_blend_mode(
|
||||
int layer_count,
|
||||
int index,
|
||||
int blend_mode)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (blend_mode < 0 || blend_mode >= document_layer_legacy_blend_mode_count) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_blend_mode;
|
||||
plan.index = index;
|
||||
plan.blend_mode = blend_mode;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_highlight(
|
||||
int layer_count,
|
||||
int index,
|
||||
bool highlight)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_highlight;
|
||||
plan.index = index;
|
||||
plan.flag = highlight;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "node_dialog_picker.h"
|
||||
#include "node_panel_floating.h"
|
||||
#include "app_core/app_preferences.h"
|
||||
#include "app_core/document_layer.h"
|
||||
#include "app_core/app_status.h"
|
||||
#include "settings.h"
|
||||
#include "serializer.h"
|
||||
@@ -182,17 +183,30 @@ void App::init_sidebar()
|
||||
};
|
||||
|
||||
layers->on_layer_add = [this](Node*, std::shared_ptr<class Layer> layer, int index) {
|
||||
Canvas::I->layer_add(layers->m_layers.back()->m_label_text.c_str(), layer, index);
|
||||
Canvas::I->m_unsaved = true;
|
||||
const auto plan = pp::app::plan_document_layer_add(
|
||||
static_cast<int>(Canvas::I->m_layers.size()),
|
||||
index,
|
||||
layers->m_layers.back()->m_label_text);
|
||||
if (!plan)
|
||||
return;
|
||||
Canvas::I->layer_add(plan.value().name, layer, plan.value().insert_index);
|
||||
Canvas::I->m_unsaved = plan.value().marks_unsaved;
|
||||
Canvas::I->anim_update();
|
||||
animation->load_layers();
|
||||
title_update();
|
||||
if (plan.value().reloads_animation_layers)
|
||||
animation->load_layers();
|
||||
if (plan.value().updates_title)
|
||||
title_update();
|
||||
};
|
||||
|
||||
layers->on_layer_duplicate = [this](Node*, int source_index) {
|
||||
Canvas::I->layer_add(layers->m_layers.back()->m_label_text.c_str(), nullptr, source_index + 1);
|
||||
auto& dst = Canvas::I->m_layers[source_index + 1];
|
||||
auto& src = Canvas::I->m_layers[source_index];
|
||||
const auto plan = pp::app::plan_document_layer_duplicate(
|
||||
static_cast<int>(Canvas::I->m_layers.size()),
|
||||
source_index);
|
||||
if (!plan)
|
||||
return;
|
||||
Canvas::I->layer_add(layers->m_layers.back()->m_label_text.c_str(), nullptr, plan.value().insert_index);
|
||||
auto& dst = Canvas::I->m_layers[plan.value().insert_index];
|
||||
auto& src = Canvas::I->m_layers[plan.value().source_index];
|
||||
for (int i = 1; i < src->frames_count(); i++)
|
||||
dst->add_frame();
|
||||
Canvas::I->anim_update();
|
||||
@@ -217,57 +231,115 @@ void App::init_sidebar()
|
||||
dst->m_opacity = src->m_opacity;
|
||||
dst->m_blend_mode = src->m_blend_mode;
|
||||
dst->m_alpha_locked = src->m_alpha_locked;
|
||||
Canvas::I->m_unsaved = true;
|
||||
animation->load_layers();
|
||||
title_update();
|
||||
Canvas::I->m_unsaved = plan.value().marks_unsaved;
|
||||
if (plan.value().reloads_animation_layers)
|
||||
animation->load_layers();
|
||||
if (plan.value().updates_title)
|
||||
title_update();
|
||||
};
|
||||
|
||||
layers->on_layer_change = [this](Node*, int old_idx, int new_idx) {
|
||||
canvas->m_canvas->m_current_layer_idx = new_idx;
|
||||
animation->load_layers();
|
||||
const auto plan = pp::app::plan_document_layer_select(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
new_idx);
|
||||
if (!plan)
|
||||
return;
|
||||
canvas->m_canvas->m_current_layer_idx = plan.value().index;
|
||||
if (plan.value().reloads_animation_layers)
|
||||
animation->load_layers();
|
||||
};
|
||||
|
||||
layers->on_layer_order = [this](Node*, int old_idx, int new_idx) {
|
||||
canvas->m_canvas->layer_order(old_idx, new_idx);
|
||||
canvas->m_canvas->m_unsaved = true;
|
||||
animation->load_layers();
|
||||
title_update();
|
||||
const auto plan = pp::app::plan_document_layer_reorder(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
old_idx,
|
||||
new_idx);
|
||||
if (!plan || !plan.value().mutates_document)
|
||||
return;
|
||||
canvas->m_canvas->layer_order(plan.value().from_index, plan.value().to_index);
|
||||
canvas->m_canvas->m_unsaved = plan.value().marks_unsaved;
|
||||
if (plan.value().reloads_animation_layers)
|
||||
animation->load_layers();
|
||||
if (plan.value().updates_title)
|
||||
title_update();
|
||||
};
|
||||
|
||||
layers->on_layer_delete = [this](Node*, int idx) {
|
||||
canvas->m_canvas->layer_remove(idx);
|
||||
canvas->m_canvas->m_unsaved = true;
|
||||
animation->load_layers();
|
||||
title_update();
|
||||
const auto plan = pp::app::plan_document_layer_remove(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx);
|
||||
if (!plan)
|
||||
return;
|
||||
canvas->m_canvas->layer_remove(plan.value().index);
|
||||
canvas->m_canvas->m_unsaved = plan.value().marks_unsaved;
|
||||
if (plan.value().reloads_animation_layers)
|
||||
animation->load_layers();
|
||||
if (plan.value().updates_title)
|
||||
title_update();
|
||||
};
|
||||
|
||||
layers->on_layer_opacity_changed = [this](Node*, int idx, float value) {
|
||||
canvas->m_canvas->m_layers[idx]->m_opacity = value;
|
||||
canvas->m_canvas->m_unsaved = true;
|
||||
title_update();
|
||||
const auto plan = pp::app::plan_document_layer_opacity(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
value);
|
||||
if (!plan)
|
||||
return;
|
||||
canvas->m_canvas->m_layers[plan.value().index]->m_opacity = plan.value().opacity;
|
||||
canvas->m_canvas->m_unsaved = plan.value().marks_unsaved;
|
||||
if (plan.value().updates_title)
|
||||
title_update();
|
||||
};
|
||||
|
||||
layers->on_layer_visibility_changed = [this](Node*, int idx, bool visible) {
|
||||
canvas->m_canvas->m_layers[idx]->m_visible = visible;
|
||||
canvas->m_canvas->m_unsaved = true;
|
||||
animation->load_layers();
|
||||
title_update();
|
||||
const auto plan = pp::app::plan_document_layer_visibility(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
visible);
|
||||
if (!plan)
|
||||
return;
|
||||
canvas->m_canvas->m_layers[plan.value().index]->m_visible = plan.value().flag;
|
||||
canvas->m_canvas->m_unsaved = plan.value().marks_unsaved;
|
||||
if (plan.value().reloads_animation_layers)
|
||||
animation->load_layers();
|
||||
if (plan.value().updates_title)
|
||||
title_update();
|
||||
};
|
||||
|
||||
layers->on_layer_alpha_lock_changed = [this](Node*, int idx, bool locked) {
|
||||
canvas->m_canvas->m_layers[idx]->m_alpha_locked = locked;
|
||||
canvas->m_canvas->m_unsaved = true;
|
||||
title_update();
|
||||
const auto plan = pp::app::plan_document_layer_alpha_lock(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
locked);
|
||||
if (!plan)
|
||||
return;
|
||||
canvas->m_canvas->m_layers[plan.value().index]->m_alpha_locked = plan.value().flag;
|
||||
canvas->m_canvas->m_unsaved = plan.value().marks_unsaved;
|
||||
if (plan.value().updates_title)
|
||||
title_update();
|
||||
};
|
||||
|
||||
layers->on_layer_blend_mode_changed = [this](Node*, int idx, int mode) {
|
||||
canvas->m_canvas->m_layers[idx]->m_blend_mode = mode;
|
||||
canvas->m_canvas->m_unsaved = true;
|
||||
title_update();
|
||||
const auto plan = pp::app::plan_document_layer_blend_mode(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
mode);
|
||||
if (!plan)
|
||||
return;
|
||||
canvas->m_canvas->m_layers[plan.value().index]->m_blend_mode = plan.value().blend_mode;
|
||||
canvas->m_canvas->m_unsaved = plan.value().marks_unsaved;
|
||||
if (plan.value().updates_title)
|
||||
title_update();
|
||||
};
|
||||
|
||||
layers->on_layer_highlight_changed = [this](Node*, int idx, bool highlight) {
|
||||
canvas->m_canvas->m_layers[idx]->m_hightlight = highlight;
|
||||
const auto plan = pp::app::plan_document_layer_highlight(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
highlight);
|
||||
if (!plan)
|
||||
return;
|
||||
canvas->m_canvas->m_layers[plan.value().index]->m_hightlight = plan.value().flag;
|
||||
};
|
||||
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-stroke"))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user