Add brush stroke control boundary
This commit is contained in:
@@ -28,6 +28,83 @@ enum class BrushTextureListOperation {
|
||||
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;
|
||||
@@ -62,6 +139,20 @@ struct BrushTextureListPlan {
|
||||
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;
|
||||
@@ -88,6 +179,20 @@ public:
|
||||
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<std::string_view> brush_texture_source_stem(
|
||||
std::string_view source_path) noexcept
|
||||
{
|
||||
@@ -116,6 +221,24 @@ public:
|
||||
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,
|
||||
@@ -189,6 +312,81 @@ public:
|
||||
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,
|
||||
@@ -315,6 +513,63 @@ public:
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "pch.h"
|
||||
#include "app_core/brush_ui.h"
|
||||
#include "log.h"
|
||||
#include "node_panel_stroke.h"
|
||||
#include "canvas.h"
|
||||
@@ -6,6 +7,137 @@
|
||||
#include "app.h"
|
||||
#include "abr.h"
|
||||
|
||||
namespace {
|
||||
|
||||
class LegacyBrushStrokeControlServices final : public pp::app::BrushStrokeControlServices {
|
||||
public:
|
||||
explicit LegacyBrushStrokeControlServices(NodePanelStroke& panel) : panel_(panel) {}
|
||||
|
||||
void set_float_setting(pp::app::BrushStrokeFloatSetting setting, float value) override
|
||||
{
|
||||
auto& brush = *Canvas::I->m_current_brush;
|
||||
switch (setting) {
|
||||
case pp::app::BrushStrokeFloatSetting::tip_size: brush.m_tip_size = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_spacing: brush.m_tip_spacing = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_flow: brush.m_tip_flow = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_opacity: brush.m_tip_opacity = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_angle: brush.m_tip_angle = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_angle_smooth: brush.m_tip_angle_smooth = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_mix: brush.m_tip_mix = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_wet: brush.m_tip_wet = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_noise: brush.m_tip_noise = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_hue: brush.m_tip_hue = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_saturation: brush.m_tip_sat = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_value: brush.m_tip_val = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_scale: brush.m_jitter_scale = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_angle: brush.m_jitter_angle = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_scatter: brush.m_jitter_scatter = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_flow: brush.m_jitter_flow = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_opacity: brush.m_jitter_opacity = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_hue: brush.m_jitter_hue = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_saturation: brush.m_jitter_sat = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_value: brush.m_jitter_val = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_aspect: brush.m_jitter_aspect = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_size: brush.m_dual_size = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_spacing: brush.m_dual_spacing = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_scatter: brush.m_dual_scatter = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_aspect: brush.m_tip_aspect = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_opacity: brush.m_dual_opacity = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_flow: brush.m_dual_flow = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_rotate: brush.m_dual_rotate = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::pattern_scale: brush.m_pattern_scale = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::pattern_brightness: brush.m_pattern_brightness = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::pattern_contrast: brush.m_pattern_contrast = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::pattern_depth: brush.m_pattern_depth = value; break;
|
||||
}
|
||||
}
|
||||
|
||||
void set_bool_setting(pp::app::BrushStrokeBoolSetting setting, bool value) override
|
||||
{
|
||||
auto& brush = *Canvas::I->m_current_brush;
|
||||
switch (setting) {
|
||||
case pp::app::BrushStrokeBoolSetting::tip_angle_init: brush.m_tip_angle_init = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_angle_follow: brush.m_tip_angle_follow = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_flow_pressure: brush.m_tip_flow_pressure = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_opacity_pressure: brush.m_tip_opacity_pressure = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_size_pressure: brush.m_tip_size_pressure = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis: brush.m_jitter_scatter_bothaxis = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis: brush.m_jitter_aspect_bothaxis = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample: brush.m_jitter_hsv_eachsample = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_invert: brush.m_tip_invert = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_flip_x: brush.m_tip_flipx = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_flip_y: brush.m_tip_flipy = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_enabled: brush.m_pattern_enabled = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_enabled: brush.m_dual_enabled = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis: brush.m_dual_scatter_bothaxis = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_invert: brush.m_dual_invert = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_flip_x: brush.m_dual_flipx = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_flip_y: brush.m_dual_flipy = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_random_flip: brush.m_dual_randflip = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_random_flip_x: brush.m_tip_randflipx = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_random_flip_y: brush.m_tip_randflipy = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_each_sample: brush.m_pattern_eachsample = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_invert: brush.m_pattern_invert = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_flip_x: brush.m_pattern_flipx = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_flip_y: brush.m_pattern_flipy = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_random_offset: brush.m_pattern_rand_offset = value; break;
|
||||
}
|
||||
}
|
||||
|
||||
void set_blend_mode(pp::app::BrushStrokeBlendSetting setting, int blend_mode) override
|
||||
{
|
||||
auto& brush = *Canvas::I->m_current_brush;
|
||||
switch (setting) {
|
||||
case pp::app::BrushStrokeBlendSetting::tip: brush.m_blend_mode = blend_mode; break;
|
||||
case pp::app::BrushStrokeBlendSetting::dual: brush.m_dual_blend_mode = blend_mode; break;
|
||||
case pp::app::BrushStrokeBlendSetting::pattern: brush.m_pattern_blend_mode = blend_mode; break;
|
||||
}
|
||||
}
|
||||
|
||||
void reset_tip_aspect(float value) override
|
||||
{
|
||||
panel_.m_tip_aspect->set_value(value);
|
||||
Canvas::I->m_current_brush->m_tip_aspect = value;
|
||||
}
|
||||
|
||||
void reset_default_brush() override
|
||||
{
|
||||
auto brush = std::make_shared<Brush>();
|
||||
brush->load_tip(
|
||||
panel_.m_brush_popup->get_texture_path(panel_.m_default_brush_index),
|
||||
panel_.m_brush_popup->get_thumb_path(panel_.m_default_brush_index));
|
||||
brush->m_tip_size = 30;
|
||||
brush->m_tip_flow = .9f;
|
||||
brush->m_tip_spacing = .1f;
|
||||
brush->m_tip_opacity = 1.f;
|
||||
Canvas::I->m_current_brush = brush;
|
||||
}
|
||||
|
||||
void update_stroke_controls() override
|
||||
{
|
||||
panel_.update_controls();
|
||||
}
|
||||
|
||||
void refresh_stroke_preview() override
|
||||
{
|
||||
if (panel_.m_preview) {
|
||||
panel_.m_preview->m_brush = Canvas::I->m_current_brush;
|
||||
}
|
||||
}
|
||||
|
||||
void notify_stroke_changed() override
|
||||
{
|
||||
if (panel_.on_stroke_change) {
|
||||
panel_.on_stroke_change(&panel_);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
NodePanelStroke& panel_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Node* NodePanelStroke::clone_instantiate() const
|
||||
{
|
||||
return new NodePanelStroke();
|
||||
@@ -156,7 +288,8 @@ void NodePanelStroke::init_controls()
|
||||
//m_presets_popup->m_flood_events = true;
|
||||
//m_presets_popup->m_capture_children = false;
|
||||
|
||||
int br_idx = std::max(m_brush_popup->find_brush("Round-Hard"), 0);
|
||||
m_default_brush_index = std::max(m_brush_popup->find_brush("Round-Hard"), 0);
|
||||
const int br_idx = m_default_brush_index;
|
||||
|
||||
// init main brush
|
||||
auto b = std::make_shared<Brush>();
|
||||
@@ -331,73 +464,73 @@ void NodePanelStroke::init_controls()
|
||||
|
||||
m_blend_mode = find<NodeComboBox>("blend-mode");
|
||||
m_blend_mode->on_select = [this](Node*, int index) {
|
||||
Canvas::I->m_current_brush->m_blend_mode = index;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, index);
|
||||
if (plan) {
|
||||
execute_stroke_control_plan(plan.value());
|
||||
}
|
||||
};
|
||||
|
||||
init_slider(m_tip_size, "tip-size", &Brush::m_tip_size);
|
||||
init_slider(m_tip_spacing, "tip-spacing", &Brush::m_tip_spacing);
|
||||
init_slider(m_tip_flow, "tip-flow", &Brush::m_tip_flow);
|
||||
init_slider(m_tip_opacity, "tip-opacity", &Brush::m_tip_opacity);
|
||||
init_slider(m_tip_angle, "tip-angle", &Brush::m_tip_angle);
|
||||
init_slider(m_tip_angle_smooth, "tip-angle-smooth", &Brush::m_tip_angle_smooth);
|
||||
init_slider(m_tip_mix, "tip-mix", &Brush::m_tip_mix);
|
||||
init_slider(m_tip_wet, "tip-wet", &Brush::m_tip_wet);
|
||||
init_slider(m_tip_noise, "tip-noise", &Brush::m_tip_noise);
|
||||
init_slider(m_tip_hue, "tip-hue", &Brush::m_tip_hue);
|
||||
init_slider(m_tip_sat, "tip-sat", &Brush::m_tip_sat);
|
||||
init_slider(m_tip_val, "tip-val", &Brush::m_tip_val);
|
||||
init_slider(m_jitter_scale, "jitter-scale", &Brush::m_jitter_scale);
|
||||
init_slider(m_jitter_angle, "jitter-angle", &Brush::m_jitter_angle);
|
||||
init_slider(m_jitter_scatter, "jitter-scatter", &Brush::m_jitter_scatter);
|
||||
init_slider(m_jitter_flow, "jitter-flow", &Brush::m_jitter_flow);
|
||||
init_slider(m_jitter_opacity, "jitter-opacity", &Brush::m_jitter_opacity);
|
||||
init_slider(m_jitter_hue, "jitter-hue", &Brush::m_jitter_hue);
|
||||
init_slider(m_jitter_sat, "jitter-sat", &Brush::m_jitter_sat);
|
||||
init_slider(m_jitter_val, "jitter-val", &Brush::m_jitter_val);
|
||||
init_slider(m_jitter_aspect, "jitter-aspect", &Brush::m_jitter_aspect);
|
||||
init_slider(m_tip_size, "tip-size", pp::app::BrushStrokeFloatSetting::tip_size, &Brush::m_tip_size);
|
||||
init_slider(m_tip_spacing, "tip-spacing", pp::app::BrushStrokeFloatSetting::tip_spacing, &Brush::m_tip_spacing);
|
||||
init_slider(m_tip_flow, "tip-flow", pp::app::BrushStrokeFloatSetting::tip_flow, &Brush::m_tip_flow);
|
||||
init_slider(m_tip_opacity, "tip-opacity", pp::app::BrushStrokeFloatSetting::tip_opacity, &Brush::m_tip_opacity);
|
||||
init_slider(m_tip_angle, "tip-angle", pp::app::BrushStrokeFloatSetting::tip_angle, &Brush::m_tip_angle);
|
||||
init_slider(m_tip_angle_smooth, "tip-angle-smooth", pp::app::BrushStrokeFloatSetting::tip_angle_smooth, &Brush::m_tip_angle_smooth);
|
||||
init_slider(m_tip_mix, "tip-mix", pp::app::BrushStrokeFloatSetting::tip_mix, &Brush::m_tip_mix);
|
||||
init_slider(m_tip_wet, "tip-wet", pp::app::BrushStrokeFloatSetting::tip_wet, &Brush::m_tip_wet);
|
||||
init_slider(m_tip_noise, "tip-noise", pp::app::BrushStrokeFloatSetting::tip_noise, &Brush::m_tip_noise);
|
||||
init_slider(m_tip_hue, "tip-hue", pp::app::BrushStrokeFloatSetting::tip_hue, &Brush::m_tip_hue);
|
||||
init_slider(m_tip_sat, "tip-sat", pp::app::BrushStrokeFloatSetting::tip_saturation, &Brush::m_tip_sat);
|
||||
init_slider(m_tip_val, "tip-val", pp::app::BrushStrokeFloatSetting::tip_value, &Brush::m_tip_val);
|
||||
init_slider(m_jitter_scale, "jitter-scale", pp::app::BrushStrokeFloatSetting::jitter_scale, &Brush::m_jitter_scale);
|
||||
init_slider(m_jitter_angle, "jitter-angle", pp::app::BrushStrokeFloatSetting::jitter_angle, &Brush::m_jitter_angle);
|
||||
init_slider(m_jitter_scatter, "jitter-scatter", pp::app::BrushStrokeFloatSetting::jitter_scatter, &Brush::m_jitter_scatter);
|
||||
init_slider(m_jitter_flow, "jitter-flow", pp::app::BrushStrokeFloatSetting::jitter_flow, &Brush::m_jitter_flow);
|
||||
init_slider(m_jitter_opacity, "jitter-opacity", pp::app::BrushStrokeFloatSetting::jitter_opacity, &Brush::m_jitter_opacity);
|
||||
init_slider(m_jitter_hue, "jitter-hue", pp::app::BrushStrokeFloatSetting::jitter_hue, &Brush::m_jitter_hue);
|
||||
init_slider(m_jitter_sat, "jitter-sat", pp::app::BrushStrokeFloatSetting::jitter_saturation, &Brush::m_jitter_sat);
|
||||
init_slider(m_jitter_val, "jitter-val", pp::app::BrushStrokeFloatSetting::jitter_value, &Brush::m_jitter_val);
|
||||
init_slider(m_jitter_aspect, "jitter-aspect", pp::app::BrushStrokeFloatSetting::jitter_aspect, &Brush::m_jitter_aspect);
|
||||
|
||||
init_checkbox(m_tip_angle_init, "tip-angle-init", &Brush::m_tip_angle_init);
|
||||
init_checkbox(m_tip_angle_follow, "tip-angle-follow", &Brush::m_tip_angle_follow);
|
||||
init_checkbox(m_tip_flow_pressure, "tip-flow-pressure", &Brush::m_tip_flow_pressure);
|
||||
init_checkbox(m_tip_opacity_pressure, "tip-opacity-pressure", &Brush::m_tip_opacity_pressure);
|
||||
init_checkbox(m_tip_size_pressure, "tip-size-pressure", &Brush::m_tip_size_pressure);
|
||||
init_checkbox(m_jitter_scatter_bothaxis, "jitter-scatter-bothaxis", &Brush::m_jitter_scatter_bothaxis);
|
||||
init_checkbox(m_jitter_aspect_bothaxis, "jitter-aspect-bothaxis", &Brush::m_jitter_aspect_bothaxis);
|
||||
init_checkbox(m_jitter_hsv_eachsample, "jitter-hsv-eachsample", &Brush::m_jitter_hsv_eachsample);
|
||||
init_checkbox(m_tip_angle_init, "tip-angle-init", pp::app::BrushStrokeBoolSetting::tip_angle_init, &Brush::m_tip_angle_init);
|
||||
init_checkbox(m_tip_angle_follow, "tip-angle-follow", pp::app::BrushStrokeBoolSetting::tip_angle_follow, &Brush::m_tip_angle_follow);
|
||||
init_checkbox(m_tip_flow_pressure, "tip-flow-pressure", pp::app::BrushStrokeBoolSetting::tip_flow_pressure, &Brush::m_tip_flow_pressure);
|
||||
init_checkbox(m_tip_opacity_pressure, "tip-opacity-pressure", pp::app::BrushStrokeBoolSetting::tip_opacity_pressure, &Brush::m_tip_opacity_pressure);
|
||||
init_checkbox(m_tip_size_pressure, "tip-size-pressure", pp::app::BrushStrokeBoolSetting::tip_size_pressure, &Brush::m_tip_size_pressure);
|
||||
init_checkbox(m_jitter_scatter_bothaxis, "jitter-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis, &Brush::m_jitter_scatter_bothaxis);
|
||||
init_checkbox(m_jitter_aspect_bothaxis, "jitter-aspect-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis, &Brush::m_jitter_aspect_bothaxis);
|
||||
init_checkbox(m_jitter_hsv_eachsample, "jitter-hsv-eachsample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample, &Brush::m_jitter_hsv_eachsample);
|
||||
|
||||
init_checkbox(m_tip_invert, "tip-invert", &Brush::m_tip_invert);
|
||||
init_checkbox(m_tip_flipx, "tip-flipx", &Brush::m_tip_flipx);
|
||||
init_checkbox(m_tip_flipy, "tip-flipy", &Brush::m_tip_flipy);
|
||||
init_checkbox(m_pattern_enabled, "pattern-enabled", &Brush::m_pattern_enabled);
|
||||
init_checkbox(m_dual_enabled, "dual-enabled", &Brush::m_dual_enabled);
|
||||
init_checkbox(m_dual_scatter_bothaxis, "dual-scatter-bothaxis", &Brush::m_dual_scatter_bothaxis);
|
||||
init_checkbox(m_dual_invert, "dual-invert", &Brush::m_dual_invert);
|
||||
init_checkbox(m_dual_flipx, "dual-flipx", &Brush::m_dual_flipx);
|
||||
init_checkbox(m_dual_flipy, "dual-flipy", &Brush::m_dual_flipy);
|
||||
init_checkbox(m_dual_randflip, "dual-randflip", &Brush::m_dual_randflip);
|
||||
init_checkbox(m_tip_randflipx, "tip-randflipx", &Brush::m_tip_randflipx);
|
||||
init_checkbox(m_tip_randflipy, "tip-randflipy", &Brush::m_tip_randflipy);
|
||||
init_checkbox(m_pattern_eachsample, "pattern-eachsample", &Brush::m_pattern_eachsample);
|
||||
init_checkbox(m_tip_invert, "tip-invert", pp::app::BrushStrokeBoolSetting::tip_invert, &Brush::m_tip_invert);
|
||||
init_checkbox(m_tip_flipx, "tip-flipx", pp::app::BrushStrokeBoolSetting::tip_flip_x, &Brush::m_tip_flipx);
|
||||
init_checkbox(m_tip_flipy, "tip-flipy", pp::app::BrushStrokeBoolSetting::tip_flip_y, &Brush::m_tip_flipy);
|
||||
init_checkbox(m_pattern_enabled, "pattern-enabled", pp::app::BrushStrokeBoolSetting::pattern_enabled, &Brush::m_pattern_enabled);
|
||||
init_checkbox(m_dual_enabled, "dual-enabled", pp::app::BrushStrokeBoolSetting::dual_enabled, &Brush::m_dual_enabled);
|
||||
init_checkbox(m_dual_scatter_bothaxis, "dual-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis, &Brush::m_dual_scatter_bothaxis);
|
||||
init_checkbox(m_dual_invert, "dual-invert", pp::app::BrushStrokeBoolSetting::dual_invert, &Brush::m_dual_invert);
|
||||
init_checkbox(m_dual_flipx, "dual-flipx", pp::app::BrushStrokeBoolSetting::dual_flip_x, &Brush::m_dual_flipx);
|
||||
init_checkbox(m_dual_flipy, "dual-flipy", pp::app::BrushStrokeBoolSetting::dual_flip_y, &Brush::m_dual_flipy);
|
||||
init_checkbox(m_dual_randflip, "dual-randflip", pp::app::BrushStrokeBoolSetting::dual_random_flip, &Brush::m_dual_randflip);
|
||||
init_checkbox(m_tip_randflipx, "tip-randflipx", pp::app::BrushStrokeBoolSetting::tip_random_flip_x, &Brush::m_tip_randflipx);
|
||||
init_checkbox(m_tip_randflipy, "tip-randflipy", pp::app::BrushStrokeBoolSetting::tip_random_flip_y, &Brush::m_tip_randflipy);
|
||||
init_checkbox(m_pattern_eachsample, "pattern-eachsample", pp::app::BrushStrokeBoolSetting::pattern_each_sample, &Brush::m_pattern_eachsample);
|
||||
|
||||
init_checkbox(m_pattern_invert, "pattern-invert", &Brush::m_pattern_invert);
|
||||
init_checkbox(m_pattern_flipx, "pattern-flipx", &Brush::m_pattern_flipx);
|
||||
init_checkbox(m_pattern_flipy, "pattern-flipy", &Brush::m_pattern_flipy);
|
||||
init_checkbox(m_pattern_rand_offset, "pattern-rand-offset", &Brush::m_pattern_rand_offset);
|
||||
init_checkbox(m_pattern_invert, "pattern-invert", pp::app::BrushStrokeBoolSetting::pattern_invert, &Brush::m_pattern_invert);
|
||||
init_checkbox(m_pattern_flipx, "pattern-flipx", pp::app::BrushStrokeBoolSetting::pattern_flip_x, &Brush::m_pattern_flipx);
|
||||
init_checkbox(m_pattern_flipy, "pattern-flipy", pp::app::BrushStrokeBoolSetting::pattern_flip_y, &Brush::m_pattern_flipy);
|
||||
init_checkbox(m_pattern_rand_offset, "pattern-rand-offset", pp::app::BrushStrokeBoolSetting::pattern_random_offset, &Brush::m_pattern_rand_offset);
|
||||
|
||||
init_slider(m_dual_size, "dual-size", &Brush::m_dual_size);
|
||||
init_slider(m_dual_spacing, "dual-spacing", &Brush::m_dual_spacing);
|
||||
init_slider(m_dual_scatter, "dual-scatter", &Brush::m_dual_scatter);
|
||||
init_slider(m_tip_aspect, "tip-aspect", &Brush::m_tip_aspect);
|
||||
init_slider(m_dual_opacity, "dual-opacity", &Brush::m_dual_opacity);
|
||||
init_slider(m_dual_flow, "dual-flow", &Brush::m_dual_flow);
|
||||
init_slider(m_dual_rotate, "dual-rotate", &Brush::m_dual_rotate);
|
||||
init_slider(m_pattern_scale, "pattern-scale", &Brush::m_pattern_scale);
|
||||
init_slider(m_pattern_brightness, "pattern-brightness", &Brush::m_pattern_brightness);
|
||||
init_slider(m_pattern_contrast, "pattern-contrast", &Brush::m_pattern_contrast);
|
||||
init_slider(m_pattern_depth, "pattern-depth", &Brush::m_pattern_depth);
|
||||
init_slider(m_dual_size, "dual-size", pp::app::BrushStrokeFloatSetting::dual_size, &Brush::m_dual_size);
|
||||
init_slider(m_dual_spacing, "dual-spacing", pp::app::BrushStrokeFloatSetting::dual_spacing, &Brush::m_dual_spacing);
|
||||
init_slider(m_dual_scatter, "dual-scatter", pp::app::BrushStrokeFloatSetting::dual_scatter, &Brush::m_dual_scatter);
|
||||
init_slider(m_tip_aspect, "tip-aspect", pp::app::BrushStrokeFloatSetting::tip_aspect, &Brush::m_tip_aspect);
|
||||
init_slider(m_dual_opacity, "dual-opacity", pp::app::BrushStrokeFloatSetting::dual_opacity, &Brush::m_dual_opacity);
|
||||
init_slider(m_dual_flow, "dual-flow", pp::app::BrushStrokeFloatSetting::dual_flow, &Brush::m_dual_flow);
|
||||
init_slider(m_dual_rotate, "dual-rotate", pp::app::BrushStrokeFloatSetting::dual_rotate, &Brush::m_dual_rotate);
|
||||
init_slider(m_pattern_scale, "pattern-scale", pp::app::BrushStrokeFloatSetting::pattern_scale, &Brush::m_pattern_scale);
|
||||
init_slider(m_pattern_brightness, "pattern-brightness", pp::app::BrushStrokeFloatSetting::pattern_brightness, &Brush::m_pattern_brightness);
|
||||
init_slider(m_pattern_contrast, "pattern-contrast", pp::app::BrushStrokeFloatSetting::pattern_contrast, &Brush::m_pattern_contrast);
|
||||
init_slider(m_pattern_depth, "pattern-depth", pp::app::BrushStrokeFloatSetting::pattern_depth, &Brush::m_pattern_depth);
|
||||
|
||||
SliderCurve curve_cubic {
|
||||
[](float v) { return glm::pow(v, 3.f); },
|
||||
@@ -469,27 +602,23 @@ void NodePanelStroke::init_controls()
|
||||
|
||||
m_tip_aspect_reset = find<NodeButtonCustom>("tip-aspect-reset");
|
||||
m_tip_aspect_reset->on_click = [this](Node*) {
|
||||
m_tip_aspect->set_value(0.5);
|
||||
Canvas::I->m_current_brush->m_tip_aspect = 0.5f;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
execute_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset());
|
||||
};
|
||||
|
||||
m_dual_blend_mode = find<NodeComboBox>("dual-blend-mode");
|
||||
m_dual_blend_mode->on_select = [this](Node*, int index) {
|
||||
Canvas::I->m_current_brush->m_dual_blend_mode = index;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::dual, index);
|
||||
if (plan) {
|
||||
execute_stroke_control_plan(plan.value());
|
||||
}
|
||||
};
|
||||
|
||||
m_pattern_blend_mode = find<NodeComboBox>("pattern-blend-mode");
|
||||
m_pattern_blend_mode->on_select = [this](Node*, int index) {
|
||||
Canvas::I->m_current_brush->m_pattern_blend_mode = index;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, index);
|
||||
if (plan) {
|
||||
execute_stroke_control_plan(plan.value());
|
||||
}
|
||||
};
|
||||
|
||||
m_preview->m_brush = Canvas::I->m_current_brush;
|
||||
@@ -518,54 +647,68 @@ void NodePanelStroke::init_controls()
|
||||
}
|
||||
|
||||
m_brush_settings_reset = find<NodeButton>("brush-settings-reset");
|
||||
m_brush_settings_reset->on_click = [br_idx,this](Node*) {
|
||||
auto b = std::make_shared<Brush>();
|
||||
b->load_tip(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx));
|
||||
//b->load_dual(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx));
|
||||
//b->load_pattern(m_pattern_popup->get_texture_path(0), m_pattern_popup->get_thumb_path(0));
|
||||
b->m_tip_size = 30;
|
||||
b->m_tip_flow = .9f;
|
||||
b->m_tip_spacing = .1f;
|
||||
b->m_tip_opacity = 1.f;
|
||||
Canvas::I->m_current_brush = b;
|
||||
update_controls();
|
||||
App::I->brush_update(true, true);
|
||||
m_brush_settings_reset->on_click = [this](Node*) {
|
||||
execute_stroke_control_plan(pp::app::plan_brush_default_settings_reset());
|
||||
};
|
||||
|
||||
update_controls();
|
||||
}
|
||||
|
||||
void NodePanelStroke::init_slider(NodeSliderH*& target, const char* id, float Brush::* prop)
|
||||
void NodePanelStroke::init_slider(
|
||||
NodeSliderH*& target,
|
||||
const char* id,
|
||||
pp::app::BrushStrokeFloatSetting setting,
|
||||
float Brush::* prop)
|
||||
{
|
||||
target = find<NodeSliderH>(id);
|
||||
target->on_value_changed = std::bind(&NodePanelStroke::handle_slide,
|
||||
this, prop, std::placeholders::_1, std::placeholders::_2);
|
||||
this, setting, prop, std::placeholders::_1, std::placeholders::_2);
|
||||
//m_canvas->m_brush->*prop = target->m_values;
|
||||
}
|
||||
|
||||
void NodePanelStroke::handle_slide(float Brush::* prop, Node* target, float value)
|
||||
void NodePanelStroke::handle_slide(
|
||||
pp::app::BrushStrokeFloatSetting setting,
|
||||
float Brush::* prop,
|
||||
Node* target,
|
||||
float value)
|
||||
{
|
||||
auto curve = m_curves.find((NodeSliderH*)target);
|
||||
Canvas::I->m_current_brush.get()->*prop = curve != m_curves.end() ? curve->second.to_value(value) : value;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto brush_value = curve != m_curves.end() ? curve->second.to_value(value) : value;
|
||||
const auto plan = pp::app::plan_brush_stroke_float_setting(setting, brush_value);
|
||||
if (plan) {
|
||||
execute_stroke_control_plan(plan.value());
|
||||
} else {
|
||||
Canvas::I->m_current_brush.get()->*prop = brush_value;
|
||||
}
|
||||
}
|
||||
|
||||
void NodePanelStroke::init_checkbox(NodeCheckBox*& target, const char* id, bool Brush::* prop)
|
||||
void NodePanelStroke::init_checkbox(
|
||||
NodeCheckBox*& target,
|
||||
const char* id,
|
||||
pp::app::BrushStrokeBoolSetting setting,
|
||||
bool Brush::* prop)
|
||||
{
|
||||
target = find<NodeCheckBox>(id);
|
||||
target->on_value_changed = std::bind(&NodePanelStroke::handle_checkbox,
|
||||
this, prop, std::placeholders::_1, std::placeholders::_2);
|
||||
this, setting, std::placeholders::_2);
|
||||
Canvas::I->m_current_brush.get()->*prop = target->checked;
|
||||
}
|
||||
|
||||
void NodePanelStroke::handle_checkbox(bool Brush::* prop, Node *target, bool value)
|
||||
void NodePanelStroke::handle_checkbox(
|
||||
pp::app::BrushStrokeBoolSetting setting,
|
||||
bool value)
|
||||
{
|
||||
Canvas::I->m_current_brush.get()->*prop = value;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto plan = pp::app::plan_brush_stroke_bool_setting(setting, value);
|
||||
execute_stroke_control_plan(plan);
|
||||
}
|
||||
|
||||
void NodePanelStroke::execute_stroke_control_plan(const pp::app::BrushStrokeControlPlan& plan)
|
||||
{
|
||||
LegacyBrushStrokeControlServices services(*this);
|
||||
const auto status = pp::app::execute_brush_stroke_control_plan(plan, services);
|
||||
if (!status.ok()) {
|
||||
LOG("Brush stroke control action failed: %s", status.message);
|
||||
}
|
||||
}
|
||||
|
||||
kEventResult NodePanelStroke::handle_event(Event* e)
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
#include "node_image.h"
|
||||
#include "node_panel_brush.h"
|
||||
|
||||
namespace pp::app {
|
||||
struct BrushStrokeControlPlan;
|
||||
enum class BrushStrokeBoolSetting;
|
||||
enum class BrushStrokeFloatSetting;
|
||||
enum class BrushStrokeBlendSetting;
|
||||
} // namespace pp::app
|
||||
|
||||
class NodePanelStroke : public Node
|
||||
{
|
||||
public:
|
||||
@@ -103,6 +110,7 @@ public:
|
||||
inline float to_slider(float v) { return m_inv(v); }
|
||||
};
|
||||
std::map<NodeSliderH*, SliderCurve> m_curves;
|
||||
int m_default_brush_index = 0;
|
||||
|
||||
virtual Node* clone_instantiate() const override;
|
||||
virtual void clone_finalize(Node* dest) const override;
|
||||
@@ -116,9 +124,10 @@ public:
|
||||
void set_size(float value, bool normalized, bool propagate);
|
||||
|
||||
void init_fold(const std::string& name);
|
||||
void init_slider(NodeSliderH*& slider, const char* id, float Brush::* prop);
|
||||
void handle_slide(float Brush::* prop, Node* target, float value);
|
||||
void init_slider(NodeSliderH*& slider, const char* id, pp::app::BrushStrokeFloatSetting setting, float Brush::* prop);
|
||||
void handle_slide(pp::app::BrushStrokeFloatSetting setting, float Brush::* prop, Node* target, float value);
|
||||
|
||||
void init_checkbox(NodeCheckBox*& slider, const char* id, bool Brush::* prop);
|
||||
void handle_checkbox(bool Brush::* prop, Node* target, bool value);
|
||||
void init_checkbox(NodeCheckBox*& slider, const char* id, pp::app::BrushStrokeBoolSetting setting, bool Brush::* prop);
|
||||
void handle_checkbox(pp::app::BrushStrokeBoolSetting setting, bool value);
|
||||
void execute_stroke_control_plan(const pp::app::BrushStrokeControlPlan& plan);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user