mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Effects: Scribble has a Variation for Path Overlap, Curviness and Spacing, and stores 270° as -90°
The reference app's Scribble Options have a Variation for each of Path Overlap, Curviness and Spacing. Scribble had one `variation`, which moved each line's ends both along the line and across it, and every turn of a scribble took the same curviness. An angle such as 270 was stored as given. Scribble now takes `overlapVariation` (pt, default 0.5), which moves the ends along the line, `spacingVariation` (pt, 0.5), which moves them across it, and `curvinessVariation` (%, 0), which varies the curviness from one turn to the next. The dialog shows each Variation after the option it varies, and the Appearance panel lists all three. A Scribble saved or scripted with the earlier `variation` reads it as the Path Overlap and Spacing variations. With the defaults, and with the earlier `variation`, Scribble draws the same outline as before. `effect.apply` and `effect.setParams` store an angle above -180 and up to 180, so 270 is stored as -90, and the dialog shows the stored value. The Appearance panel's inline editor lists an effect's values in their current form, as the dialog does. It listed an earlier key next to the defaults of the keys that replace it. Docs: docs/effects-parity.md (Stylize row), docs/target-app-parity.md.
This commit is contained in:
@@ -134,7 +134,7 @@ fn lengths_of(id: &str) -> Lengths {
|
||||
"threeD.inflate" => always(&["depth"]),
|
||||
"path.outlineStroke" => always(&["width"]),
|
||||
"stylize.roundCorners" | "stylize.feather" | "blur.gaussian" | "blur.smart" | "sharpen.unsharpMask" => always(&["radius"]),
|
||||
"stylize.scribble" => always(&["overlap", "strokeWidth", "spacing", "variation"]),
|
||||
"stylize.scribble" => always(&["overlap", "overlapVariation", "strokeWidth", "spacing", "spacingVariation"]),
|
||||
"stylize.dropShadow" => always(&["x", "y", "blur"]),
|
||||
"stylize.innerGlow" | "stylize.outerGlow" => always(&["blur"]),
|
||||
"stylize.glowingEdges" => always(&["edgeWidth", "smoothness"]),
|
||||
@@ -301,8 +301,8 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"stylize.scribble",
|
||||
"Scribble…",
|
||||
STYLIZE,
|
||||
"{angle: deg (30), overlap: pt (0), strokeWidth: pt (3), curviness: % (5), spacing: pt (5), variation: pt (0.5), seed: int (0)} (simplified: a single hatching scribble outlined to a filled shape)",
|
||||
json!({"angle": 30.0, "overlap": 0.0, "strokeWidth": 3.0, "curviness": 5.0, "spacing": 5.0, "variation": 0.5, "seed": 0}),
|
||||
"{angle: deg (30; stored above -180 and up to 180: 270 becomes -90, -180 becomes 180), overlap: pt (0; Path Overlap: a positive value extends each line beyond the path, a negative one ends it inside), overlapVariation: pt (0..100, 0.5; how far each line's ends move along it), strokeWidth: pt (3), curviness: % (5), curvinessVariation: % (0..100, 0; how far each turn's curviness moves from curviness), spacing: pt (5), spacingVariation: pt (0..100, 0.5; how far each line's ends move across it), variation: pt (the earlier form, read as overlapVariation and spacingVariation unless those are given), seed: int (0)} (simplified: a single hatching scribble outlined to a filled shape)",
|
||||
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, "seed": 0}),
|
||||
),
|
||||
r(
|
||||
"stylize.dropShadow",
|
||||
@@ -642,8 +642,9 @@ pub fn default_params(id: &str) -> Option<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `params` layered over the defaults of `id` (unknown keys are kept), with the 3D effects'
|
||||
/// earlier `perspective` read as `perspectiveAngle` (see [`upgrade_params`]).
|
||||
/// `params` layered over the defaults of `id` (unknown keys are kept), in their current form (see
|
||||
/// [`upgrade_params`]). The 3D effects' earlier `perspective` is read as `perspectiveAngle`, and
|
||||
/// Scribble's earlier `variation` is read as `overlapVariation` and `spacingVariation`.
|
||||
pub fn merged_params(id: &str, params: &Value) -> Value {
|
||||
let mut out = match catalog_index().get(id).map(|e| &e.defaults) {
|
||||
Some(Value::Object(m)) => m.clone(),
|
||||
@@ -660,14 +661,17 @@ pub fn merged_params(id: &str, params: &Value) -> Value {
|
||||
/// `params` for effect `id` in their current form. The 3D effects took Perspective as a 0–100 %
|
||||
/// strength, `perspective`, before the documented 0°–160° angle, `perspectiveAngle`, and the light
|
||||
/// as `lightAzimuth` and `lightElevation` before the documented Rotation and Height
|
||||
/// (`lightRotation`, `lightHeight`, `lightBehind`). Earlier values (from a file or a script of
|
||||
/// (`lightRotation`, `lightHeight`, `lightBehind`). Scribble took one `variation` before its
|
||||
/// separate `overlapVariation` and `spacingVariation`. Earlier values (from a file or a script of
|
||||
/// then) become the current ones that show exactly the same, unless those are given too, which win.
|
||||
/// Scribble's angle is also stored above -180° and up to 180° (270° becomes -90°).
|
||||
pub fn upgrade_params(id: &str, params: &Value) -> Value {
|
||||
match params {
|
||||
Value::Object(m) => {
|
||||
let mut m = m.clone();
|
||||
upgrade_perspective(id, &mut m);
|
||||
upgrade_light(id, &mut m);
|
||||
upgrade_scribble(id, &mut m);
|
||||
Value::Object(m)
|
||||
}
|
||||
other => other.clone(),
|
||||
@@ -700,6 +704,21 @@ fn upgrade_perspective(id: &str, m: &mut Map<String, Value>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade_scribble(id: &str, m: &mut Map<String, Value>) {
|
||||
if id != "stylize.scribble" {
|
||||
return;
|
||||
}
|
||||
if let Some(variation) = m.remove("variation") {
|
||||
for key in ["overlapVariation", "spacingVariation"] {
|
||||
m.entry(key).or_insert_with(|| variation.clone());
|
||||
}
|
||||
}
|
||||
if let Some(angle) = m.get("angle").and_then(Value::as_f64).filter(|a| a.is_finite() && (*a <= -180.0 || *a > 180.0)) {
|
||||
let wrapped = angle.rem_euclid(360.0);
|
||||
m.insert("angle".into(), json!(if wrapped > 180.0 { wrapped - 360.0 } else { wrapped }));
|
||||
}
|
||||
}
|
||||
|
||||
/// A new effect record with default parameters (overridden by `params`).
|
||||
pub fn new_effect(id: &str, params: &Value) -> Option<Effect> {
|
||||
if !catalog_index().contains_key(id) {
|
||||
|
||||
@@ -112,14 +112,19 @@ fn crosses_outline(p: Point, q: Point, polys: &[Vec<Point>]) -> bool {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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
|
||||
/// each turn's curviness moves from `curviness` by up to `curvinessVariation` percent.
|
||||
pub fn scribble(path: &PathData, b: Rect, p: &Value) -> PathData {
|
||||
let angle = num(p, "angle", 30.0).to_radians();
|
||||
let overlap = num(p, "overlap", 0.0);
|
||||
let width = num(p, "strokeWidth", 3.0).clamp(0.01, 1000.0);
|
||||
let curvy = num(p, "curviness", 5.0).clamp(0.0, 100.0) / 100.0;
|
||||
let curviness = num(p, "curviness", 5.0).clamp(0.0, 100.0);
|
||||
let curvy = curviness / 100.0;
|
||||
let curvy_var = num(p, "curvinessVariation", 0.0).clamp(0.0, 100.0);
|
||||
let spacing = num(p, "spacing", 5.0).max(0.1);
|
||||
let variation = num(p, "variation", 0.5).clamp(0.0, 100.0);
|
||||
let along = num(p, "overlapVariation", 0.5).clamp(0.0, 100.0);
|
||||
let across = num(p, "spacingVariation", 0.5).clamp(0.0, 100.0);
|
||||
let seed = seed(p);
|
||||
let c = b.center();
|
||||
let to_local = Affine::translate(c.to_vec2()) * Affine::rotate(angle) * Affine::translate(-c.to_vec2());
|
||||
@@ -140,6 +145,8 @@ pub fn scribble(path: &PathData, b: Rect, p: &Value) -> PathData {
|
||||
// stays inside: one chain, as before.
|
||||
struct Chain {
|
||||
pts: Vec<Point>,
|
||||
// Each point's curviness noise in [-1, 1].
|
||||
bends: Vec<f64>,
|
||||
end: Point,
|
||||
row: u64,
|
||||
}
|
||||
@@ -160,10 +167,12 @@ pub fn scribble(path: &PathData, b: Rect, p: &Value) -> PathData {
|
||||
let spans: Vec<(f64, f64)> = xs.as_chunks::<2>().0.iter().map(|s| (s[0] - overlap, s[1] + overlap)).collect();
|
||||
let open = chains.len();
|
||||
for (a, bx) in spans {
|
||||
let jitter = |k: u64| variation * noise(seed, row, k, 3);
|
||||
let (l, r) = (Point::new(a + jitter(0), y + jitter(1)), Point::new(bx + jitter(2), y + jitter(3)));
|
||||
let jitter = |k: u64, v: f64| v * noise(seed, row, k, 3);
|
||||
let (l, r) = (Point::new(a + jitter(0, along), y + jitter(1, across)), Point::new(bx + jitter(2, along), y + jitter(3, across)));
|
||||
let even = row.is_multiple_of(2);
|
||||
let pair = if even { [l, r] } else { [r, l] };
|
||||
let bend = [noise(seed, row, 4, 3), noise(seed, row, 5, 3)];
|
||||
let bend = if even { bend } else { [bend[1], bend[0]] };
|
||||
// Un-jittered ends of the span in drawing order.
|
||||
let (start, end) = if even { (Point::new(a, y), Point::new(bx, y)) } else { (Point::new(bx, y), Point::new(a, y)) };
|
||||
// The nearest chain of the previous row that this span can join without leaving
|
||||
@@ -176,9 +185,10 @@ pub fn scribble(path: &PathData, b: Rect, p: &Value) -> PathData {
|
||||
Some(k) => {
|
||||
let c = &mut chains[k];
|
||||
c.pts.extend(pair);
|
||||
c.bends.extend(bend);
|
||||
(c.end, c.row) = (end, row);
|
||||
}
|
||||
None => chains.push(Chain { pts: pair.to_vec(), end, row }),
|
||||
None => chains.push(Chain { pts: pair.to_vec(), bends: bend.to_vec(), end, row }),
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
@@ -187,7 +197,15 @@ pub fn scribble(path: &PathData, b: Rect, p: &Value) -> PathData {
|
||||
let lines: Vec<SubPath> = chains
|
||||
.into_iter()
|
||||
.filter(|c| c.pts.len() >= 2)
|
||||
.map(|c| if curvy > 0.0 { catmull_rom(&c.pts, false, curvy * 2.0) } else { SubPath::polyline(&c.pts, false) })
|
||||
.map(|c| {
|
||||
if curvy_var > 0.0 {
|
||||
varied_curves(&c.pts, &c.bends, curviness, curvy_var)
|
||||
} else if curvy > 0.0 {
|
||||
catmull_rom(&c.pts, false, curvy * 2.0)
|
||||
} else {
|
||||
SubPath::polyline(&c.pts, false)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if lines.is_empty() {
|
||||
return PathData::default();
|
||||
@@ -198,3 +216,17 @@ pub fn scribble(path: &PathData, b: Rect, p: &Value) -> PathData {
|
||||
let outline = kurbo::stroke(center.to_bezpath(), &stroke, &kurbo::StrokeOpts::default(), 0.05);
|
||||
PathData::from_bezpath(&outline)
|
||||
}
|
||||
|
||||
/// A scribble line through `pts` whose curviness at each point is `curviness` percent plus
|
||||
/// `variation` percent times that point's `bends` noise (in -1..1), kept within 0 to 100 percent.
|
||||
fn varied_curves(pts: &[Point], bends: &[f64], curviness: f64, variation: f64) -> SubPath {
|
||||
// A tension of 6 makes each handle the whole Catmull-Rom tangent; each point then scales its
|
||||
// own to the tension `catmull_rom` takes for that curviness (curviness / 100 × 2).
|
||||
let mut line = catmull_rom(pts, false, 6.0);
|
||||
for (a, bend) in line.anchors.iter_mut().zip(bends) {
|
||||
let k = (curviness + variation * bend).clamp(0.0, 100.0) / 300.0;
|
||||
a.h_in = a.p + (a.h_in - a.p) * k;
|
||||
a.h_out = a.p + (a.h_out - a.p) * k;
|
||||
}
|
||||
line
|
||||
}
|
||||
|
||||
@@ -539,6 +539,59 @@ fn scribble_produces_filled_outline_inside_bounds() {
|
||||
assert_eq!(out, run("stylize.scribble", json!({}), &square()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scribble_variations_are_separate() {
|
||||
// At 0° the lines run along x: the Path Overlap variation moves their ends in x only, the
|
||||
// Spacing variation moves them in y only, and the Curviness variation curves some of the turns.
|
||||
let base =
|
||||
json!({"angle": 0, "curviness": 0, "strokeWidth": 0.5, "spacing": 10, "overlapVariation": 0, "spacingVariation": 0, "curvinessVariation": 0});
|
||||
let with = |key: &str, v: f64| {
|
||||
let mut p = base.clone();
|
||||
p[key] = json!(v);
|
||||
run("stylize.scribble", p, &square())
|
||||
};
|
||||
let plain = run("stylize.scribble", base.clone(), &square());
|
||||
let b = plain.bounds().unwrap();
|
||||
let moved = |a: f64, b: f64| (a - b).abs() > 0.5;
|
||||
let along = with("overlapVariation", 4.0).bounds().unwrap();
|
||||
assert!(close(along.y0, b.y0, 0.06) && close(along.y1, b.y1, 0.06), "{along:?} {b:?}");
|
||||
assert!(moved(along.x0, b.x0) || moved(along.x1, b.x1), "{along:?} {b:?}");
|
||||
let across = with("spacingVariation", 4.0).bounds().unwrap();
|
||||
assert!(close(across.x0, b.x0, 0.06) && close(across.x1, b.x1, 0.06), "{across:?} {b:?}");
|
||||
assert!(moved(across.y0, b.y0) || moved(across.y1, b.y1), "{across:?} {b:?}");
|
||||
assert_ne!(with("curvinessVariation", 50.0), plain);
|
||||
// On a curviness of its own, the variation changes how much each turn curves.
|
||||
let mut p = base.clone();
|
||||
p["curviness"] = json!(20);
|
||||
let even = run("stylize.scribble", p.clone(), &square());
|
||||
p["curvinessVariation"] = json!(20);
|
||||
assert_ne!(run("stylize.scribble", p, &square()), even);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scribble_reads_its_earlier_single_variation() {
|
||||
// Before the separate variations, one `variation` moved the ends both along and across.
|
||||
let old = run("stylize.scribble", json!({"variation": 3}), &square());
|
||||
assert_eq!(old, run("stylize.scribble", json!({"overlapVariation": 3, "spacingVariation": 3}), &square()));
|
||||
assert_eq!(upgrade_params("stylize.scribble", &json!({"variation": 3})), json!({"overlapVariation": 3, "spacingVariation": 3}));
|
||||
// A current key given with it wins.
|
||||
assert_eq!(
|
||||
upgrade_params("stylize.scribble", &json!({"variation": 3, "spacingVariation": 1})),
|
||||
json!({"overlapVariation": 3, "spacingVariation": 1})
|
||||
);
|
||||
assert!(default_params("stylize.scribble").unwrap().get("variation").is_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)] {
|
||||
assert_eq!(new_effect("stylize.scribble", &json!({"angle": given})).unwrap().params["angle"].as_f64(), Some(stored), "{given}");
|
||||
}
|
||||
// An angle in range is kept as given; other effects' angles are not changed.
|
||||
assert_eq!(upgrade_params("stylize.scribble", &json!({"angle": 45})), json!({"angle": 45}));
|
||||
assert_eq!(upgrade_params("distort.twist", &json!({"angle": 270})), json!({"angle": 270}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effects_chain_in_order_and_skip_hidden_and_raster() {
|
||||
let chain = [fx("distort.transform", json!({"moveH": 10})), fx("distort.transform", json!({"scaleH": 200}))];
|
||||
|
||||
@@ -647,6 +647,24 @@ mod tests {
|
||||
assert_eq!(params(&s), json!({"lightRotation": 0.0, "lightHeight": 60, "lightBehind": false}));
|
||||
}
|
||||
|
||||
/// Scripts from before Scribble's separate variations give one `variation`, which is stored as the
|
||||
/// Path Overlap and Spacing variations. An angle is stored above -180° and up to 180°.
|
||||
#[test]
|
||||
fn scribble_stores_an_earlier_variation_as_two_and_its_angle_within_180() {
|
||||
let (mut s, id) = session_with_rect();
|
||||
let p = json!({"effect": "stylize.scribble", "ids": [id.0], "item": null, "params": {"angle": 270, "variation": 2}});
|
||||
s.execute("effect.apply", &p).unwrap();
|
||||
let params = |s: &Session| node(s, id).appearance.effects[0].params.clone();
|
||||
assert_eq!((params(&s)["angle"].as_f64(), params(&s).get("variation")), (Some(-90.0), None), "{}", params(&s));
|
||||
assert_eq!((¶ms(&s)["overlapVariation"], ¶ms(&s)["spacingVariation"]), (&json!(2), &json!(2)));
|
||||
s.execute("effect.setParams", &json!({"index": 0, "params": {"angle": -200}, "ids": [id.0], "item": null})).unwrap();
|
||||
assert_eq!(params(&s)["angle"].as_f64(), Some(160.0));
|
||||
// A file of then stores its variation; setting one of the two keeps it for the other.
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).node_mut(id).unwrap().appearance.effects[0].params = json!({"variation": 3});
|
||||
s.execute("effect.setParams", &json!({"index": 0, "params": {"spacingVariation": 1}, "ids": [id.0], "item": null})).unwrap();
|
||||
assert_eq!(params(&s), json!({"overlapVariation": 3, "spacingVariation": 1}));
|
||||
}
|
||||
|
||||
fn effects_for_test(s: &Session, id: NodeId) -> vectorcraft_doc::Node {
|
||||
effects::revolve_art(&node(s, id)).unwrap()
|
||||
}
|
||||
|
||||
@@ -148,7 +148,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
None => {
|
||||
let doc = vectorcraft_effects::effect_info(&id).map(|e| e.params).unwrap_or_default();
|
||||
let unit = app.session.general_unit();
|
||||
form::param_fields(ui, d, &|k| vectorcraft_effects::is_length(&id, k, relative), &|k| choices(&id, k), &|k| doc_rank(doc, k), unit)
|
||||
let changed =
|
||||
form::param_fields(ui, d, &|k| vectorcraft_effects::is_length(&id, k, relative), &|k| choices(&id, k), &|k| doc_rank(doc, k), unit);
|
||||
// The fields show what the effect stores: Scribble's 270° angle reads -90°.
|
||||
if let Value::Object(m) = vectorcraft_effects::upgrade_params(&id, &Value::Object(d.fields.clone())) {
|
||||
d.fields = m;
|
||||
}
|
||||
changed
|
||||
}
|
||||
};
|
||||
ui.add_space(6.0);
|
||||
|
||||
@@ -432,6 +432,7 @@ pub(super) fn humanize(k: &str) -> String {
|
||||
"Channel4" => "Channel 4:".into(),
|
||||
"Include Cmy Blacks" => "Include Blacks with CMY:".into(),
|
||||
"Align To Path" => "Align to Path:".into(),
|
||||
"Overlap Variation" | "Curviness Variation" | "Spacing Variation" => "Variation:".into(),
|
||||
"Create" => "Create New Fields by:".into(),
|
||||
_ => format!("{s}:"),
|
||||
}
|
||||
|
||||
@@ -238,6 +238,28 @@ 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_shows_the_angle_as_stored_and_each_variation() {
|
||||
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();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("angle".into(), json!(270));
|
||||
frame(&mut app, Default::default());
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["angle"].as_f64(), Some(-90.0), "the field reads what is stored");
|
||||
confirm(&mut app).unwrap();
|
||||
let n = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
|
||||
assert_eq!(n.appearance.effects[0].params["angle"].as_f64(), Some(-90.0));
|
||||
// Each variation follows the option it varies, as in the reference app's Scribble Options.
|
||||
let doc = vectorcraft_effects::effect_info("stylize.scribble").unwrap().params;
|
||||
let order = ["angle", "overlap", "overlapVariation", "strokeWidth", "curviness", "curvinessVariation", "spacing", "spacingVariation", "seed"];
|
||||
let ranks: Vec<usize> = order.iter().map(|k| super::effect::doc_rank(doc, k)).collect();
|
||||
assert!(ranks.windows(2).all(|w| w[0] < w[1]) && ranks[8] < usize::MAX, "{ranks:?}");
|
||||
for key in ["overlapVariation", "curvinessVariation", "spacingVariation"] {
|
||||
assert_eq!(super::form::humanize(key), "Variation:");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revolve_dialog_preview_cancel_confirm_and_undo() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -728,13 +728,7 @@ fn has_options(id: &str) -> bool {
|
||||
/// Drop Shadow's Color or Darkness as radio buttons); edits go through `effect.setParams`.
|
||||
fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option<usize>, k: usize, e: &Effect) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let defaults = vectorcraft_effects::default_params(&e.id).unwrap_or(Value::Null);
|
||||
let mut params = defaults.as_object().cloned().unwrap_or_default();
|
||||
if let Some(cur) = e.params.as_object() {
|
||||
for (key, v) in cur {
|
||||
params.insert(key.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
let params = editor_params(e);
|
||||
let mut change: Option<(String, Value)> = None;
|
||||
let mut edit = false;
|
||||
let built_in = built_in_effect(&e.id);
|
||||
@@ -808,6 +802,15 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option<usize>, k:
|
||||
}
|
||||
}
|
||||
|
||||
/// The values [`effect_editor`] lists: the effect's own values over its defaults, with earlier keys
|
||||
/// in their current form (Scribble's one `variation` as its Path Overlap and Spacing variations).
|
||||
fn editor_params(e: &Effect) -> serde_json::Map<String, Value> {
|
||||
match vectorcraft_effects::merged_params(&e.id, &e.params) {
|
||||
Value::Object(m) => m,
|
||||
_ => serde_json::Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// "offsetX" → "Offset X".
|
||||
pub fn humanize(key: &str) -> String {
|
||||
let mut out = String::new();
|
||||
@@ -1037,6 +1040,19 @@ mod tests {
|
||||
assert_eq!(n.appearance.effects[0].params["shade"], json!("darkness"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_effect_editor_lists_earlier_parameters_in_their_current_form() {
|
||||
// A Scribble saved with one `variation` lists it as its Path Overlap and Spacing variations.
|
||||
let e = Effect { id: "stylize.scribble".into(), params: json!({"variation": 2, "angle": 270}), visible: true };
|
||||
let p = editor_params(&e);
|
||||
assert!(!p.contains_key("variation"), "{p:?}");
|
||||
assert_eq!((&p["overlapVariation"], &p["spacingVariation"], &p["curvinessVariation"]), (&json!(2), &json!(2), &json!(0.0)));
|
||||
assert_eq!(p["angle"].as_f64(), Some(-90.0));
|
||||
// Another effect lists its values over its defaults, as before.
|
||||
let e = Effect { id: "distort.twist".into(), params: json!({"angle": 270}), visible: true };
|
||||
assert_eq!(editor_params(&e).get("angle"), Some(&json!(270)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_pick_the_command() {
|
||||
use vectorcraft_doc::{Appearance, FillLayer};
|
||||
|
||||
@@ -26,7 +26,7 @@ Part of [target-app-parity.md](target-app-parity.md); open work is in [gaps.md](
|
||||
| Path | Offset Path, Outline Object, Outline Stroke | 2/3 | Outline Object |
|
||||
| Pathfinder | Add, Intersect, Exclude, Subtract, Minus Back, Divide, Trim, Merge, Crop, Outline, Hard Mix, Soft Mix, Trap | 10/13 | Hard Mix, Soft Mix, Trap |
|
||||
| Rasterize | Rasterize… (as a live effect) | 0/1 | the live effect (Object › Rasterize exists) |
|
||||
| Stylize | Drop Shadow, Feather, Inner Glow, Outer Glow, Round Corners, Scribble | 6/6 | Scribble draws one zig-zag per region, with no lines across holes and notches (#1651) |
|
||||
| Stylize | Drop Shadow, Feather, Inner Glow, Outer Glow, Round Corners, Scribble | 6/6 | Scribble Options' Settings presets |
|
||||
| SVG Filters | Apply SVG Filter… and the filter list | 0/1 | the whole submenu (SVG filters on import round-trip already) |
|
||||
| Warp | Arc, Arc Lower, Arc Upper, Arch, Bulge, Shell Lower, Shell Upper, Flag, Wave, Fish, Rise, Fisheye, Inflate, Squeeze, Twist | 15/15 | warp point editing in envelopes; Arc is a circular arc in the object's own proportions (#1578) |
|
||||
|
||||
|
||||
@@ -510,6 +510,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). Zig Zag spaces its ridges evenly along each segment, so lines and curves get a regular wave in Corner and Smooth (#1558). Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578). `effect.apply` and `effect.setParams` give an error for a misspelt or wrongly typed effect parameter instead of running the effect with its default (#1623, #1592). Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643). Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648). On the canvas, in Expand Appearance and in exports, Effect → Pathfinder uses the glyph outlines of type in the group, and results painted like the type keep its opacity and blend mode.
|
||||
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Cap is on by default with continuous curved vector rims; shading uses vector gradients automatically, with sharp profile corners, uniform caps and round seam joins. One light and approximate visibility remain ([revolve.md](revolve.md)).
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). Zig Zag spaces its ridges evenly along each segment, so lines and curves get a regular wave in Corner and Smooth (#1558). Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578). `effect.apply` and `effect.setParams` give an error for a misspelt or wrongly typed effect parameter instead of running the effect with its default (#1623, #1592). Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643). Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648).
|
||||
- Scribble (Effect › Stylize › Scribble…) has a Variation for each of Path Overlap, Curviness and Spacing ([Scribble options](https://helpx.adobe.com/illustrator/using/creating-sketches-mosaics.html)) (`overlapVariation`, `curvinessVariation`, `spacingVariation`), shown in its dialog and in the Appearance panel. A Scribble saved with the earlier single `variation` reads it as the Path Overlap and Spacing variations and draws as before. The angle is stored above -180° and up to 180°, so 270° is stored as -90°.
|
||||
- **Simplify feedback and component selection (#1272):** the existing tolerance dialog previews from the original interaction snapshot and shows original/new anchor counts. Preview off and Cancel restore the document and selection; OK is one Undo. A refit or Add Anchor Points clears component indexes only on paths whose geometry changes, keeping their objects selected whole and unchanged paths' components intact. Engine and real-egui frame regressions cover document/selection restoration and grouped paths.
|
||||
Public references: [Simplify benefits](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/simplify-path-benefits.html) and [advanced options](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/simplify-paths-advanced-options-overview.html).
|
||||
Nearly unchanged appearance is expected when reducing points. Percentage/auto-simplify controls, the corner-angle UI and Show Original are not implemented; the checklist is partial, without raising scores.
|
||||
|
||||
Reference in New Issue
Block a user