mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
@@ -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};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)] {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user