mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 03:41:28 +00:00
Merge pull request #5139 from dotCooCoo/fix/ai-os10-distort
.ai import: type keeps its Distort & Transform effects live
This commit is contained in:
@@ -298,7 +298,8 @@ pub fn tweak(path: &PathData, b: Rect, p: &Value) -> PathData {
|
||||
}
|
||||
|
||||
/// Twist: rotation that is strongest at the centre and fades to zero at the bounding circle,
|
||||
/// making at most `max` anchors ([`map_nonlinear_within`]).
|
||||
/// clockwise on the page for a positive `angle`, making at most `max` anchors
|
||||
/// ([`map_nonlinear_within`]).
|
||||
pub fn twist(path: &PathData, b: Rect, angle: f64, max: usize) -> PathData {
|
||||
let ang = angle.clamp(-3600.0, 3600.0).to_radians();
|
||||
if ang == 0.0 {
|
||||
@@ -309,7 +310,8 @@ pub fn twist(path: &PathData, b: Rect, angle: f64, max: usize) -> PathData {
|
||||
let f = move |q: Point| {
|
||||
let d = q - c;
|
||||
let t = (1.0 - d.hypot() / r).max(0.0);
|
||||
let a = -ang * t;
|
||||
// y runs down, so a positive turn is clockwise on the page.
|
||||
let a = ang * t;
|
||||
let (s, co) = a.sin_cos();
|
||||
c + Vec2::new(d.x * co - d.y * s, d.x * s + d.y * co)
|
||||
};
|
||||
|
||||
@@ -942,6 +942,18 @@ fn round_corners_keeps_where_a_closed_path_starts() {
|
||||
assert_eq!(stylize::round_corners(&d, f64::INFINITY), stylize::round_corners(&d, 1e9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twist_turns_clockwise_for_positive_angles() {
|
||||
// A positive angle turns clockwise, a negative one counterclockwise
|
||||
// (https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html).
|
||||
// The document's y runs down, so clockwise on the page takes a point right of the center down.
|
||||
let right = PathData::single(SubPath::polyline(&[Point::new(55.0, 50.0), Point::new(56.0, 50.0)], false));
|
||||
let bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let turned = |angle: f64| apply_geometry(&[fx("distort.twist", json!({"angle": angle}))], &right, bounds).subpaths[0].anchors[0].p;
|
||||
assert!(turned(45.0).y > 50.5, "{:?}", turned(45.0));
|
||||
assert!(turned(-45.0).y < 49.5, "{:?}", turned(-45.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twist_fixes_the_outer_circle_and_rotates_inside() {
|
||||
let out = run("distort.twist", json!({"angle": 0}), &square());
|
||||
|
||||
@@ -3906,6 +3906,29 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
|
||||
"(Fill Style Filter) 0 0 /Filter ,",
|
||||
"(Adobe Outer Glow) 1 0 /Filter ,",
|
||||
"(Adobe Drop Shadow) 1 0 /Filter ,",
|
||||
"(Adobe Transform) 1 0 /Filter ,",
|
||||
"(Adobe Free Distort) 1 0 /Filter ,",
|
||||
"(Adobe Punk and Bloat) 1 0 /Filter ,",
|
||||
"(Adobe Roughen) 1 0 /Filter ,",
|
||||
"(Adobe Scribble and Tweak) 1 0 /Filter ,",
|
||||
"(Adobe Twirl) 1 0 /Filter ,",
|
||||
"(Adobe Zigzag) 1 0 /Filter ,",
|
||||
"1e308 /Real (scaleH_Factor) ,",
|
||||
"-1e308 /Real (rotate_Radians) ,",
|
||||
"1000 /Int (numCopies) ,",
|
||||
"8 /Int (pinPoint) ,",
|
||||
"0 /Bool (scaleLines) ,",
|
||||
"0 /Bool (transformPatterns) ,",
|
||||
"0 /Real (src0h) ,",
|
||||
"1e-300 /Real (src1h) ,",
|
||||
"-1 /Real (src2v) ,",
|
||||
"1e308 /Real (dst3v) ,",
|
||||
"-200 /Real (d_factor) ,",
|
||||
"3600 /Real (angle) ,",
|
||||
"1 /Real (absoluteness) ,",
|
||||
"100 /Real (ridges) ,",
|
||||
"1e4 /Real (amount) ,",
|
||||
"0 /Bool (anch) ,",
|
||||
"(Adobe Inner Glow) 1 0 /Filter ,",
|
||||
"(Adobe Fuzzy Mask) 1 0 /Filter ,",
|
||||
"(Adobe Round Corners) 1 0 /Filter ,",
|
||||
@@ -4146,6 +4169,61 @@ proptest! {
|
||||
survive("Path effect art style in an EPS", || vectorcraft_engine::cmd::fileio::load("x.eps", &eps).ok().map(|l| l.doc))?;
|
||||
}
|
||||
|
||||
/// Art styles with a Distort & Transform effect (Transform, Free Distort, Pucker & Bloat, Roughen,
|
||||
/// Tweak, Twist, Zig Zag) whose dictionary holds any of its entries, and unknown ones, as any kind,
|
||||
/// with any value (huge, tiny, not a number, a string), repeated or left out, on a path and on type,
|
||||
/// on the whole object or on its fill.
|
||||
#[test]
|
||||
fn ai_distort_effects_never_panic(
|
||||
name in prop::sample::select(vec![
|
||||
"Adobe Transform",
|
||||
"Adobe Free Distort",
|
||||
"Adobe Punk and Bloat",
|
||||
"Adobe Roughen",
|
||||
"Adobe Scribble and Tweak",
|
||||
"Adobe Twirl",
|
||||
"Adobe Zigzag",
|
||||
]),
|
||||
entries in prop::collection::vec((
|
||||
prop::sample::select(vec![
|
||||
"scaleH_Factor", "scaleV_Factor", "scaleH_Percent", "rotate_Radians", "rotate_Degrees", "moveH_Pts", "moveV_Pts",
|
||||
"numCopies", "pinPoint", "reflectX", "reflectY", "randomize", "transformObjects", "transformPatterns", "scaleLines",
|
||||
"src0h", "src0v", "src1h", "src1v", "src2h", "src2v", "src3h", "src3v", "dst0h", "dst0v", "dst1h", "dst1v", "dst2h",
|
||||
"dst2v", "dst3h", "dst3v", "d_factor", "absoluteness", "size", "asiz", "dtal", "roundness", "horz", "vert", "ahor",
|
||||
"aver", "anch", "in", "out", "angle", "amount", "relAmount", "ridges", "futureKey",
|
||||
]),
|
||||
prop::sample::select(vec!["Int", "Real", "Bool"]),
|
||||
prop_oneof![
|
||||
(-1e4f64..1e4).prop_map(|v| v.to_string()),
|
||||
prop::sample::select(vec!["0", "1", "-1", "0.5", "1e-300", "1e308", "-1e308", "1e999", "NaN", "(x)", ""]).prop_map(str::to_string),
|
||||
],
|
||||
), 0..24),
|
||||
on_fill in any::<bool>(),
|
||||
) {
|
||||
use vectorcraft_testkit::ai;
|
||||
let keys: String = entries.iter().map(|(k, kind, v)| format!("{v} /{kind} ({k}) ,\n")).collect();
|
||||
let basic = |name: &str, body: &str| format!("/BasicFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n{body}; /Dict ;\n /Part ,\n");
|
||||
let compound = |name: &str, kids: &[String]| format!("/CompoundFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n{};\n /Part ,\n", kids.concat());
|
||||
let fill = basic("Fill Style Filter", "/FillStyle : 0 O\n0 0 0 0 0 0 1 Xa\n0 1 Xd\n/Def ;\n (FillStyle) ,\n");
|
||||
let effect = basic(name, &keys);
|
||||
let root = if on_fill {
|
||||
compound("Chain Style Filter", &[compound("Stack Style Filter", &[compound("Chain Style Filter", &[fill, effect]), basic("Conduit Filter", "")])])
|
||||
} else {
|
||||
compound("Chain Style Filter", &[compound("Stack Style Filter", &[fill, basic("Conduit Filter", "")]), effect])
|
||||
};
|
||||
let styles = format!("%AI9_BeginArtStyles\n/KnownStyle :\n(Fx) /Name ,\n/ActiveStyle :\n{root}%AI9_EndArtStyles\n");
|
||||
let hidden = ai::rect(20.0, 20.0, 80.0, 60.0).replace('\n', "\n%_");
|
||||
let art = format!(
|
||||
"0 Ae\nu\n0 0 1 0 k\n{}f\nU\n%_{hidden}f\n1 (Fx) XW\nu\n/AI11Text :\n1 /FreeUndo ,\n;\n0 0 1 0 k\n{}f\nU\n1 (Fx) XW\n",
|
||||
ai::rect(20.0, 20.0, 80.0, 60.0),
|
||||
ai::rect(100.0, 20.0, 180.0, 60.0)
|
||||
);
|
||||
let data = ai::editing_data(200.0, 100.0, &format!("{styles}{}", ai::layer("L", &art)));
|
||||
survive("distort art style", || Some(vectorcraft_eps::layered_ai(data.as_bytes(), Document::new(200.0, 100.0), vec![], false, false, 1.0).0))?;
|
||||
let eps = ai::eps(data.as_bytes(), ai::page_ps());
|
||||
survive("distort art style in an EPS", || vectorcraft_engine::cmd::fileio::load("x.eps", &eps).ok().map(|l| l.doc))?;
|
||||
}
|
||||
|
||||
/// Layer options (`Lb`) of any number of operands with any values, and guide paths (`(N) *`,
|
||||
/// `(S) *`) of any points, among them ones across the whole canvas or from edge to edge of
|
||||
/// artboards that overlap or are the same, on layers and sublayers.
|
||||
|
||||
@@ -60,6 +60,11 @@ const OFFSET: &str = "Adobe Offset Path"; // brand-ok: the filter's name in the
|
||||
const OUTLINE_TYPE: &str = "Adobe Outline Type"; // brand-ok: the filter's name in the files
|
||||
const OUTLINE_STROKE: &str = "Adobe Outline Stroke"; // brand-ok: the filter's name in the files
|
||||
const TRANSFORM: &str = "Adobe Transform"; // brand-ok: the filter's name in the files
|
||||
const FREE_DISTORT: &str = "Adobe Free Distort"; // brand-ok: the filter's name in the files
|
||||
const PUCKER_BLOAT: &str = "Adobe Punk and Bloat"; // brand-ok: the filter's name in the files
|
||||
const TWEAK: &str = "Adobe Scribble and Tweak"; // brand-ok: the filter's name in the files
|
||||
const TWIST: &str = "Adobe Twirl"; // brand-ok: the filter's name in the files
|
||||
const ZIG_ZAG: &str = "Adobe Zigzag"; // brand-ok: the filter's name in the files
|
||||
const GAUSSIAN: &str = "Adobe PSL Gaussian Blur"; // brand-ok: the filter's name in the files
|
||||
const BAS_RELIEF: &str = "PSAdapter_plugin_BsRl";
|
||||
const SHAPE_EFFECTS: &str = "Adobe Shape Effects"; // brand-ok: the filter's name in the files
|
||||
@@ -546,6 +551,186 @@ fn width_profile(arrays: &HashMap<String, Vec<f64>>) -> Option<WidthProfile> {
|
||||
(!uniform).then_some(WidthProfile { points })
|
||||
}
|
||||
|
||||
/// Entry `key` of `f`'s dictionary, written as `kind`: `Ok(None)` when the dictionary doesn't hold
|
||||
/// it, `Err` when it holds it as another kind, without a finite number, or as an `/Int` that isn't
|
||||
/// whole or a `/Bool` other than 0 and 1.
|
||||
fn entry(f: &Filter, key: &str, kind: Kind) -> Result<Option<f64>, ()> {
|
||||
let Some(k) = f.kinds.get(key) else { return Ok(None) };
|
||||
match f.params.get(key) {
|
||||
Some(v)
|
||||
if *k == kind
|
||||
&& v.is_finite()
|
||||
&& match kind {
|
||||
Kind::Real => true,
|
||||
Kind::Int => v.fract() == 0.0,
|
||||
Kind::Bool => *v == 0.0 || *v == 1.0,
|
||||
Kind::Fill | Kind::Other => false,
|
||||
} =>
|
||||
{
|
||||
Ok(Some(*v))
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry `key` as a switch, 0 or 1 written as `kind`: `Ok(None)` when it is missing.
|
||||
fn switch(f: &Filter, key: &str, kind: Kind) -> Result<Option<bool>, ()> {
|
||||
match entry(f, key, kind)? {
|
||||
None => Ok(None),
|
||||
Some(0.0) => Ok(Some(false)),
|
||||
Some(1.0) => Ok(Some(true)),
|
||||
Some(_) => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Real entry `key` within `range`, `missing` when the dictionary doesn't hold it.
|
||||
fn real(f: &Filter, key: &str, missing: f64, range: std::ops::RangeInclusive<f64>) -> Option<f64> {
|
||||
entry(f, key, Kind::Real).ok()?.map_or(Some(missing), |v| range.contains(&v).then_some(v))
|
||||
}
|
||||
|
||||
/// What an effect in a style could change the paint of, for the Transform options whose value,
|
||||
/// when the dictionary leaves it out, comes from the app's settings rather than the file.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Under {
|
||||
/// A stroke, or other effects with distances that Scale Strokes & Effects would scale.
|
||||
strokes: bool,
|
||||
/// A pattern.
|
||||
patterns: bool,
|
||||
}
|
||||
|
||||
/// Anything: an effect on the whole object, whose contents can paint in any way.
|
||||
const ANYTHING: Under = Under { strokes: true, patterns: true };
|
||||
|
||||
/// The Transform effect `f` (the factor and radian entries; the percent and degree ones are what
|
||||
/// its dialog shows). `None` keeps the drawn look: an entry not read, Random (the file holds no
|
||||
/// seed), or a missing Scale Strokes & Effects or Transform Patterns that would change `under`.
|
||||
fn transform(f: &Filter, under: Under) -> Option<serde_json::Value> {
|
||||
for key in ["scaleH_Percent", "scaleV_Percent", "rotate_Degrees"] {
|
||||
entry(f, key, Kind::Real).ok()?;
|
||||
}
|
||||
// The renderer's limits on scales and moves (`vectorcraft_effects`' `MAX_SCALE` and `MAX_MOVE`).
|
||||
let scale = |key: &str| real(f, key, 1.0, -1000.0..=1000.0).map(|v| v * 100.0);
|
||||
let (h, v) = (scale("scaleH_Factor")?, scale("scaleV_Factor")?);
|
||||
let (move_h, move_v) = (real(f, "moveH_Pts", 0.0, -1e7..=1e7)?, real(f, "moveV_Pts", 0.0, -1e7..=1e7)?);
|
||||
let rotate = Some(entry(f, "rotate_Radians", Kind::Real).ok()?.unwrap_or(0.0).to_degrees()).filter(|d| d.is_finite())?;
|
||||
let copies = entry(f, "numCopies", Kind::Int).ok()?.unwrap_or(0.0);
|
||||
let reference = entry(f, "pinPoint", Kind::Int).ok()?.unwrap_or(4.0);
|
||||
if !(0.0..=1000.0).contains(&copies) || !(0.0..=8.0).contains(&reference) {
|
||||
return None;
|
||||
}
|
||||
let on = |key: &str| switch(f, key, Kind::Bool);
|
||||
let (reflect_x, reflect_y) = (on("reflectX").ok()?.unwrap_or(false), on("reflectY").ok()?.unwrap_or(false));
|
||||
let objects = on("transformObjects").ok()?.unwrap_or(true);
|
||||
let (strokes, patterns) = (on("scaleLines").ok()?, on("transformPatterns").ok()?);
|
||||
if on("randomize").ok()?.unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
let moves = h != 100.0 || v != 100.0 || move_h != 0.0 || move_v != 0.0 || rotate != 0.0 || reflect_x || reflect_y;
|
||||
let scales = ((h * v).abs().sqrt() - 100.0).abs() > 1e-9;
|
||||
if (strokes.is_none() && scales && under.strokes) || (patterns.is_none() && moves && under.patterns) {
|
||||
return None;
|
||||
}
|
||||
// Art space has y up: a move up is a negative `moveV` in the document.
|
||||
Some(json!({
|
||||
"scaleH": h,
|
||||
"scaleV": v,
|
||||
"moveH": move_h,
|
||||
"moveV": 0.0 - move_v,
|
||||
"rotate": rotate,
|
||||
"copies": copies as i64,
|
||||
"reflectX": reflect_x,
|
||||
"reflectY": reflect_y,
|
||||
"reference": reference as i64,
|
||||
"objects": objects,
|
||||
"patterns": patterns.unwrap_or(false),
|
||||
"strokes": strokes.unwrap_or(false),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Free Distort's corners: the destination corners `dst0h`… measured in the source frame `src0h`…
|
||||
/// (top left, top right, bottom left, bottom right, y up), as the catalog's unit-box corners (top
|
||||
/// left, top right, bottom right, bottom left, y down). The source frame is only the unit the
|
||||
/// corners are measured in: the distorted box is laid over the object's own bounds, as the catalog
|
||||
/// effect lays it, also when the frame is another size (a unit square, or bounds with the stroke).
|
||||
/// No corners is no change; some but not all, or a frame that isn't an upright rectangle, isn't read.
|
||||
fn free_distort(f: &Filter) -> Option<serde_json::Value> {
|
||||
let mut corners = [[[0.0; 2]; 4]; 2];
|
||||
let mut held = 0;
|
||||
for (which, c) in ["src", "dst"].into_iter().zip(&mut corners) {
|
||||
for (i, corner) in c.iter_mut().enumerate() {
|
||||
for (axis, v) in ["h", "v"].into_iter().zip(corner.iter_mut()) {
|
||||
if let Some(x) = entry(f, &format!("{which}{i}{axis}"), Kind::Real).ok()? {
|
||||
*v = x;
|
||||
held += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if held == 0 {
|
||||
return Some(json!({"corners": [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]}));
|
||||
}
|
||||
let [[tl, tr, bl, br], dst] = corners;
|
||||
let (left, top) = (tl[0], tl[1]);
|
||||
let (width, height) = (tr[0] - left, top - bl[1]);
|
||||
let tolerance = 1e-5 * width.abs().max(height.abs());
|
||||
let upright = (tr[1] - top).abs() <= tolerance
|
||||
&& (br[1] - bl[1]).abs() <= tolerance
|
||||
&& (bl[0] - left).abs() <= tolerance
|
||||
&& (br[0] - tr[0]).abs() <= tolerance;
|
||||
let extent = |d: f64| d.is_finite() && d > 0.0;
|
||||
if held < 16 || !upright || !extent(width) || !extent(height) {
|
||||
return None;
|
||||
}
|
||||
let mut out = vec![];
|
||||
for i in [0, 1, 3, 2] {
|
||||
let [h, v] = *dst.get(i)?;
|
||||
let (x, y) = ((h - left) / width, (top - v) / height);
|
||||
if !(x.is_finite() && y.is_finite() && x.abs() <= 1e4 && y.abs() <= 1e4) {
|
||||
return None;
|
||||
}
|
||||
out.push([x, y]);
|
||||
}
|
||||
Some(json!({ "corners": out }))
|
||||
}
|
||||
|
||||
/// A `0` relative, `1` absolute switch (`absoluteness`), `absolute` when it is missing, and the size
|
||||
/// it picks: `relative_key` in percent or `absolute_key` in points, 0 when missing.
|
||||
fn sized(f: &Filter, absolute: bool, relative_key: &str, absolute_key: &str) -> Option<(bool, f64)> {
|
||||
let absolute = switch(f, "absoluteness", Kind::Real).ok()?.unwrap_or(absolute);
|
||||
let relative = real(f, relative_key, 0.0, 0.0..=1e4)?;
|
||||
let points = real(f, absolute_key, 0.0, 0.0..=1e4)?;
|
||||
Some((absolute, if absolute { points } else { relative }))
|
||||
}
|
||||
|
||||
/// Corner (`roundness` 0, or missing) or Smooth (1).
|
||||
fn points(f: &Filter) -> Option<&'static str> {
|
||||
Some(if switch(f, "roundness", Kind::Real).ok()?.unwrap_or(false) { "smooth" } else { "corner" })
|
||||
}
|
||||
|
||||
/// The Tweak effect `f` (`Adobe Scribble and Tweak`): relative unless `absoluteness` says otherwise,
|
||||
/// moving anchors and both handles unless the dictionary turns them off.
|
||||
fn tweak(f: &Filter) -> Option<serde_json::Value> {
|
||||
let (absolute, h) = sized(f, false, "horz", "ahor")?;
|
||||
let (_, v) = sized(f, false, "vert", "aver")?;
|
||||
let on = |key: &str| switch(f, key, Kind::Bool).map(|v| v.unwrap_or(true));
|
||||
let (anchors, ins, outs) = (on("anch").ok()?, on("in").ok()?, on("out").ok()?);
|
||||
Some(json!({"h": h, "v": v, "relative": !absolute, "anchors": anchors, "in": ins, "out": outs}))
|
||||
}
|
||||
|
||||
/// The Zig Zag effect `f`: absolute unless `absoluteness` says otherwise, 4 ridges and Corner points
|
||||
/// when they are missing.
|
||||
fn zig_zag(f: &Filter) -> Option<serde_json::Value> {
|
||||
let (absolute, size) = sized(f, true, "relAmount", "amount")?;
|
||||
let ridges = real(f, "ridges", 4.0, 0.0..=100.0).filter(|r| r.fract() == 0.0)?;
|
||||
Some(json!({"size": size, "relative": !absolute, "ridges": ridges as i64, "points": points(f)?}))
|
||||
}
|
||||
|
||||
/// Whether Tweak params `p` leave the outline as it is: its random outline isn't in the file, so any
|
||||
/// other keeps the drawn look.
|
||||
fn still(p: &serde_json::Value) -> bool {
|
||||
(p["h"].as_f64() == Some(0.0) && p["v"].as_f64() == Some(0.0)) || ["anchors", "in", "out"].iter().all(|k| p[k] == json!(false))
|
||||
}
|
||||
|
||||
/// A Stylize effect's dictionary, read only as it is written: each setting a key of the kind it
|
||||
/// names, with a finite value in the effect's range.
|
||||
struct Settings<'a>(&'a Filter);
|
||||
@@ -763,8 +948,9 @@ fn path_effect(f: &Filter) -> Option<Effect> {
|
||||
Some(Effect { id: id.into(), params, visible: f.visible })
|
||||
}
|
||||
|
||||
/// The live effect filter `f` is, when it is one read here.
|
||||
fn effect(f: &Filter) -> Option<Effect> {
|
||||
/// The live effect filter `f` is, when it is one read here; `under` is what it could change the
|
||||
/// paint of.
|
||||
fn effect(f: &Filter, under: Under) -> Option<Effect> {
|
||||
if let Some(stylized) = stylize(f) {
|
||||
let (id, params) = stylized?;
|
||||
return Some(Effect { id: id.into(), params, visible: f.visible });
|
||||
@@ -789,22 +975,14 @@ fn effect(f: &Filter) -> Option<Effect> {
|
||||
)
|
||||
}
|
||||
OFFSET | OUTLINE_TYPE | OUTLINE_STROKE => return path_effect(f),
|
||||
// Art space has y up: a move up is a negative `moveV` in the document.
|
||||
TRANSFORM => (
|
||||
"distort.transform",
|
||||
json!({
|
||||
"scaleH": param("scaleH_Percent").unwrap_or(100.0),
|
||||
"scaleV": param("scaleV_Percent").unwrap_or(100.0),
|
||||
"moveH": param("moveH_Pts").unwrap_or(0.0),
|
||||
"moveV": -param("moveV_Pts").unwrap_or(0.0),
|
||||
"rotate": param("rotate_Degrees").unwrap_or(0.0),
|
||||
"copies": param("numCopies").unwrap_or(0.0).clamp(0.0, 1000.0) as i64,
|
||||
"reflectX": flag("reflectX"),
|
||||
"reflectY": flag("reflectY"),
|
||||
"reference": param("pinPoint").unwrap_or(4.0).clamp(0.0, 8.0) as i64,
|
||||
"random": flag("randomize"),
|
||||
}),
|
||||
),
|
||||
TRANSFORM => ("distort.transform", transform(f, under)?),
|
||||
FREE_DISTORT => ("distort.freeDistort", free_distort(f)?),
|
||||
// Negative puckers (https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html).
|
||||
PUCKER_BLOAT => ("distort.puckerBloat", json!({"amount": real(f, "d_factor", 0.0, -200.0..=200.0)?})),
|
||||
TWEAK => ("distort.tweak", tweak(f).filter(still)?),
|
||||
// A positive angle turns clockwise in both.
|
||||
TWIST => ("distort.twist", json!({"angle": real(f, "angle", 0.0, -3600.0..=3600.0)?})),
|
||||
ZIG_ZAG => ("distort.zigZag", zig_zag(f)?),
|
||||
GAUSSIAN => ("blur.gaussian", json!({"radius": param("blur").unwrap_or(5.0).clamp(0.0, 1000.0)})),
|
||||
// Its settings are in a Photoshop plug-in's binary block: the effect's defaults stand in.
|
||||
BAS_RELIEF => ("sketch.basRelief", json!({"detail": 13.0, "smoothness": 3.0, "lightDirection": "bottom"})),
|
||||
@@ -878,7 +1056,25 @@ impl Builder<'_> {
|
||||
let mut look = ItemLook { visible: f.visible, ..ItemLook::default() };
|
||||
let mut painted: Option<StyleItem> = None;
|
||||
let mut paints = 0;
|
||||
for k in self.children(id) {
|
||||
let kids = self.children(id);
|
||||
// What this item's effects could change: its own paint, and its other effects' distances.
|
||||
let mut under = Under { strokes: false, patterns: false };
|
||||
let mut effects = 0;
|
||||
for &k in &kids {
|
||||
let c = &filters[k];
|
||||
match c.name.as_str() {
|
||||
_ if c.compound => {}
|
||||
FILL | STROKE => match paint_item(c) {
|
||||
Ok(Some(StyleItem::Stroke { .. })) => under.strokes = true,
|
||||
Ok(Some(StyleItem::Pattern { .. })) => under.patterns = true,
|
||||
_ => {}
|
||||
},
|
||||
BLEND => {}
|
||||
_ => effects += 1,
|
||||
}
|
||||
}
|
||||
under.strokes |= effects > 1;
|
||||
for k in kids {
|
||||
let c = &filters[k];
|
||||
match c.name.as_str() {
|
||||
_ if c.compound => self.plain = false,
|
||||
@@ -897,7 +1093,7 @@ impl Builder<'_> {
|
||||
}
|
||||
// Convert to Shape on one fill or stroke isn't read yet: what it draws there is
|
||||
// the drawn look's.
|
||||
_ => match effect(c) {
|
||||
_ => match effect(c, under) {
|
||||
Some(e) if !e.id.starts_with("convertToShape.") => look.effects.push(e),
|
||||
_ => self.plain = false,
|
||||
},
|
||||
@@ -955,7 +1151,7 @@ fn style(filters: &[Filter]) -> ArtStyle {
|
||||
st.transparency = Some((opacity, super::read::blend_mode(mode), isolate, knockout));
|
||||
}
|
||||
}
|
||||
_ => match effect(f) {
|
||||
_ => match effect(f, ANYTHING) {
|
||||
Some(e) => st.effects.push(e),
|
||||
None => b.plain = false,
|
||||
},
|
||||
|
||||
@@ -593,6 +593,226 @@ fn offset_path_with_settings_not_read_keeps_the_drawn_look() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The style `Fx`: effect `name` on the whole object, its dictionary holding `keys`.
|
||||
fn distort_effect_style(name: &str, keys: &str) -> ArtStyle {
|
||||
let data = format!("/KnownStyle :\n(Fx) /Name ,\n/ActiveStyle :\n{}", compound("Chain Style Filter", &[basic(name, keys)]));
|
||||
art_styles(data.as_bytes()).remove("Fx").unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The params of effect `name` with `keys`, `None` when the style keeps the drawn look.
|
||||
fn read_effect(name: &str, keys: &str) -> Option<serde_json::Value> {
|
||||
let st = distort_effect_style(name, keys);
|
||||
if !st.plain {
|
||||
return None;
|
||||
}
|
||||
let [e] = st.effects.as_slice() else { panic!("{st:?}") };
|
||||
Some(e.params.clone())
|
||||
}
|
||||
|
||||
/// A filter named `name` whose dictionary holds `entries`.
|
||||
fn dict(name: &str, entries: &[(&str, Kind, f64)]) -> Filter {
|
||||
let kinds = entries.iter().map(|(k, kind, _)| (k.to_string(), *kind)).collect();
|
||||
let params = entries.iter().map(|(k, _, v)| (k.to_string(), *v)).collect();
|
||||
Filter { name: name.into(), kinds, params, ..Filter::default() }
|
||||
}
|
||||
|
||||
fn close(a: &serde_json::Value, want: f64) -> bool {
|
||||
a.as_f64().is_some_and(|a| (a - want).abs() < 1e-6)
|
||||
}
|
||||
|
||||
/// Every switch of the Transform effect, as files write them.
|
||||
const SWITCHES: &str = "0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n";
|
||||
|
||||
#[test]
|
||||
fn a_transform_reads_its_factors_and_radians() {
|
||||
// GUARDS: the percent and degree entries (what the dialog shows) were read, and the options were not.
|
||||
let p = read_effect(
|
||||
"Adobe Transform",
|
||||
"150 /Real (scaleH_Percent) ,\n1.4 /Real (scaleH_Factor) ,\n80 /Real (scaleV_Percent) ,\n1.2 /Real (scaleV_Factor) ,\n\
|
||||
30 /Real (rotate_Degrees) ,\n0.471238898 /Real (rotate_Radians) ,\n-8 /Real (moveV_Pts) ,\n3 /Real (moveH_Pts) ,\n\
|
||||
2 /Int (numCopies) ,\n0 /Int (pinPoint) ,\n1 /Bool (reflectX) ,\n0 /Bool (transformObjects) ,\n1 /Bool (transformPatterns) ,\n\
|
||||
1 /Bool (scaleLines) ,\n0 /Bool (randomize) ,\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(close(&p["scaleH"], 140.0) && close(&p["scaleV"], 120.0) && close(&p["rotate"], 27.0), "{p}");
|
||||
assert_eq!((&p["moveH"], &p["moveV"], &p["copies"], &p["reference"]), (&json!(3.0), &json!(8.0), &json!(2), &json!(0)));
|
||||
assert_eq!((&p["reflectX"], &p["reflectY"]), (&json!(true), &json!(false)));
|
||||
assert_eq!((&p["objects"], &p["patterns"], &p["strokes"]), (&json!(false), &json!(true), &json!(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_transforms_percent_and_degrees_alone_change_nothing() {
|
||||
let p = read_effect("Adobe Transform", &format!("150 /Real (scaleH_Percent) ,\n30 /Real (rotate_Degrees) ,\n{SWITCHES}")).unwrap();
|
||||
assert_eq!((&p["scaleH"], &p["scaleV"], &p["rotate"]), (&json!(100.0), &json!(100.0), &json!(0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_transforms_reference_point_and_move_up() {
|
||||
for pin in 0..9 {
|
||||
let p = read_effect("Adobe Transform", &format!("0.5 /Real (scaleH_Factor) ,\n{pin} /Int (pinPoint) ,\n{SWITCHES}")).unwrap();
|
||||
assert_eq!(p["reference"], json!(pin));
|
||||
}
|
||||
// A move up the page in the file is a move up in the document, whose y runs down.
|
||||
let p = read_effect("Adobe Transform", &format!("10 /Real (moveV_Pts) ,\n{SWITCHES}")).unwrap();
|
||||
assert_eq!(p["moveV"], json!(-10.0));
|
||||
let p = read_effect("Adobe Transform", &format!("4 /Int (pinPoint) ,\n{SWITCHES}")).unwrap();
|
||||
assert_eq!((&p["objects"], &p["copies"], &p["scaleH"]), (&json!(true), &json!(0), &json!(100.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_transform_without_the_options_its_change_depends_on_keeps_the_drawn_look() {
|
||||
// Missing Scale Strokes & Effects or Transform Patterns are the app's settings, not the file's.
|
||||
let half = "0.5 /Real (scaleH_Factor) ,\n0.5 /Real (scaleV_Factor) ,\n";
|
||||
assert_eq!(read_effect("Adobe Transform", &format!("{half}0 /Bool (transformPatterns) ,\n")), None);
|
||||
assert_eq!(read_effect("Adobe Transform", &format!("{half}0 /Bool (scaleLines) ,\n")), None);
|
||||
// A move scales nothing; with Transform Patterns given, it reads.
|
||||
let p = read_effect("Adobe Transform", "5 /Real (moveH_Pts) ,\n0 /Bool (transformPatterns) ,\n").unwrap();
|
||||
assert_eq!(p["strokes"], json!(false));
|
||||
// On a fill's own Transform, only that fill's paint counts: a solid fill moves the same either way.
|
||||
let moved = basic("Adobe Transform", "5 /Real (moveH_Pts) ,\n");
|
||||
let item = |paint: &str| {
|
||||
let item = compound("Chain Style Filter", &[fill(paint), moved.clone()]);
|
||||
let data =
|
||||
format!("/KnownStyle :\n(Fx) /Name ,\n/ActiveStyle :\n{}", compound("Chain Style Filter", &[compound("Stack Style Filter", &[item])]));
|
||||
art_styles(data.as_bytes()).remove("Fx").unwrap_or_default().plain
|
||||
};
|
||||
assert!(item("0 0 0 1 0 0 0 Xa"));
|
||||
assert!(!item("(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_random_transform_keeps_the_drawn_look() {
|
||||
// The file holds no seed for its random share.
|
||||
assert_eq!(read_effect("Adobe Transform", &format!("1 /Bool (randomize) ,\n{SWITCHES}")), None);
|
||||
}
|
||||
|
||||
/// Free Distort's dictionary: the source corners `src` and the destination `dst`, top left, top
|
||||
/// right, bottom left, bottom right, y up.
|
||||
fn corners(src: [[f64; 2]; 4], dst: [[f64; 2]; 4]) -> String {
|
||||
let mut out = String::new();
|
||||
for (which, c) in [("src", src), ("dst", dst)] {
|
||||
for (i, [h, v]) in c.into_iter().enumerate() {
|
||||
out += &format!("{h} /Real ({which}{i}h) ,\n{v} /Real ({which}{i}v) ,\n");
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn unit_corners(p: &serde_json::Value) -> Vec<[f64; 2]> {
|
||||
p["corners"].as_array().unwrap().iter().map(|c| [c[0].as_f64().unwrap(), c[1].as_f64().unwrap()]).collect()
|
||||
}
|
||||
|
||||
fn corners_near(a: &[[f64; 2]], b: &[[f64; 2]]) -> bool {
|
||||
a.len() == b.len() && a.iter().zip(b).all(|(a, b)| (a[0] - b[0]).abs() < 1e-9 && (a[1] - b[1]).abs() < 1e-9)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_distort_measures_its_corners_in_its_own_frame() {
|
||||
// GUARDS: Free Distort wasn't read, and type with it came in as its drawn look.
|
||||
let dst = [[180.0, 310.0], [320.0, 300.0], [210.0, 240.0], [250.0, 260.0]];
|
||||
// A frame wider than the object (its stroke's): the corners are fractions of that frame.
|
||||
let p = read_effect("Adobe Free Distort", &corners([[195.0, 305.0], [305.0, 305.0], [195.0, 245.0], [305.0, 245.0]], dst)).unwrap();
|
||||
let want = [[-15.0 / 110.0, -5.0 / 60.0], [125.0 / 110.0, 5.0 / 60.0], [0.5, 0.75], [15.0 / 110.0, 65.0 / 60.0]];
|
||||
assert!(corners_near(&unit_corners(&p), &want), "{p}");
|
||||
// A unit frame: the same corners measured in it, far outside the unit box.
|
||||
let p = read_effect("Adobe Free Distort", &corners([[0.0, 0.0], [1.0, 0.0], [0.0, -1.0], [1.0, -1.0]], dst)).unwrap();
|
||||
assert!(corners_near(&unit_corners(&p), &[[180.0, -310.0], [320.0, -300.0], [250.0, -260.0], [210.0, -240.0]]), "{p}");
|
||||
// No corners: no change.
|
||||
let p = read_effect("Adobe Free Distort", "").unwrap();
|
||||
assert!(corners_near(&unit_corners(&p), &[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]), "{p}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_distort_without_every_corner_or_an_upright_frame_keeps_the_drawn_look() {
|
||||
let src = [[0.0, 0.0], [1.0, 0.0], [0.0, -1.0], [1.0, -1.0]];
|
||||
let dst = [[0.1, 0.0], [1.0, 0.1], [0.0, -1.0], [1.0, -1.2]];
|
||||
let all = corners(src, dst);
|
||||
// The destination alone, one entry missing, a frame that is turned or of no height.
|
||||
let dst_only: String = all.lines().filter(|l| l.contains("(dst")).map(|l| format!("{l}\n")).collect();
|
||||
let short = all.replace("-1.2 /Real (dst3v) ,\n", "");
|
||||
for keys in [
|
||||
dst_only,
|
||||
short,
|
||||
corners([[0.0, 0.0], [1.0, 0.5], [0.0, -1.0], [1.0, -1.0]], dst),
|
||||
corners([[0.0, 0.0], [1.0, 0.0], [0.0, 0.0], [1.0, 0.0]], dst),
|
||||
// A frame too wide for a number.
|
||||
corners([[-1e308, 0.0], [1e308, 0.0], [-1e308, -1.0], [1e308, -1.0]], dst),
|
||||
] {
|
||||
assert_eq!(read_effect("Adobe Free Distort", &keys), None, "{keys}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pucker_and_twist_keep_their_sense() {
|
||||
// Negative is Pucker; a positive angle turns clockwise in the file and in the catalog.
|
||||
assert_eq!(read_effect("Adobe Punk and Bloat", "-35 /Real (d_factor) ,\n"), Some(json!({"amount": -35.0})));
|
||||
assert_eq!(read_effect("Adobe Punk and Bloat", ""), Some(json!({"amount": 0.0})));
|
||||
assert_eq!(read_effect("Adobe Twirl", "45 /Real (angle) ,\n"), Some(json!({"angle": 45.0})));
|
||||
assert_eq!(read_effect("Adobe Twirl", ""), Some(json!({"angle": 0.0})));
|
||||
assert_eq!(read_effect("Adobe Punk and Bloat", "250 /Real (d_factor) ,\n"), None);
|
||||
assert_eq!(read_effect("Adobe Twirl", "4000 /Real (angle) ,\n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roughen_keeps_the_drawn_look() {
|
||||
// Its outline is random and the file holds no seed; at a size of 0 the effect still redraws the
|
||||
// path's segments.
|
||||
assert_eq!(read_effect("Adobe Roughen", "30 /Real (size) ,\n"), None);
|
||||
assert_eq!(read_effect("Adobe Roughen", ""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tweak_picks_its_amounts_by_its_mode_and_moves_every_point_unless_told() {
|
||||
let amounts = [("horz", Kind::Real, 30.0), ("vert", Kind::Real, 20.0), ("ahor", Kind::Real, 2.0), ("aver", Kind::Real, 3.0)];
|
||||
let mut absolute = amounts.to_vec();
|
||||
absolute.push(("absoluteness", Kind::Real, 1.0));
|
||||
let want = |h: f64, v: f64, relative: bool| json!({"h": h, "v": v, "relative": relative, "anchors": true, "in": true, "out": true});
|
||||
assert_eq!(tweak(&dict("Adobe Scribble and Tweak", &amounts)), Some(want(30.0, 20.0, true)));
|
||||
assert_eq!(tweak(&dict("Adobe Scribble and Tweak", &absolute)), Some(want(2.0, 3.0, false)));
|
||||
let off = tweak(&dict("Adobe Scribble and Tweak", &[("in", Kind::Bool, 0.0)])).unwrap();
|
||||
assert_eq!((&off["anchors"], &off["in"], &off["out"]), (&json!(true), &json!(false), &json!(true)));
|
||||
// Random too: it reads only when it moves nothing.
|
||||
assert_eq!(read_effect("Adobe Scribble and Tweak", "30 /Real (horz) ,\n"), None);
|
||||
assert!(read_effect("Adobe Scribble and Tweak", "").is_some());
|
||||
let still = "30 /Real (horz) ,\n0 /Bool (anch) ,\n0 /Bool (in) ,\n0 /Bool (out) ,\n";
|
||||
assert!(read_effect("Adobe Scribble and Tweak", still).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zig_zag_is_absolute_with_four_ridges_and_corners_unless_told() {
|
||||
let p = read_effect("Adobe Zigzag", "30 /Real (relAmount) ,\n2 /Real (amount) ,\n").unwrap();
|
||||
assert_eq!(p, json!({"size": 2.0, "relative": false, "ridges": 4, "points": "corner"}));
|
||||
let p = read_effect(
|
||||
"Adobe Zigzag",
|
||||
"30 /Real (relAmount) ,\n2 /Real (amount) ,\n0 /Real (absoluteness) ,\n6 /Real (ridges) ,\n1 /Real (roundness) ,\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(p, json!({"size": 30.0, "relative": true, "ridges": 6, "points": "smooth"}));
|
||||
assert_eq!(read_effect("Adobe Zigzag", "2.5 /Real (ridges) ,\n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_distort_entry_this_cant_read_keeps_the_drawn_look_and_unknown_ones_are_ignored() {
|
||||
// Not a number, the wrong kind, a switch other than 0 or 1, a whole number that isn't.
|
||||
for (name, keys) in [
|
||||
("Adobe Twirl", "NaN /Real (angle) ,\n"),
|
||||
("Adobe Twirl", "45 /Int (angle) ,\n"),
|
||||
("Adobe Zigzag", "2 /Real (absoluteness) ,\n"),
|
||||
("Adobe Transform", "4 /Real (pinPoint) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"),
|
||||
("Adobe Transform", "2 /Bool (reflectX) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"),
|
||||
("Adobe Transform", "1e999 /Real (scaleH_Percent) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"),
|
||||
("Adobe Transform", "1001 /Int (numCopies) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"),
|
||||
// In degrees, too large for a number.
|
||||
("Adobe Transform", "1e308 /Real (rotate_Radians) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"),
|
||||
("Adobe Free Distort", "(x) /Real (src0h) ,\n"),
|
||||
] {
|
||||
assert_eq!(read_effect(name, keys), None, "{name}: {keys}");
|
||||
}
|
||||
assert_eq!(read_effect("Adobe Twirl", "37 /Real (angle) ,\n9 /Real (futureKey) ,\n"), Some(json!({"angle": 37.0})));
|
||||
// The last of a repeated entry stands.
|
||||
assert_eq!(read_effect("Adobe Twirl", "37 /Real (angle) ,\n12 /Real (angle) ,\n"), Some(json!({"angle": 12.0})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hidden_fill_comes_in_hidden() {
|
||||
let hidden = fill("0 0 0 1 0 0 0 Xa").replacen("1 /Visible", "0 /Visible", 1);
|
||||
|
||||
@@ -3459,6 +3459,71 @@ fn type_with_a_stacked_art_style_comes_in_as_type_with_that_appearance() {
|
||||
check(&r.document, &r.warnings);
|
||||
}
|
||||
|
||||
/// An art style section with style `Bent`: a blue fill over the characters, and effect `name`, its
|
||||
/// dictionary holding `keys`, on the whole object.
|
||||
fn whole_object_style(name: &str, keys: &str) -> String {
|
||||
let basic = |name: &str, body: &str| {
|
||||
format!("/BasicFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n{body}; /Dict ;\n /Part ,\n")
|
||||
};
|
||||
let compound = |name: &str, kids: &[String]| format!("/CompoundFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n{};\n /Part ,\n", kids.concat());
|
||||
let fill = basic("Fill Style Filter", "/FillStyle : 0 O\n0 0 0 0 0 0 1 Xa\n0 1 Xd\n/Def ;\n (FillStyle) ,\n");
|
||||
let stack = compound("Stack Style Filter", &[fill, basic("Conduit Filter", "")]);
|
||||
let root = compound("Chain Style Filter", &[stack, basic(name, keys)]);
|
||||
format!("%AI9_BeginArtStyles\n/KnownStyle :\n(Bent) /Name ,\n/ActiveStyle :\n{root}%AI9_EndArtStyles\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_with_a_distort_effect_comes_in_as_type_with_it_live() {
|
||||
// GUARDS: Free Distort, Pucker & Bloat, Twist, Zig Zag and Tweak weren't read, and type with them came in
|
||||
// without its art style, the style's drawing beside it as paths.
|
||||
use serde_json::json;
|
||||
let doc = text_document(&[("Bang\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]);
|
||||
let drawing = "0 0 1 0 k\n54 56 m\n100 56 L\n100 80 L\n54 80 L\nf";
|
||||
let look = format!("u\n/AI11Text :\n1 /FreeUndo ,\n0 /FrameIndex ,\n0 /StoryIndex ,\n;\n{drawing}\nU\n9 () XW\n");
|
||||
let own: String = text_object(0).lines().map(|l| format!("%_{l}\n")).collect();
|
||||
let open = |name: &str, keys: &str| {
|
||||
let art = native_with_text(&(whole_object_style(name, keys) + &layer("Art", true, &format!("{look}{own}1 (Bent) XW\n"))), &doc);
|
||||
let (d, _) = crate::ai_alone(&private(&art)).unwrap();
|
||||
let (mut texts, mut paths) = (vec![], 0);
|
||||
d.walk(|n| match &n.kind {
|
||||
NodeKind::Text(t) => texts.push((t.plain_text(), n.appearance.effects.clone())),
|
||||
NodeKind::Path { .. } => paths += 1,
|
||||
_ => {}
|
||||
});
|
||||
(texts, paths)
|
||||
};
|
||||
for (name, keys, id, param, want) in [
|
||||
("Adobe Twirl", "45 /Real (angle) ,\n", "distort.twist", "angle", json!(45.0)),
|
||||
(
|
||||
"Adobe Transform",
|
||||
"5 /Real (moveH_Pts) ,\n-7 /Real (moveV_Pts) ,\n0 /Bool (transformPatterns) ,\n",
|
||||
"distort.transform",
|
||||
"moveV",
|
||||
json!(7.0),
|
||||
),
|
||||
(
|
||||
"Adobe Scribble and Tweak",
|
||||
"30 /Real (horz) ,\n20 /Real (vert) ,\n0 /Bool (anch) ,\n0 /Bool (in) ,\n0 /Bool (out) ,\n",
|
||||
"distort.tweak",
|
||||
"h",
|
||||
json!(30.0),
|
||||
),
|
||||
("Adobe Punk and Bloat", "-50 /Real (d_factor) ,\n", "distort.puckerBloat", "amount", json!(-50.0)),
|
||||
("Adobe Zigzag", "3 /Real (amount) ,\n1 /Real (absoluteness) ,\n7 /Real (ridges) ,\n", "distort.zigZag", "ridges", json!(7)),
|
||||
("Adobe Free Distort", "", "distort.freeDistort", "corners", json!([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])),
|
||||
] {
|
||||
let (texts, paths) = open(name, keys);
|
||||
let [(text, effects)] = texts.as_slice() else { panic!("{name}: {texts:?}") };
|
||||
assert_eq!((text.as_str(), paths), ("Bang", 0), "{name}");
|
||||
let [e] = effects.as_slice() else { panic!("{name}: {effects:?}") };
|
||||
assert_eq!((e.id.as_str(), &e.params[param]), (id, &want), "{name}");
|
||||
}
|
||||
// An angle that isn't a number: the drawing as paths, and no effect on the type.
|
||||
let (texts, paths) = open("Adobe Twirl", "NaN /Real (angle) ,\n");
|
||||
assert_eq!(paths, 1, "{texts:?}");
|
||||
assert!(texts.iter().all(|(_, effects)| effects.is_empty()), "{texts:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_with_convert_to_shape_comes_in_as_type_with_it_live() {
|
||||
// GUARDS: `Adobe Shape Effects` wasn't read, and type with it came in without its art style,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user