#pragma once #include "foundation/result.h" #include #include #include #include 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 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 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 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 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::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::failure( pp::foundation::Status::invalid_argument("brush texture source path must include a file extension")); } return pp::foundation::Result::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 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::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::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_brush_ui_texture( BrushUiTextureSlot slot, std::string_view path, std::string_view thumbnail_path) { if (path.empty()) { return pp::foundation::Result::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::success(std::move(plan)); } [[nodiscard]] inline pp::foundation::Result plan_brush_ui_preset_replace(bool has_preset_brush) { if (!has_preset_brush) { return pp::foundation::Result::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::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 plan_brush_stroke_float_setting( BrushStrokeFloatSetting setting, float value) { const auto status = validate_brush_stroke_float(value); if (!status.ok()) { return pp::foundation::Result::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::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 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::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::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 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::failure( pp::foundation::Status::invalid_argument("brush texture directory must not be empty")); } if (data_path.empty()) { return pp::foundation::Result::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::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::success(std::move(plan)); } [[nodiscard]] inline pp::foundation::Result plan_brush_texture_list_remove( int item_count, int current_index, bool current_is_user_texture) { if (item_count <= 0) { return pp::foundation::Result::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::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::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_brush_texture_list_move( int item_count, int current_index, int offset) { if (item_count <= 0) { return pp::foundation::Result::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::failure( pp::foundation::Status::out_of_range("selected brush texture index is outside the list")); } if (offset == 0) { return pp::foundation::Result::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::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"); } } // namespace pp::app