863 lines
30 KiB
C++
863 lines
30 KiB
C++
#pragma once
|
|
|
|
#include "foundation/result.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <string_view>
|
|
|
|
namespace pp::app {
|
|
|
|
enum class BrushUiTextureSlot {
|
|
tip,
|
|
pattern,
|
|
dual,
|
|
};
|
|
|
|
enum class BrushUiOperation {
|
|
set_tip_color,
|
|
set_texture,
|
|
replace_brush_from_preset,
|
|
stroke_settings_changed,
|
|
};
|
|
|
|
enum class BrushTextureListOperation {
|
|
add_texture,
|
|
remove_texture,
|
|
move_texture,
|
|
};
|
|
|
|
enum class BrushPresetListOperation {
|
|
add_current_brush,
|
|
remove_preset,
|
|
move_preset,
|
|
select_preset,
|
|
clear_presets,
|
|
};
|
|
|
|
enum class BrushStrokeControlOperation {
|
|
set_float,
|
|
set_bool,
|
|
set_blend_mode,
|
|
reset_tip_aspect,
|
|
reset_default_brush,
|
|
};
|
|
|
|
enum class BrushStrokeFloatSetting {
|
|
tip_size,
|
|
tip_spacing,
|
|
tip_flow,
|
|
tip_opacity,
|
|
tip_angle,
|
|
tip_angle_smooth,
|
|
tip_mix,
|
|
tip_wet,
|
|
tip_noise,
|
|
tip_hue,
|
|
tip_saturation,
|
|
tip_value,
|
|
jitter_scale,
|
|
jitter_angle,
|
|
jitter_scatter,
|
|
jitter_flow,
|
|
jitter_opacity,
|
|
jitter_hue,
|
|
jitter_saturation,
|
|
jitter_value,
|
|
jitter_aspect,
|
|
dual_size,
|
|
dual_spacing,
|
|
dual_scatter,
|
|
tip_aspect,
|
|
dual_opacity,
|
|
dual_flow,
|
|
dual_rotate,
|
|
pattern_scale,
|
|
pattern_brightness,
|
|
pattern_contrast,
|
|
pattern_depth,
|
|
};
|
|
|
|
enum class BrushStrokeBoolSetting {
|
|
tip_angle_init,
|
|
tip_angle_follow,
|
|
tip_flow_pressure,
|
|
tip_opacity_pressure,
|
|
tip_size_pressure,
|
|
jitter_scatter_both_axis,
|
|
jitter_aspect_both_axis,
|
|
jitter_hsv_each_sample,
|
|
tip_invert,
|
|
tip_flip_x,
|
|
tip_flip_y,
|
|
pattern_enabled,
|
|
dual_enabled,
|
|
dual_scatter_both_axis,
|
|
dual_invert,
|
|
dual_flip_x,
|
|
dual_flip_y,
|
|
dual_random_flip,
|
|
tip_random_flip_x,
|
|
tip_random_flip_y,
|
|
pattern_each_sample,
|
|
pattern_invert,
|
|
pattern_flip_x,
|
|
pattern_flip_y,
|
|
pattern_random_offset,
|
|
};
|
|
|
|
enum class BrushStrokeBlendSetting {
|
|
tip,
|
|
dual,
|
|
pattern,
|
|
};
|
|
|
|
struct BrushUiPlan {
|
|
BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
|
|
BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
|
|
std::string path;
|
|
std::string thumbnail_path;
|
|
float r = 0.0F;
|
|
float g = 0.0F;
|
|
float b = 0.0F;
|
|
float a = 1.0F;
|
|
bool mutates_brush = false;
|
|
bool preserves_existing_color = false;
|
|
bool loads_brush_resources = false;
|
|
bool update_color_ui = false;
|
|
bool update_brush_ui = false;
|
|
};
|
|
|
|
struct BrushTextureListPlan {
|
|
BrushTextureListOperation operation = BrushTextureListOperation::add_texture;
|
|
int item_count = 0;
|
|
int current_index = -1;
|
|
int target_index = -1;
|
|
int move_offset = 0;
|
|
std::string source_path;
|
|
std::string high_path;
|
|
std::string thumbnail_path;
|
|
std::string brush_name;
|
|
bool user_texture = false;
|
|
bool deletes_texture_files = false;
|
|
bool saves_list = false;
|
|
bool notifies_selection = false;
|
|
bool converts_brush_alpha = false;
|
|
bool no_op = false;
|
|
};
|
|
|
|
struct BrushPresetListPlan {
|
|
BrushPresetListOperation operation = BrushPresetListOperation::select_preset;
|
|
int item_count = 0;
|
|
int current_index = -1;
|
|
int target_index = -1;
|
|
int move_offset = 0;
|
|
bool saves_list = false;
|
|
bool updates_empty_notification = false;
|
|
bool selects_target = false;
|
|
bool clears_selection = false;
|
|
bool notifies_brush_changed = false;
|
|
bool no_op = false;
|
|
};
|
|
|
|
struct BrushStrokeControlPlan {
|
|
BrushStrokeControlOperation operation = BrushStrokeControlOperation::set_float;
|
|
BrushStrokeFloatSetting float_setting = BrushStrokeFloatSetting::tip_size;
|
|
BrushStrokeBoolSetting bool_setting = BrushStrokeBoolSetting::tip_angle_init;
|
|
BrushStrokeBlendSetting blend_setting = BrushStrokeBlendSetting::tip;
|
|
float float_value = 0.0F;
|
|
bool bool_value = false;
|
|
int blend_mode = 0;
|
|
bool mutates_brush = false;
|
|
bool updates_controls = false;
|
|
bool refreshes_preview = false;
|
|
bool notifies_stroke_change = false;
|
|
};
|
|
|
|
class BrushUiServices {
|
|
public:
|
|
virtual ~BrushUiServices() = default;
|
|
|
|
virtual void set_tip_color(float r, float g, float b, float a) = 0;
|
|
virtual void set_texture(BrushUiTextureSlot slot, std::string_view path, std::string_view thumbnail_path) = 0;
|
|
virtual void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) = 0;
|
|
virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0;
|
|
};
|
|
|
|
class BrushTextureListServices {
|
|
public:
|
|
virtual ~BrushTextureListServices() = default;
|
|
|
|
virtual pp::foundation::Status add_texture_from_source(
|
|
std::string_view source_path,
|
|
std::string_view high_path,
|
|
std::string_view thumbnail_path,
|
|
std::string_view brush_name,
|
|
bool converts_brush_alpha) = 0;
|
|
virtual void remove_texture(int index, bool delete_texture_files) = 0;
|
|
virtual void move_texture(int from_index, int to_index) = 0;
|
|
virtual void select_texture(int index) = 0;
|
|
virtual void save_texture_list() = 0;
|
|
};
|
|
|
|
class BrushPresetListServices {
|
|
public:
|
|
virtual ~BrushPresetListServices() = default;
|
|
|
|
virtual pp::foundation::Status add_current_brush_preset(int target_index) = 0;
|
|
virtual void remove_brush_preset(
|
|
int current_index,
|
|
int target_index,
|
|
bool selects_target,
|
|
bool clears_selection) = 0;
|
|
virtual void move_brush_preset(int from_index, int to_index) = 0;
|
|
virtual void select_brush_preset(int index, bool notify_brush_changed) = 0;
|
|
virtual void clear_brush_presets(bool clears_selection) = 0;
|
|
virtual void update_preset_empty_notification() = 0;
|
|
virtual void save_preset_list() = 0;
|
|
};
|
|
|
|
class BrushStrokeControlServices {
|
|
public:
|
|
virtual ~BrushStrokeControlServices() = default;
|
|
|
|
virtual void set_float_setting(BrushStrokeFloatSetting setting, float value) = 0;
|
|
virtual void set_bool_setting(BrushStrokeBoolSetting setting, bool value) = 0;
|
|
virtual void set_blend_mode(BrushStrokeBlendSetting setting, int blend_mode) = 0;
|
|
virtual void reset_tip_aspect(float value) = 0;
|
|
virtual void reset_default_brush() = 0;
|
|
virtual void update_stroke_controls() = 0;
|
|
virtual void refresh_stroke_preview() = 0;
|
|
virtual void notify_stroke_changed() = 0;
|
|
};
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<std::string_view> brush_texture_source_stem(
|
|
std::string_view source_path) noexcept
|
|
{
|
|
const auto slash = source_path.find_last_of("/\\");
|
|
const auto name_begin = slash == std::string_view::npos ? 0U : slash + 1U;
|
|
if (name_begin >= source_path.size()) {
|
|
return pp::foundation::Result<std::string_view>::failure(
|
|
pp::foundation::Status::invalid_argument("brush texture source path must contain a file name"));
|
|
}
|
|
|
|
const auto dot = source_path.find_last_of('.');
|
|
if (dot == std::string_view::npos || dot <= name_begin || dot + 1U >= source_path.size()) {
|
|
return pp::foundation::Result<std::string_view>::failure(
|
|
pp::foundation::Status::invalid_argument("brush texture source path must include a file extension"));
|
|
}
|
|
|
|
return pp::foundation::Result<std::string_view>::success(source_path.substr(name_begin, dot - name_begin));
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept
|
|
{
|
|
if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
|
|
return pp::foundation::Status::out_of_range("brush color channels must be finite and within 0..1");
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_float(float value) noexcept
|
|
{
|
|
if (!std::isfinite(value)) {
|
|
return pp::foundation::Status::invalid_argument("brush stroke float setting must be finite");
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_blend_mode(int blend_mode) noexcept
|
|
{
|
|
if (blend_mode < 0 || blend_mode > 63) {
|
|
return pp::foundation::Status::out_of_range("brush stroke blend mode must be within 0..63");
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_color(
|
|
float r,
|
|
float g,
|
|
float b,
|
|
float a)
|
|
{
|
|
for (const auto value : { r, g, b, a }) {
|
|
const auto channel_status = validate_brush_ui_color_channel(value);
|
|
if (!channel_status.ok()) {
|
|
return pp::foundation::Result<BrushUiPlan>::failure(channel_status);
|
|
}
|
|
}
|
|
|
|
BrushUiPlan plan;
|
|
plan.operation = BrushUiOperation::set_tip_color;
|
|
plan.r = r;
|
|
plan.g = g;
|
|
plan.b = b;
|
|
plan.a = a;
|
|
plan.mutates_brush = true;
|
|
plan.update_color_ui = true;
|
|
return pp::foundation::Result<BrushUiPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_texture(
|
|
BrushUiTextureSlot slot,
|
|
std::string_view path,
|
|
std::string_view thumbnail_path)
|
|
{
|
|
if (path.empty()) {
|
|
return pp::foundation::Result<BrushUiPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush texture path must not be empty"));
|
|
}
|
|
|
|
BrushUiPlan plan;
|
|
plan.operation = BrushUiOperation::set_texture;
|
|
plan.texture_slot = slot;
|
|
plan.path = std::string(path);
|
|
plan.thumbnail_path = std::string(thumbnail_path);
|
|
plan.mutates_brush = true;
|
|
plan.loads_brush_resources = true;
|
|
plan.update_color_ui = true;
|
|
plan.update_brush_ui = true;
|
|
return pp::foundation::Result<BrushUiPlan>::success(std::move(plan));
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_preset_replace(bool has_preset_brush)
|
|
{
|
|
if (!has_preset_brush) {
|
|
return pp::foundation::Result<BrushUiPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("preset brush must be available"));
|
|
}
|
|
|
|
BrushUiPlan plan;
|
|
plan.operation = BrushUiOperation::replace_brush_from_preset;
|
|
plan.mutates_brush = true;
|
|
plan.preserves_existing_color = true;
|
|
plan.loads_brush_resources = true;
|
|
plan.update_color_ui = true;
|
|
plan.update_brush_ui = true;
|
|
return pp::foundation::Result<BrushUiPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline constexpr BrushUiPlan plan_brush_ui_stroke_settings_changed() noexcept
|
|
{
|
|
BrushUiPlan plan;
|
|
plan.operation = BrushUiOperation::stroke_settings_changed;
|
|
plan.mutates_brush = true;
|
|
plan.update_color_ui = true;
|
|
plan.update_brush_ui = true;
|
|
return plan;
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_float_setting(
|
|
BrushStrokeFloatSetting setting,
|
|
float value)
|
|
{
|
|
const auto status = validate_brush_stroke_float(value);
|
|
if (!status.ok()) {
|
|
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
|
|
}
|
|
|
|
BrushStrokeControlPlan plan;
|
|
plan.operation = BrushStrokeControlOperation::set_float;
|
|
plan.float_setting = setting;
|
|
plan.float_value = value;
|
|
plan.mutates_brush = true;
|
|
plan.refreshes_preview = true;
|
|
plan.notifies_stroke_change = true;
|
|
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_stroke_bool_setting(
|
|
BrushStrokeBoolSetting setting,
|
|
bool value) noexcept
|
|
{
|
|
BrushStrokeControlPlan plan;
|
|
plan.operation = BrushStrokeControlOperation::set_bool;
|
|
plan.bool_setting = setting;
|
|
plan.bool_value = value;
|
|
plan.mutates_brush = true;
|
|
plan.refreshes_preview = true;
|
|
plan.notifies_stroke_change = true;
|
|
return plan;
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_blend_mode(
|
|
BrushStrokeBlendSetting setting,
|
|
int blend_mode)
|
|
{
|
|
const auto status = validate_brush_stroke_blend_mode(blend_mode);
|
|
if (!status.ok()) {
|
|
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
|
|
}
|
|
|
|
BrushStrokeControlPlan plan;
|
|
plan.operation = BrushStrokeControlOperation::set_blend_mode;
|
|
plan.blend_setting = setting;
|
|
plan.blend_mode = blend_mode;
|
|
plan.mutates_brush = true;
|
|
plan.refreshes_preview = true;
|
|
plan.notifies_stroke_change = true;
|
|
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_tip_aspect_reset(float value = 0.5F) noexcept
|
|
{
|
|
BrushStrokeControlPlan plan;
|
|
plan.operation = BrushStrokeControlOperation::reset_tip_aspect;
|
|
plan.float_setting = BrushStrokeFloatSetting::tip_aspect;
|
|
plan.float_value = value;
|
|
plan.mutates_brush = true;
|
|
plan.refreshes_preview = true;
|
|
plan.notifies_stroke_change = true;
|
|
return plan;
|
|
}
|
|
|
|
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_default_settings_reset() noexcept
|
|
{
|
|
BrushStrokeControlPlan plan;
|
|
plan.operation = BrushStrokeControlOperation::reset_default_brush;
|
|
plan.mutates_brush = true;
|
|
plan.updates_controls = true;
|
|
plan.refreshes_preview = true;
|
|
plan.notifies_stroke_change = true;
|
|
return plan;
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_add(
|
|
std::string_view directory_name,
|
|
std::string_view data_path,
|
|
std::string_view source_path)
|
|
{
|
|
if (directory_name.empty()) {
|
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush texture directory must not be empty"));
|
|
}
|
|
if (data_path.empty()) {
|
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush texture data path must not be empty"));
|
|
}
|
|
|
|
const auto stem = brush_texture_source_stem(source_path);
|
|
if (!stem) {
|
|
return pp::foundation::Result<BrushTextureListPlan>::failure(stem.status());
|
|
}
|
|
|
|
BrushTextureListPlan plan;
|
|
plan.operation = BrushTextureListOperation::add_texture;
|
|
plan.source_path = std::string(source_path);
|
|
plan.brush_name = std::string(stem.value());
|
|
plan.high_path = std::string(data_path) + "/" + std::string(directory_name) + "/" + plan.brush_name + ".png";
|
|
plan.thumbnail_path = std::string(data_path) + "/" + std::string(directory_name) + "/thumbs/"
|
|
+ plan.brush_name + ".png";
|
|
plan.user_texture = true;
|
|
plan.saves_list = true;
|
|
plan.converts_brush_alpha = directory_name == "brushes";
|
|
return pp::foundation::Result<BrushTextureListPlan>::success(std::move(plan));
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_remove(
|
|
int item_count,
|
|
int current_index,
|
|
bool current_is_user_texture)
|
|
{
|
|
if (item_count <= 0) {
|
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush texture list must contain an item to remove"));
|
|
}
|
|
if (current_index < 0 || current_index >= item_count) {
|
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
|
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
|
|
}
|
|
|
|
BrushTextureListPlan plan;
|
|
plan.operation = BrushTextureListOperation::remove_texture;
|
|
plan.item_count = item_count;
|
|
plan.current_index = current_index;
|
|
plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
|
|
plan.user_texture = current_is_user_texture;
|
|
plan.deletes_texture_files = current_is_user_texture;
|
|
plan.saves_list = true;
|
|
plan.notifies_selection = plan.target_index >= 0;
|
|
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_move(
|
|
int item_count,
|
|
int current_index,
|
|
int offset)
|
|
{
|
|
if (item_count <= 0) {
|
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush texture list must contain an item to move"));
|
|
}
|
|
if (current_index < 0 || current_index >= item_count) {
|
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
|
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
|
|
}
|
|
if (offset == 0) {
|
|
return pp::foundation::Result<BrushTextureListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush texture move offset must not be zero"));
|
|
}
|
|
|
|
BrushTextureListPlan plan;
|
|
plan.operation = BrushTextureListOperation::move_texture;
|
|
plan.item_count = item_count;
|
|
plan.current_index = current_index;
|
|
plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
|
|
plan.move_offset = offset;
|
|
plan.saves_list = true;
|
|
plan.no_op = plan.target_index == current_index;
|
|
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_add(
|
|
int item_count,
|
|
bool has_current_brush)
|
|
{
|
|
if (item_count < 0) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::out_of_range("brush preset item count must not be negative"));
|
|
}
|
|
if (!has_current_brush) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("current brush must be available to add a preset"));
|
|
}
|
|
|
|
BrushPresetListPlan plan;
|
|
plan.operation = BrushPresetListOperation::add_current_brush;
|
|
plan.item_count = item_count;
|
|
plan.target_index = item_count;
|
|
plan.saves_list = true;
|
|
plan.updates_empty_notification = true;
|
|
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_remove(
|
|
int item_count,
|
|
int current_index)
|
|
{
|
|
if (item_count <= 0) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush preset list must contain an item to remove"));
|
|
}
|
|
if (current_index < 0 || current_index >= item_count) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::out_of_range("selected brush preset index is outside the list"));
|
|
}
|
|
|
|
BrushPresetListPlan plan;
|
|
plan.operation = BrushPresetListOperation::remove_preset;
|
|
plan.item_count = item_count;
|
|
plan.current_index = current_index;
|
|
plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
|
|
plan.saves_list = true;
|
|
plan.updates_empty_notification = true;
|
|
plan.selects_target = plan.target_index >= 0;
|
|
plan.clears_selection = plan.target_index < 0;
|
|
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_move(
|
|
int item_count,
|
|
int current_index,
|
|
int offset)
|
|
{
|
|
if (item_count <= 0) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush preset list must contain an item to move"));
|
|
}
|
|
if (current_index < 0 || current_index >= item_count) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::out_of_range("selected brush preset index is outside the list"));
|
|
}
|
|
if (offset == 0) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush preset move offset must not be zero"));
|
|
}
|
|
|
|
BrushPresetListPlan plan;
|
|
plan.operation = BrushPresetListOperation::move_preset;
|
|
plan.item_count = item_count;
|
|
plan.current_index = current_index;
|
|
plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
|
|
plan.move_offset = offset;
|
|
plan.saves_list = true;
|
|
plan.no_op = plan.target_index == current_index;
|
|
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_select(
|
|
int item_count,
|
|
int index)
|
|
{
|
|
if (item_count <= 0) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::invalid_argument("brush preset list must contain an item to select"));
|
|
}
|
|
if (index < 0 || index >= item_count) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::out_of_range("selected brush preset index is outside the list"));
|
|
}
|
|
|
|
BrushPresetListPlan plan;
|
|
plan.operation = BrushPresetListOperation::select_preset;
|
|
plan.item_count = item_count;
|
|
plan.current_index = index;
|
|
plan.target_index = index;
|
|
plan.selects_target = true;
|
|
plan.notifies_brush_changed = true;
|
|
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_clear(int item_count)
|
|
{
|
|
if (item_count < 0) {
|
|
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
|
pp::foundation::Status::out_of_range("brush preset item count must not be negative"));
|
|
}
|
|
|
|
BrushPresetListPlan plan;
|
|
plan.operation = BrushPresetListOperation::clear_presets;
|
|
plan.item_count = item_count;
|
|
plan.saves_list = true;
|
|
plan.updates_empty_notification = true;
|
|
plan.clears_selection = true;
|
|
plan.no_op = item_count == 0;
|
|
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
|
|
const BrushUiPlan& plan,
|
|
BrushUiServices& services)
|
|
{
|
|
switch (plan.operation) {
|
|
case BrushUiOperation::set_tip_color:
|
|
{
|
|
for (const auto value : { plan.r, plan.g, plan.b, plan.a }) {
|
|
const auto channel_status = validate_brush_ui_color_channel(value);
|
|
if (!channel_status.ok()) {
|
|
return channel_status;
|
|
}
|
|
}
|
|
services.set_tip_color(plan.r, plan.g, plan.b, plan.a);
|
|
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
case BrushUiOperation::set_texture:
|
|
if (plan.path.empty()) {
|
|
return pp::foundation::Status::invalid_argument("brush texture path must not be empty");
|
|
}
|
|
services.set_texture(plan.texture_slot, plan.path, plan.thumbnail_path);
|
|
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
|
|
return pp::foundation::Status::success();
|
|
|
|
case BrushUiOperation::replace_brush_from_preset:
|
|
services.replace_brush_from_preset(plan.preserves_existing_color, plan.loads_brush_resources);
|
|
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
|
|
return pp::foundation::Status::success();
|
|
|
|
case BrushUiOperation::stroke_settings_changed:
|
|
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
return pp::foundation::Status::invalid_argument("unknown brush UI operation");
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Status execute_brush_stroke_control_plan(
|
|
const BrushStrokeControlPlan& plan,
|
|
BrushStrokeControlServices& services)
|
|
{
|
|
switch (plan.operation) {
|
|
case BrushStrokeControlOperation::set_float:
|
|
{
|
|
const auto status = validate_brush_stroke_float(plan.float_value);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
services.set_float_setting(plan.float_setting, plan.float_value);
|
|
break;
|
|
}
|
|
|
|
case BrushStrokeControlOperation::set_bool:
|
|
services.set_bool_setting(plan.bool_setting, plan.bool_value);
|
|
break;
|
|
|
|
case BrushStrokeControlOperation::set_blend_mode:
|
|
{
|
|
const auto status = validate_brush_stroke_blend_mode(plan.blend_mode);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
services.set_blend_mode(plan.blend_setting, plan.blend_mode);
|
|
break;
|
|
}
|
|
|
|
case BrushStrokeControlOperation::reset_tip_aspect:
|
|
{
|
|
const auto status = validate_brush_stroke_float(plan.float_value);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
services.reset_tip_aspect(plan.float_value);
|
|
break;
|
|
}
|
|
|
|
case BrushStrokeControlOperation::reset_default_brush:
|
|
services.reset_default_brush();
|
|
break;
|
|
}
|
|
|
|
if (plan.updates_controls) {
|
|
services.update_stroke_controls();
|
|
}
|
|
if (plan.refreshes_preview) {
|
|
services.refresh_stroke_preview();
|
|
}
|
|
if (plan.notifies_stroke_change) {
|
|
services.notify_stroke_changed();
|
|
}
|
|
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Status execute_brush_texture_list_plan(
|
|
const BrushTextureListPlan& plan,
|
|
BrushTextureListServices& services)
|
|
{
|
|
switch (plan.operation) {
|
|
case BrushTextureListOperation::add_texture:
|
|
{
|
|
if (plan.source_path.empty() || plan.high_path.empty() || plan.thumbnail_path.empty()
|
|
|| plan.brush_name.empty()) {
|
|
return pp::foundation::Status::invalid_argument("brush texture add plan has incomplete paths");
|
|
}
|
|
|
|
const auto add_status = services.add_texture_from_source(
|
|
plan.source_path,
|
|
plan.high_path,
|
|
plan.thumbnail_path,
|
|
plan.brush_name,
|
|
plan.converts_brush_alpha);
|
|
if (!add_status.ok()) {
|
|
return add_status;
|
|
}
|
|
if (plan.saves_list) {
|
|
services.save_texture_list();
|
|
}
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
case BrushTextureListOperation::remove_texture:
|
|
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
|
|
return pp::foundation::Status::out_of_range("brush texture remove plan has invalid selection");
|
|
}
|
|
services.remove_texture(plan.current_index, plan.deletes_texture_files);
|
|
if (plan.notifies_selection && plan.target_index >= 0) {
|
|
services.select_texture(plan.target_index);
|
|
}
|
|
if (plan.saves_list) {
|
|
services.save_texture_list();
|
|
}
|
|
return pp::foundation::Status::success();
|
|
|
|
case BrushTextureListOperation::move_texture:
|
|
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
|
|
|| plan.target_index < 0 || plan.target_index >= plan.item_count) {
|
|
return pp::foundation::Status::out_of_range("brush texture move plan has invalid indices");
|
|
}
|
|
services.move_texture(plan.current_index, plan.target_index);
|
|
if (plan.saves_list) {
|
|
services.save_texture_list();
|
|
}
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
return pp::foundation::Status::invalid_argument("unknown brush texture list operation");
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Status execute_brush_preset_list_plan(
|
|
const BrushPresetListPlan& plan,
|
|
BrushPresetListServices& services)
|
|
{
|
|
switch (plan.operation) {
|
|
case BrushPresetListOperation::add_current_brush:
|
|
{
|
|
if (plan.item_count < 0 || plan.target_index != plan.item_count) {
|
|
return pp::foundation::Status::out_of_range("brush preset add plan has invalid target");
|
|
}
|
|
|
|
const auto add_status = services.add_current_brush_preset(plan.target_index);
|
|
if (!add_status.ok()) {
|
|
return add_status;
|
|
}
|
|
if (plan.updates_empty_notification) {
|
|
services.update_preset_empty_notification();
|
|
}
|
|
if (plan.saves_list) {
|
|
services.save_preset_list();
|
|
}
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
case BrushPresetListOperation::remove_preset:
|
|
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
|
|
return pp::foundation::Status::out_of_range("brush preset remove plan has invalid selection");
|
|
}
|
|
if (plan.selects_target && (plan.target_index < 0 || plan.target_index >= plan.item_count - 1)) {
|
|
return pp::foundation::Status::out_of_range("brush preset remove plan has invalid target");
|
|
}
|
|
services.remove_brush_preset(
|
|
plan.current_index,
|
|
plan.target_index,
|
|
plan.selects_target,
|
|
plan.clears_selection);
|
|
if (plan.updates_empty_notification) {
|
|
services.update_preset_empty_notification();
|
|
}
|
|
if (plan.saves_list) {
|
|
services.save_preset_list();
|
|
}
|
|
return pp::foundation::Status::success();
|
|
|
|
case BrushPresetListOperation::move_preset:
|
|
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
|
|
|| plan.target_index < 0 || plan.target_index >= plan.item_count) {
|
|
return pp::foundation::Status::out_of_range("brush preset move plan has invalid indices");
|
|
}
|
|
services.move_brush_preset(plan.current_index, plan.target_index);
|
|
if (plan.saves_list) {
|
|
services.save_preset_list();
|
|
}
|
|
return pp::foundation::Status::success();
|
|
|
|
case BrushPresetListOperation::select_preset:
|
|
if (plan.item_count <= 0 || plan.target_index < 0 || plan.target_index >= plan.item_count) {
|
|
return pp::foundation::Status::out_of_range("brush preset select plan has invalid target");
|
|
}
|
|
services.select_brush_preset(plan.target_index, plan.notifies_brush_changed);
|
|
return pp::foundation::Status::success();
|
|
|
|
case BrushPresetListOperation::clear_presets:
|
|
if (plan.item_count < 0) {
|
|
return pp::foundation::Status::out_of_range("brush preset clear plan has invalid item count");
|
|
}
|
|
services.clear_brush_presets(plan.clears_selection);
|
|
if (plan.updates_empty_notification) {
|
|
services.update_preset_empty_notification();
|
|
}
|
|
if (plan.saves_list) {
|
|
services.save_preset_list();
|
|
}
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
return pp::foundation::Status::invalid_argument("unknown brush preset list operation");
|
|
}
|
|
|
|
} // namespace pp::app
|