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)] {