G2: Scribble effect Settings presets

Squashed from #4274 onto main.
This commit is contained in:
nrq6993-a11y
2026-10-27 14:36:12 +07:00
committed by bflatastic
parent ca60c8e8a7
commit dfec712c2d
5 changed files with 128 additions and 1 deletions
+1
View File
@@ -86,6 +86,7 @@ pub use solids::{
EXTRUDE, INFLATE, ROTATE, ROTATION_PRESETS, SOLIDS, extrude_options, has_solid, inflate_options, is_three_d, solid_art, validate_solid,
validate_three_d, view_options,
};
pub use stylize::{ScribbleSetting, scribble_setting_id, scribble_settings};
pub use trap::TRAP;
pub use warp::{WarpStyle, warp_point};
+49 -1
View File
@@ -1,6 +1,6 @@
//! Convert to Shape, Round Corners and Scribble.
use serde_json::Value;
use serde_json::{Value, json};
use vectorcraft_geom::kurbo;
use vectorcraft_geom::shapes::{self, KAPPA};
use vectorcraft_geom::{Affine, Anchor, PathData, Point, Rect, SubPath, corners};
@@ -144,6 +144,54 @@ fn crosses_outline(p: Point, q: Point, polys: &[Vec<Point>]) -> bool {
})
}
/// One named Settings entry in Effect › Stylize › Scribble (public feature names; parameter sets
/// are chosen for VectorCraft).
pub struct ScribbleSetting {
pub id: &'static str,
pub label: &'static str,
pub params: Value,
}
/// The Scribble dialog's Settings presets, in menu order.
pub fn scribble_settings() -> &'static [ScribbleSetting] {
static PRESETS: std::sync::OnceLock<Vec<ScribbleSetting>> = std::sync::OnceLock::new();
PRESETS.get_or_init(|| {
[
("default", "Default", json!({"angle": 30.0, "overlap": 0.0, "overlapVariation": 0.5, "strokeWidth": 3.0, "curviness": 5.0, "curvinessVariation": 0.0, "spacing": 5.0, "spacingVariation": 0.5})),
("childlike", "Childlike", json!({"angle": 45.0, "overlap": 2.0, "overlapVariation": 2.0, "strokeWidth": 2.0, "curviness": 15.0, "curvinessVariation": 30.0, "spacing": 4.0, "spacingVariation": 1.5})),
("dense", "Dense", json!({"angle": 30.0, "overlap": 0.0, "overlapVariation": 0.0, "strokeWidth": 3.0, "curviness": 0.0, "curvinessVariation": 0.0, "spacing": 2.0, "spacingVariation": 0.0})),
("loose", "Loose", json!({"angle": 30.0, "overlap": 0.0, "overlapVariation": 1.0, "strokeWidth": 2.0, "curviness": 20.0, "curvinessVariation": 40.0, "spacing": 12.0, "spacingVariation": 3.0})),
("moire", "Moire", json!({"angle": 0.0, "overlap": 0.0, "overlapVariation": 0.0, "strokeWidth": 1.0, "curviness": 80.0, "curvinessVariation": 10.0, "spacing": 3.0, "spacingVariation": 0.5})),
("sharp", "Sharp", json!({"angle": 30.0, "overlap": 0.0, "overlapVariation": 0.0, "strokeWidth": 2.0, "curviness": 0.0, "curvinessVariation": 0.0, "spacing": 3.0, "spacingVariation": 0.0})),
("sketch", "Sketch", json!({"angle": 30.0, "overlap": -1.0, "overlapVariation": 0.5, "strokeWidth": 2.0, "curviness": 10.0, "curvinessVariation": 15.0, "spacing": 6.0, "spacingVariation": 1.0})),
("snarl", "Snarl", json!({"angle": 15.0, "overlap": 3.0, "overlapVariation": 3.0, "strokeWidth": 4.0, "curviness": 40.0, "curvinessVariation": 50.0, "spacing": 8.0, "spacingVariation": 4.0})),
("swash", "Swash", json!({"angle": 60.0, "overlap": 5.0, "overlapVariation": 2.0, "strokeWidth": 5.0, "curviness": 30.0, "curvinessVariation": 20.0, "spacing": 10.0, "spacingVariation": 2.0})),
("tight", "Tight", json!({"angle": 30.0, "overlap": 0.0, "overlapVariation": 0.0, "strokeWidth": 3.0, "curviness": 5.0, "curvinessVariation": 0.0, "spacing": 2.0, "spacingVariation": 0.2})),
("zigzag", "Zigzag", json!({"angle": 90.0, "overlap": 0.0, "overlapVariation": 0.0, "strokeWidth": 3.0, "curviness": 0.0, "curvinessVariation": 0.0, "spacing": 8.0, "spacingVariation": 0.0})),
]
.into_iter()
.map(|(id, label, params)| ScribbleSetting { id, label, params })
.collect()
})
}
/// Which Settings preset matches `params` (seed ignored), if any.
pub fn scribble_setting_id(params: &Value) -> Option<&'static str> {
let obj = params.as_object()?;
scribble_settings().iter().find(|p| p.params.as_object().is_some_and(|want| scribble_params_match(obj, want))).map(|p| p.id)
}
fn scribble_params_match(have: &serde_json::Map<String, Value>, want: &serde_json::Map<String, Value>) -> bool {
want.iter().all(|(k, v)| have.get(k).is_some_and(|a| scribble_param_eq(a, v)))
}
fn scribble_param_eq(a: &Value, b: &Value) -> bool {
match (a.as_f64(), b.as_f64()) {
(Some(x), Some(y)) => (x - y).abs() < 1e-6,
_ => a == b,
}
}
/// Scribble (simplified): a zig-zag hatching across the filled area at `angle`, one per region
/// of the shape, outlined to `strokeWidth` so the fill paints it. `overlapVariation` moves each
/// line's ends along it by up to that many points, `spacingVariation` moves them across it, and
+11
View File
@@ -925,6 +925,17 @@ fn scribble_reads_its_earlier_single_variation() {
assert!(default_params("stylize.scribble").unwrap().get("variation").is_none());
}
#[test]
fn scribble_settings_presets_are_distinct_and_recognized() {
let presets = crate::scribble_settings();
assert_eq!(presets.len(), 11);
assert_eq!(presets[0].id, "default");
assert_eq!(crate::scribble_setting_id(&presets[0].params), Some("default"));
assert_eq!(crate::scribble_setting_id(&presets[9].params), Some("tight"));
let custom = json!({"angle": 30.0, "overlap": 0.0, "overlapVariation": 0.5, "strokeWidth": 3.0, "curviness": 5.0, "curvinessVariation": 0.0, "spacing": 4.0, "spacingVariation": 0.5});
assert_eq!(crate::scribble_setting_id(&custom), None);
}
#[test]
fn scribble_angle_is_stored_above_minus_180_and_up_to_180() {
for (given, stored) in [(270.0, -90.0), (-180.0, 180.0), (180.0, 180.0), (540.0, 180.0), (-270.0, 90.0), (-200.0, 160.0)] {
+48
View File
@@ -161,6 +161,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
None if id == GLASS => glass_fields(app, ui, d, GLASS),
None if id == TEXTURIZER => glass_fields(app, ui, d, TEXTURIZER),
None if id == TRANSFORM => transform_fields(ui, d, app.session.general_unit()),
None if id == SCRIBBLE => scribble_fields(ui, d, app.session.general_unit()),
None if id == ZIG_ZAG => zig_zag_fields(ui, d, app.session.general_unit()),
None if id == DROP_SHADOW => drop_shadow_fields(ui, d, app.session.general_unit()),
None if id == "artistic.filmGrain" => film_grain_fields(ui, d),
@@ -478,6 +479,53 @@ fn values_changed(d: &mut Dialog) -> bool {
/// The Transform effect (Effect › Distort & Transform › Transform…).
const TRANSFORM: &str = "distort.transform";
/// Scribble (Effect › Stylize › Scribble…).
const SCRIBBLE: &str = "stylize.scribble";
/// Dialog-only: which Settings preset is selected (`custom` when the values match none).
const SCRIBBLE_SETTING: &str = "__scribbleSetting";
/// Scribble's dialog: a Settings preset menu, then its parameters. Returns whether the values differ
/// from those last previewed.
fn scribble_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Unit) -> bool {
if !d.fields.contains_key(SCRIBBLE_SETTING) {
let id = vectorcraft_effects::scribble_setting_id(&form::params(d)).unwrap_or("custom");
d.fields.insert(SCRIBBLE_SETTING.into(), json!(id));
}
let presets = vectorcraft_effects::scribble_settings();
let cur = d.fields.get(SCRIBBLE_SETTING).and_then(Value::as_str).unwrap_or("default");
let labels: Vec<&str> = presets.iter().map(|p| p.label).chain(["Custom"]).collect();
let ids: Vec<&str> = presets.iter().map(|p| p.id).chain(["custom"]).collect();
let current = if cur == "custom" { "Custom" } else { presets.iter().find(|p| p.id == cur).map(|p| p.label).unwrap_or(labels[0]) };
widgets::subheader(ui, tl!("Settings"));
let mut settings_changed = false;
ui.horizontal(|ui| {
if let Some(i) = widgets::dropdown(ui, "fx-scribble-setting", current, &labels, 160.0) {
let picked = ids[i];
d.fields.insert(SCRIBBLE_SETTING.into(), json!(picked));
if picked != "custom"
&& let Some(preset) = presets.iter().find(|p| p.id == picked)
&& let Some(m) = preset.params.as_object()
{
for (k, v) in m {
d.fields.insert(k.clone(), v.clone());
}
}
settings_changed = true;
}
});
ui.add_space(4.0);
let doc = vectorcraft_effects::effect_info(SCRIBBLE).map(|e| e.params).unwrap_or_default();
let fields_changed =
form::param_fields(ui, d, &|k| vectorcraft_effects::is_length(SCRIBBLE, k, false), &|k| choices(SCRIBBLE, k), &|k| doc_rank(doc, k), unit);
if let Value::Object(m) = vectorcraft_effects::upgrade_params(SCRIBBLE, &Value::Object(d.fields.clone())) {
d.fields = m;
}
let id = vectorcraft_effects::scribble_setting_id(&form::params(d)).unwrap_or("custom");
d.fields.insert(SCRIBBLE_SETTING.into(), json!(id));
settings_changed || fields_changed
}
/// The Transform effect's dialog: Transform Each's Scale and Move sliders and Rotate dial, then
/// the options Transform Objects, Transform Patterns and Scale Strokes & Effects (#885), Copies,
/// the reflections, the reference point and Random. Returns whether the values differ from those
+19
View File
@@ -276,6 +276,25 @@ fn effect_dialog_applies_to_its_appearance_item() {
assert!(n.appearance.effects.is_empty() && n.appearance.items[0].effects().is_empty());
}
#[test]
fn scribble_dialog_settings_presets_load_parameters() {
let mut app = app();
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
app.run("select.all", json!({})).unwrap();
app.run("effect.dialog", json!({"effect": "stylize.scribble"})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
assert!(text.lines().any(|l| l == "Settings"), "{text}");
let tight = vectorcraft_effects::scribble_settings().iter().find(|p| p.id == "tight").unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("__scribbleSetting".into(), json!("tight"));
for (k, v) in tight.params.as_object().unwrap() {
app.ui.dialog.as_mut().unwrap().fields.insert(k.clone(), v.clone());
}
frame(&mut app, Default::default());
assert_eq!(app.ui.dialog.as_ref().unwrap().f64("spacing", 0.0), 2.0);
confirm(&mut app).unwrap();
assert_eq!(app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].params["spacing"], json!(2.0));
}
#[test]
fn scribble_dialog_shows_the_angle_as_stored_and_each_variation() {
let mut app = app();