Add brush stroke control boundary

This commit is contained in:
2026-06-03 17:42:09 +02:00
parent 9adfad9609
commit dc23a5648d
9 changed files with 1141 additions and 109 deletions

View File

@@ -327,6 +327,14 @@ struct PlanBrushTextureListArgs {
bool current_is_user_texture = false;
};
struct PlanBrushStrokeControlArgs {
std::string kind = "float";
std::string setting = "tip-size";
float value = 1.0F;
bool enabled = true;
int blend_mode = 0;
};
struct PlanGridOperationArgs {
std::string kind = "pick";
std::string path;
@@ -1112,6 +1120,111 @@ const char* brush_texture_list_operation_name(pp::app::BrushTextureListOperation
return "add-texture";
}
const char* brush_stroke_control_operation_name(pp::app::BrushStrokeControlOperation operation) noexcept
{
switch (operation) {
case pp::app::BrushStrokeControlOperation::set_float:
return "set-float";
case pp::app::BrushStrokeControlOperation::set_bool:
return "set-bool";
case pp::app::BrushStrokeControlOperation::set_blend_mode:
return "set-blend-mode";
case pp::app::BrushStrokeControlOperation::reset_tip_aspect:
return "reset-tip-aspect";
case pp::app::BrushStrokeControlOperation::reset_default_brush:
return "reset-default-brush";
}
return "set-float";
}
const char* brush_stroke_float_setting_name(pp::app::BrushStrokeFloatSetting setting) noexcept
{
switch (setting) {
case pp::app::BrushStrokeFloatSetting::tip_size: return "tip-size";
case pp::app::BrushStrokeFloatSetting::tip_spacing: return "tip-spacing";
case pp::app::BrushStrokeFloatSetting::tip_flow: return "tip-flow";
case pp::app::BrushStrokeFloatSetting::tip_opacity: return "tip-opacity";
case pp::app::BrushStrokeFloatSetting::tip_angle: return "tip-angle";
case pp::app::BrushStrokeFloatSetting::tip_angle_smooth: return "tip-angle-smooth";
case pp::app::BrushStrokeFloatSetting::tip_mix: return "tip-mix";
case pp::app::BrushStrokeFloatSetting::tip_wet: return "tip-wet";
case pp::app::BrushStrokeFloatSetting::tip_noise: return "tip-noise";
case pp::app::BrushStrokeFloatSetting::tip_hue: return "tip-hue";
case pp::app::BrushStrokeFloatSetting::tip_saturation: return "tip-saturation";
case pp::app::BrushStrokeFloatSetting::tip_value: return "tip-value";
case pp::app::BrushStrokeFloatSetting::jitter_scale: return "jitter-scale";
case pp::app::BrushStrokeFloatSetting::jitter_angle: return "jitter-angle";
case pp::app::BrushStrokeFloatSetting::jitter_scatter: return "jitter-scatter";
case pp::app::BrushStrokeFloatSetting::jitter_flow: return "jitter-flow";
case pp::app::BrushStrokeFloatSetting::jitter_opacity: return "jitter-opacity";
case pp::app::BrushStrokeFloatSetting::jitter_hue: return "jitter-hue";
case pp::app::BrushStrokeFloatSetting::jitter_saturation: return "jitter-saturation";
case pp::app::BrushStrokeFloatSetting::jitter_value: return "jitter-value";
case pp::app::BrushStrokeFloatSetting::jitter_aspect: return "jitter-aspect";
case pp::app::BrushStrokeFloatSetting::dual_size: return "dual-size";
case pp::app::BrushStrokeFloatSetting::dual_spacing: return "dual-spacing";
case pp::app::BrushStrokeFloatSetting::dual_scatter: return "dual-scatter";
case pp::app::BrushStrokeFloatSetting::tip_aspect: return "tip-aspect";
case pp::app::BrushStrokeFloatSetting::dual_opacity: return "dual-opacity";
case pp::app::BrushStrokeFloatSetting::dual_flow: return "dual-flow";
case pp::app::BrushStrokeFloatSetting::dual_rotate: return "dual-rotate";
case pp::app::BrushStrokeFloatSetting::pattern_scale: return "pattern-scale";
case pp::app::BrushStrokeFloatSetting::pattern_brightness: return "pattern-brightness";
case pp::app::BrushStrokeFloatSetting::pattern_contrast: return "pattern-contrast";
case pp::app::BrushStrokeFloatSetting::pattern_depth: return "pattern-depth";
}
return "tip-size";
}
const char* brush_stroke_bool_setting_name(pp::app::BrushStrokeBoolSetting setting) noexcept
{
switch (setting) {
case pp::app::BrushStrokeBoolSetting::tip_angle_init: return "tip-angle-init";
case pp::app::BrushStrokeBoolSetting::tip_angle_follow: return "tip-angle-follow";
case pp::app::BrushStrokeBoolSetting::tip_flow_pressure: return "tip-flow-pressure";
case pp::app::BrushStrokeBoolSetting::tip_opacity_pressure: return "tip-opacity-pressure";
case pp::app::BrushStrokeBoolSetting::tip_size_pressure: return "tip-size-pressure";
case pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis: return "jitter-scatter-both-axis";
case pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis: return "jitter-aspect-both-axis";
case pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample: return "jitter-hsv-each-sample";
case pp::app::BrushStrokeBoolSetting::tip_invert: return "tip-invert";
case pp::app::BrushStrokeBoolSetting::tip_flip_x: return "tip-flip-x";
case pp::app::BrushStrokeBoolSetting::tip_flip_y: return "tip-flip-y";
case pp::app::BrushStrokeBoolSetting::pattern_enabled: return "pattern-enabled";
case pp::app::BrushStrokeBoolSetting::dual_enabled: return "dual-enabled";
case pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis: return "dual-scatter-both-axis";
case pp::app::BrushStrokeBoolSetting::dual_invert: return "dual-invert";
case pp::app::BrushStrokeBoolSetting::dual_flip_x: return "dual-flip-x";
case pp::app::BrushStrokeBoolSetting::dual_flip_y: return "dual-flip-y";
case pp::app::BrushStrokeBoolSetting::dual_random_flip: return "dual-random-flip";
case pp::app::BrushStrokeBoolSetting::tip_random_flip_x: return "tip-random-flip-x";
case pp::app::BrushStrokeBoolSetting::tip_random_flip_y: return "tip-random-flip-y";
case pp::app::BrushStrokeBoolSetting::pattern_each_sample: return "pattern-each-sample";
case pp::app::BrushStrokeBoolSetting::pattern_invert: return "pattern-invert";
case pp::app::BrushStrokeBoolSetting::pattern_flip_x: return "pattern-flip-x";
case pp::app::BrushStrokeBoolSetting::pattern_flip_y: return "pattern-flip-y";
case pp::app::BrushStrokeBoolSetting::pattern_random_offset: return "pattern-random-offset";
}
return "tip-angle-init";
}
const char* brush_stroke_blend_setting_name(pp::app::BrushStrokeBlendSetting setting) noexcept
{
switch (setting) {
case pp::app::BrushStrokeBlendSetting::tip:
return "tip";
case pp::app::BrushStrokeBlendSetting::dual:
return "dual";
case pp::app::BrushStrokeBlendSetting::pattern:
return "pattern";
}
return "tip";
}
const char* canvas_tool_operation_name(pp::app::CanvasToolOperation operation) noexcept
{
switch (operation) {
@@ -1639,6 +1752,7 @@ void print_help()
<< " plan-animation-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\n"
<< " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n"
<< " plan-brush-texture-list --kind add|remove|move [--dir NAME] [--data-path DIR] [--source FILE] [--item-count N] [--current-index N] [--offset N] [--user-texture]\n"
<< " plan-brush-stroke-control --kind float|bool|blend|tip-aspect-reset|default-reset [--setting NAME] [--value N] [--enabled|--disabled] [--blend-mode N]\n"
<< " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n"
<< " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n"
<< " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n"
@@ -4419,6 +4533,282 @@ int plan_brush_texture_list(int argc, char** argv)
return 0;
}
pp::foundation::Result<pp::app::BrushStrokeFloatSetting> parse_brush_stroke_float_setting(
std::string_view setting)
{
static constexpr std::array<std::pair<std::string_view, pp::app::BrushStrokeFloatSetting>, 32> settings{ {
{ "tip-size", pp::app::BrushStrokeFloatSetting::tip_size },
{ "tip-spacing", pp::app::BrushStrokeFloatSetting::tip_spacing },
{ "tip-flow", pp::app::BrushStrokeFloatSetting::tip_flow },
{ "tip-opacity", pp::app::BrushStrokeFloatSetting::tip_opacity },
{ "tip-angle", pp::app::BrushStrokeFloatSetting::tip_angle },
{ "tip-angle-smooth", pp::app::BrushStrokeFloatSetting::tip_angle_smooth },
{ "tip-mix", pp::app::BrushStrokeFloatSetting::tip_mix },
{ "tip-wet", pp::app::BrushStrokeFloatSetting::tip_wet },
{ "tip-noise", pp::app::BrushStrokeFloatSetting::tip_noise },
{ "tip-hue", pp::app::BrushStrokeFloatSetting::tip_hue },
{ "tip-saturation", pp::app::BrushStrokeFloatSetting::tip_saturation },
{ "tip-sat", pp::app::BrushStrokeFloatSetting::tip_saturation },
{ "tip-value", pp::app::BrushStrokeFloatSetting::tip_value },
{ "tip-val", pp::app::BrushStrokeFloatSetting::tip_value },
{ "jitter-scale", pp::app::BrushStrokeFloatSetting::jitter_scale },
{ "jitter-angle", pp::app::BrushStrokeFloatSetting::jitter_angle },
{ "jitter-scatter", pp::app::BrushStrokeFloatSetting::jitter_scatter },
{ "jitter-flow", pp::app::BrushStrokeFloatSetting::jitter_flow },
{ "jitter-opacity", pp::app::BrushStrokeFloatSetting::jitter_opacity },
{ "jitter-hue", pp::app::BrushStrokeFloatSetting::jitter_hue },
{ "jitter-saturation", pp::app::BrushStrokeFloatSetting::jitter_saturation },
{ "jitter-sat", pp::app::BrushStrokeFloatSetting::jitter_saturation },
{ "jitter-value", pp::app::BrushStrokeFloatSetting::jitter_value },
{ "jitter-val", pp::app::BrushStrokeFloatSetting::jitter_value },
{ "jitter-aspect", pp::app::BrushStrokeFloatSetting::jitter_aspect },
{ "dual-size", pp::app::BrushStrokeFloatSetting::dual_size },
{ "dual-spacing", pp::app::BrushStrokeFloatSetting::dual_spacing },
{ "dual-scatter", pp::app::BrushStrokeFloatSetting::dual_scatter },
{ "tip-aspect", pp::app::BrushStrokeFloatSetting::tip_aspect },
{ "dual-opacity", pp::app::BrushStrokeFloatSetting::dual_opacity },
{ "dual-flow", pp::app::BrushStrokeFloatSetting::dual_flow },
{ "dual-rotate", pp::app::BrushStrokeFloatSetting::dual_rotate },
} };
for (const auto& entry : settings) {
if (setting == entry.first) {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(entry.second);
}
}
if (setting == "pattern-scale") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_scale);
}
if (setting == "pattern-brightness") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_brightness);
}
if (setting == "pattern-contrast") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_contrast);
}
if (setting == "pattern-depth") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_depth);
}
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke float setting"));
}
pp::foundation::Result<pp::app::BrushStrokeBoolSetting> parse_brush_stroke_bool_setting(
std::string_view setting)
{
static constexpr std::array<std::pair<std::string_view, pp::app::BrushStrokeBoolSetting>, 25> settings{ {
{ "tip-angle-init", pp::app::BrushStrokeBoolSetting::tip_angle_init },
{ "tip-angle-follow", pp::app::BrushStrokeBoolSetting::tip_angle_follow },
{ "tip-flow-pressure", pp::app::BrushStrokeBoolSetting::tip_flow_pressure },
{ "tip-opacity-pressure", pp::app::BrushStrokeBoolSetting::tip_opacity_pressure },
{ "tip-size-pressure", pp::app::BrushStrokeBoolSetting::tip_size_pressure },
{ "jitter-scatter-both-axis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis },
{ "jitter-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis },
{ "jitter-aspect-both-axis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis },
{ "jitter-aspect-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis },
{ "jitter-hsv-each-sample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample },
{ "jitter-hsv-eachsample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample },
{ "tip-invert", pp::app::BrushStrokeBoolSetting::tip_invert },
{ "tip-flip-x", pp::app::BrushStrokeBoolSetting::tip_flip_x },
{ "tip-flipx", pp::app::BrushStrokeBoolSetting::tip_flip_x },
{ "tip-flip-y", pp::app::BrushStrokeBoolSetting::tip_flip_y },
{ "tip-flipy", pp::app::BrushStrokeBoolSetting::tip_flip_y },
{ "pattern-enabled", pp::app::BrushStrokeBoolSetting::pattern_enabled },
{ "dual-enabled", pp::app::BrushStrokeBoolSetting::dual_enabled },
{ "dual-scatter-both-axis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis },
{ "dual-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis },
{ "dual-invert", pp::app::BrushStrokeBoolSetting::dual_invert },
{ "dual-flip-x", pp::app::BrushStrokeBoolSetting::dual_flip_x },
{ "dual-flipx", pp::app::BrushStrokeBoolSetting::dual_flip_x },
{ "dual-flip-y", pp::app::BrushStrokeBoolSetting::dual_flip_y },
{ "dual-flipy", pp::app::BrushStrokeBoolSetting::dual_flip_y },
} };
for (const auto& entry : settings) {
if (setting == entry.first) {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(entry.second);
}
}
if (setting == "dual-random-flip" || setting == "dual-randflip") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::dual_random_flip);
}
if (setting == "tip-random-flip-x" || setting == "tip-randflipx") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::tip_random_flip_x);
}
if (setting == "tip-random-flip-y" || setting == "tip-randflipy") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::tip_random_flip_y);
}
if (setting == "pattern-each-sample" || setting == "pattern-eachsample") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_each_sample);
}
if (setting == "pattern-invert") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_invert);
}
if (setting == "pattern-flip-x" || setting == "pattern-flipx") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_flip_x);
}
if (setting == "pattern-flip-y" || setting == "pattern-flipy") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_flip_y);
}
if (setting == "pattern-random-offset" || setting == "pattern-rand-offset") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_random_offset);
}
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke bool setting"));
}
pp::foundation::Result<pp::app::BrushStrokeBlendSetting> parse_brush_stroke_blend_setting(
std::string_view setting)
{
if (setting == "tip" || setting == "brush") {
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::success(
pp::app::BrushStrokeBlendSetting::tip);
}
if (setting == "dual") {
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::success(
pp::app::BrushStrokeBlendSetting::dual);
}
if (setting == "pattern") {
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::success(
pp::app::BrushStrokeBlendSetting::pattern);
}
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke blend setting"));
}
pp::foundation::Status parse_plan_brush_stroke_control_args(
int argc,
char** argv,
PlanBrushStrokeControlArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--kind" || key == "--setting") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
if (key == "--kind") {
args.kind = argv[++i];
} else {
args.setting = argv[++i];
}
} else if (key == "--value") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_float_arg(argv[++i]);
if (!value) {
return value.status();
}
args.value = value.value();
} else if (key == "--blend-mode") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_i32_arg(argv[++i]);
if (!value) {
return value.status();
}
args.blend_mode = value.value();
} else if (key == "--enabled") {
args.enabled = true;
} else if (key == "--disabled") {
args.enabled = false;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
pp::foundation::Result<pp::app::BrushStrokeControlPlan> make_brush_stroke_control_plan(
const PlanBrushStrokeControlArgs& args)
{
if (args.kind == "float" || args.kind == "slider") {
const auto setting = parse_brush_stroke_float_setting(args.setting);
if (!setting) {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::failure(setting.status());
}
return pp::app::plan_brush_stroke_float_setting(setting.value(), args.value);
}
if (args.kind == "bool" || args.kind == "toggle" || args.kind == "checkbox") {
const auto setting = parse_brush_stroke_bool_setting(args.setting);
if (!setting) {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::failure(setting.status());
}
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::success(
pp::app::plan_brush_stroke_bool_setting(setting.value(), args.enabled));
}
if (args.kind == "blend" || args.kind == "blend-mode") {
const auto setting = parse_brush_stroke_blend_setting(args.setting);
if (!setting) {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::failure(setting.status());
}
return pp::app::plan_brush_stroke_blend_mode(setting.value(), args.blend_mode);
}
if (args.kind == "tip-aspect-reset") {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::success(
pp::app::plan_brush_tip_aspect_reset());
}
if (args.kind == "default-reset" || args.kind == "reset") {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::success(
pp::app::plan_brush_default_settings_reset());
}
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke control kind"));
}
int plan_brush_stroke_control(int argc, char** argv)
{
PlanBrushStrokeControlArgs args;
const auto status = parse_plan_brush_stroke_control_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-brush-stroke-control", status.message);
return 2;
}
const auto plan = make_brush_stroke_control_plan(args);
if (!plan) {
print_error("plan-brush-stroke-control", plan.status().message);
return 2;
}
const auto& value = plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-brush-stroke-control\""
<< ",\"state\":{\"kind\":\"" << json_escape(args.kind)
<< "\",\"setting\":\"" << json_escape(args.setting)
<< "\",\"value\":" << args.value
<< ",\"enabled\":" << json_bool(args.enabled)
<< ",\"blendMode\":" << args.blend_mode
<< "},\"plan\":{\"operation\":\"" << brush_stroke_control_operation_name(value.operation)
<< "\",\"floatSetting\":\"" << brush_stroke_float_setting_name(value.float_setting)
<< "\",\"boolSetting\":\"" << brush_stroke_bool_setting_name(value.bool_setting)
<< "\",\"blendSetting\":\"" << brush_stroke_blend_setting_name(value.blend_setting)
<< "\",\"floatValue\":" << value.float_value
<< ",\"boolValue\":" << json_bool(value.bool_value)
<< ",\"blendMode\":" << value.blend_mode
<< ",\"mutatesBrush\":" << json_bool(value.mutates_brush)
<< ",\"updatesControls\":" << json_bool(value.updates_controls)
<< ",\"refreshesPreview\":" << json_bool(value.refreshes_preview)
<< ",\"notifiesStrokeChange\":" << json_bool(value.notifies_stroke_change)
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_canvas_tool_args(
int argc,
char** argv,
@@ -7545,6 +7935,10 @@ int main(int argc, char** argv)
return plan_brush_texture_list(argc, argv);
}
if (command == "plan-brush-stroke-control") {
return plan_brush_stroke_control(argc, argv);
}
if (command == "plan-canvas-tool") {
return plan_canvas_tool(argc, argv);
}