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

@@ -1156,6 +1156,42 @@ if(TARGET pano_cli)
LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_brush_stroke_control_float_smoke
COMMAND pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5)
set_tests_properties(pano_cli_plan_brush_stroke_control_float_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"set-float\".*\"floatSetting\":\"tip-size\".*\"floatValue\":42.5.*\"mutatesBrush\":true.*\"notifiesStrokeChange\":true")
add_test(NAME pano_cli_plan_brush_stroke_control_toggle_smoke
COMMAND pano_cli plan-brush-stroke-control --kind bool --setting dual-enabled --enabled)
set_tests_properties(pano_cli_plan_brush_stroke_control_toggle_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"set-bool\".*\"boolSetting\":\"dual-enabled\".*\"boolValue\":true.*\"refreshesPreview\":true")
add_test(NAME pano_cli_plan_brush_stroke_control_blend_smoke
COMMAND pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3)
set_tests_properties(pano_cli_plan_brush_stroke_control_blend_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"set-blend-mode\".*\"blendSetting\":\"pattern\".*\"blendMode\":3")
add_test(NAME pano_cli_plan_brush_stroke_control_reset_smoke
COMMAND pano_cli plan-brush-stroke-control --kind default-reset)
set_tests_properties(pano_cli_plan_brush_stroke_control_reset_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"reset-default-brush\".*\"updatesControls\":true.*\"notifiesStrokeChange\":true")
add_test(NAME pano_cli_plan_brush_stroke_control_rejects_bad_setting
COMMAND pano_cli plan-brush-stroke-control --kind float --setting imaginary --value 1)
set_tests_properties(pano_cli_plan_brush_stroke_control_rejects_bad_setting PROPERTIES
LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_brush_stroke_control_rejects_bad_blend
COMMAND pano_cli plan-brush-stroke-control --kind blend --setting tip --blend-mode 99)
set_tests_properties(pano_cli_plan_brush_stroke_control_rejects_bad_blend PROPERTIES
LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_canvas_tool_draw_smoke
COMMAND pano_cli plan-canvas-tool --kind draw)
set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES

View File

@@ -135,6 +135,80 @@ public:
std::string call_order;
};
class FakeBrushStrokeControlServices final : public pp::app::BrushStrokeControlServices {
public:
void set_float_setting(pp::app::BrushStrokeFloatSetting setting, float value) override
{
float_sets += 1;
last_float_setting = setting;
last_float_value = value;
call_order += "float;";
}
void set_bool_setting(pp::app::BrushStrokeBoolSetting setting, bool value) override
{
bool_sets += 1;
last_bool_setting = setting;
last_bool_value = value;
call_order += "bool;";
}
void set_blend_mode(pp::app::BrushStrokeBlendSetting setting, int blend_mode) override
{
blend_sets += 1;
last_blend_setting = setting;
last_blend_mode = blend_mode;
call_order += "blend;";
}
void reset_tip_aspect(float value) override
{
tip_aspect_resets += 1;
last_float_value = value;
call_order += "tip-aspect;";
}
void reset_default_brush() override
{
default_resets += 1;
call_order += "default;";
}
void update_stroke_controls() override
{
control_updates += 1;
call_order += "controls;";
}
void refresh_stroke_preview() override
{
preview_refreshes += 1;
call_order += "preview;";
}
void notify_stroke_changed() override
{
stroke_notifications += 1;
call_order += "notify;";
}
int float_sets = 0;
int bool_sets = 0;
int blend_sets = 0;
int tip_aspect_resets = 0;
int default_resets = 0;
int control_updates = 0;
int preview_refreshes = 0;
int stroke_notifications = 0;
pp::app::BrushStrokeFloatSetting last_float_setting = pp::app::BrushStrokeFloatSetting::tip_size;
pp::app::BrushStrokeBoolSetting last_bool_setting = pp::app::BrushStrokeBoolSetting::tip_angle_init;
pp::app::BrushStrokeBlendSetting last_blend_setting = pp::app::BrushStrokeBlendSetting::tip;
float last_float_value = 0.0F;
bool last_bool_value = false;
int last_blend_mode = 0;
std::string call_order;
};
void color_plan_validates_all_channels(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_brush_ui_color(0.25F, 0.5F, 0.75F, 1.0F);
@@ -202,6 +276,58 @@ void stroke_settings_plan_updates_brush_preview(pp::tests::Harness& harness)
PP_EXPECT(harness, !plan.loads_brush_resources);
}
void stroke_control_plans_validate_values_and_reject_breaking_points(pp::tests::Harness& harness)
{
const auto slider = pp::app::plan_brush_stroke_float_setting(
pp::app::BrushStrokeFloatSetting::tip_size,
42.5F);
PP_EXPECT(harness, slider);
if (slider) {
PP_EXPECT(harness, slider.value().operation == pp::app::BrushStrokeControlOperation::set_float);
PP_EXPECT(harness, slider.value().float_setting == pp::app::BrushStrokeFloatSetting::tip_size);
PP_EXPECT(harness, slider.value().float_value == 42.5F);
PP_EXPECT(harness, slider.value().mutates_brush);
PP_EXPECT(harness, slider.value().refreshes_preview);
PP_EXPECT(harness, slider.value().notifies_stroke_change);
}
const auto checkbox = pp::app::plan_brush_stroke_bool_setting(
pp::app::BrushStrokeBoolSetting::pattern_enabled,
true);
PP_EXPECT(harness, checkbox.operation == pp::app::BrushStrokeControlOperation::set_bool);
PP_EXPECT(harness, checkbox.bool_setting == pp::app::BrushStrokeBoolSetting::pattern_enabled);
PP_EXPECT(harness, checkbox.bool_value);
const auto blend = pp::app::plan_brush_stroke_blend_mode(
pp::app::BrushStrokeBlendSetting::dual,
7);
PP_EXPECT(harness, blend);
if (blend) {
PP_EXPECT(harness, blend.value().operation == pp::app::BrushStrokeControlOperation::set_blend_mode);
PP_EXPECT(harness, blend.value().blend_setting == pp::app::BrushStrokeBlendSetting::dual);
PP_EXPECT(harness, blend.value().blend_mode == 7);
}
const auto tip_aspect = pp::app::plan_brush_tip_aspect_reset();
PP_EXPECT(harness, tip_aspect.operation == pp::app::BrushStrokeControlOperation::reset_tip_aspect);
PP_EXPECT(harness, tip_aspect.float_setting == pp::app::BrushStrokeFloatSetting::tip_aspect);
PP_EXPECT(harness, tip_aspect.float_value == 0.5F);
const auto defaults = pp::app::plan_brush_default_settings_reset();
PP_EXPECT(harness, defaults.operation == pp::app::BrushStrokeControlOperation::reset_default_brush);
PP_EXPECT(harness, defaults.updates_controls);
PP_EXPECT(harness, defaults.refreshes_preview);
PP_EXPECT(harness, defaults.notifies_stroke_change);
PP_EXPECT(
harness,
!pp::app::plan_brush_stroke_float_setting(
pp::app::BrushStrokeFloatSetting::tip_flow,
std::nanf("")));
PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, -1));
PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 64));
}
void texture_list_add_plans_target_paths_and_rejects_bad_input(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_brush_texture_list_add(
@@ -388,6 +514,60 @@ void texture_list_executor_dispatches_and_preserves_failure(pp::tests::Harness&
PP_EXPECT(harness, failing_services.call_order == "add-failed;");
}
void stroke_control_executor_dispatches_and_rejects_bad_payloads(pp::tests::Harness& harness)
{
FakeBrushStrokeControlServices services;
const auto slider = pp::app::plan_brush_stroke_float_setting(
pp::app::BrushStrokeFloatSetting::pattern_depth,
0.75F);
PP_EXPECT(harness, slider);
if (slider) {
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(slider.value(), services).ok());
}
const auto checkbox = pp::app::plan_brush_stroke_bool_setting(
pp::app::BrushStrokeBoolSetting::dual_enabled,
true);
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(checkbox, services).ok());
const auto blend = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 3);
PP_EXPECT(harness, blend);
if (blend) {
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(blend.value(), services).ok());
}
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset(), services).ok());
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_default_settings_reset(), services).ok());
PP_EXPECT(harness, services.float_sets == 1);
PP_EXPECT(harness, services.last_float_setting == pp::app::BrushStrokeFloatSetting::pattern_depth);
PP_EXPECT(harness, services.last_bool_setting == pp::app::BrushStrokeBoolSetting::dual_enabled);
PP_EXPECT(harness, services.last_blend_setting == pp::app::BrushStrokeBlendSetting::pattern);
PP_EXPECT(harness, services.last_blend_mode == 3);
PP_EXPECT(harness, services.tip_aspect_resets == 1);
PP_EXPECT(harness, services.default_resets == 1);
PP_EXPECT(harness, services.preview_refreshes == 5);
PP_EXPECT(harness, services.control_updates == 1);
PP_EXPECT(harness, services.stroke_notifications == 5);
PP_EXPECT(
harness,
services.call_order == "float;preview;notify;bool;preview;notify;blend;preview;notify;tip-aspect;"
"preview;notify;default;controls;preview;notify;");
FakeBrushStrokeControlServices bad_services;
pp::app::BrushStrokeControlPlan bad_float;
bad_float.operation = pp::app::BrushStrokeControlOperation::set_float;
bad_float.float_value = std::nanf("");
PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_float, bad_services).ok());
pp::app::BrushStrokeControlPlan bad_blend;
bad_blend.operation = pp::app::BrushStrokeControlOperation::set_blend_mode;
bad_blend.blend_mode = 99;
PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_blend, bad_services).ok());
PP_EXPECT(harness, bad_services.call_order.empty());
}
void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness)
{
FakeBrushUiServices services;
@@ -434,12 +614,14 @@ int main()
harness.run("texture plan validates path and slot", texture_plan_validates_path_and_slot);
harness.run("preset plan preserves color and requires brush", preset_plan_preserves_color_and_requires_brush);
harness.run("stroke settings plan updates brush preview", stroke_settings_plan_updates_brush_preview);
harness.run("stroke control plans validate values and reject breaking points", stroke_control_plans_validate_values_and_reject_breaking_points);
harness.run("texture list add plans target paths and rejects bad input", texture_list_add_plans_target_paths_and_rejects_bad_input);
harness.run("texture list remove and move plans handle edges", texture_list_remove_and_move_plans_handle_edges);
harness.run("executor dispatches color and refresh", executor_dispatches_color_and_refresh);
harness.run("executor dispatches texture and preset", executor_dispatches_texture_and_preset);
harness.run("executor dispatches stroke refresh only", executor_dispatches_stroke_refresh_only);
harness.run("texture list executor dispatches and preserves failure", texture_list_executor_dispatches_and_preserves_failure);
harness.run("stroke control executor dispatches and rejects bad payloads", stroke_control_executor_dispatches_and_rejects_bad_payloads);
harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads);
return harness.finish();
}