Merge pull request #1390 from storytold/fix/1264-ai-gradient
.ai import: a gradient with stops in the middle runs its whole ramp (follow-up to #1264)
This commit is contained in:
commit
b185fbbe45
|
|
@ -1741,6 +1741,7 @@ fn editing_text() -> String {
|
|||
"U".into(),
|
||||
"0 Xw".into(),
|
||||
"1 0 0 0 1 0 Bg".into(),
|
||||
"Bb\n1 (G) 0 0 0 1 1 0 0 1 0 0 1 Bg\n1 0 0 1 0 0 Xm\n0 0 0 0 0 0 Bm\n1 0 0 1 5 5 Bm\n0 BB".into(),
|
||||
"0 1 w 2 J 0 j 4 M [3 2]0 d 1 D".into(),
|
||||
"0 0 0 1 (Black global) 0.4 0 Xk".into(),
|
||||
"(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p".into(),
|
||||
|
|
|
|||
|
|
@ -9,8 +9,12 @@
|
|||
//! - Gradient definitions: `(name) type stops Bd`, its stops as `colour style [opacity 6]
|
||||
//! midpoint ramp Bs` (style 0 grey, 1 CMYK, 2 RGB with its CMYK, 3 and 4 named colours), `BD`.
|
||||
//! - A gradient on an object: `[flag] Bb` (1: the stroke), `flag (name) x y angle length a b c d
|
||||
//! tx ty Bg`, the gradient matrix `a b c d tx ty Bm`, the focal point `x y angle length Bh`,
|
||||
//! `flag BB`.
|
||||
//! tx ty Bg`, the focal point `x y angle length Bh`, `flag BB`. Newer files write the gradient's
|
||||
//! place as matrices after `Bg` (whose own numbers are then not in art space): `a b c d tx ty Xm`
|
||||
//! takes the unit vector onto a linear gradient's whole ramp (0 to 100, its angle and length in
|
||||
//! it), and `a b c d tx ty Bm` takes it onto the part between two stops next to each other, one
|
||||
//! `Bm` for each pair in the order of the ramp (`Bc`, around them, are the caps past the first and
|
||||
//! last stops). A radial gradient has one `Bm`, which takes the unit circle onto it.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -166,8 +170,10 @@ pub(super) struct Instance {
|
|||
length: f64,
|
||||
/// `Bg`'s own matrix.
|
||||
bg: Affine,
|
||||
/// The gradient matrix (`Bm`), when there is one.
|
||||
bm: Option<Affine>,
|
||||
/// The matrix of a linear gradient's whole ramp (`Xm`), when there is one.
|
||||
xm: Option<Affine>,
|
||||
/// The first and the last gradient matrix (`Bm`), when there are any.
|
||||
bm: Option<(Affine, Affine)>,
|
||||
/// The focal point's offset from the origin (`Bh`).
|
||||
hilight: Vec2,
|
||||
}
|
||||
|
|
@ -197,10 +203,22 @@ impl Instance {
|
|||
}
|
||||
}
|
||||
|
||||
/// `a b c d tx ty Bm`.
|
||||
pub fn bm(&mut self, vals: &[V]) {
|
||||
/// The matrix of `a b c d tx ty Bm` or `Xm`, when it can be inverted.
|
||||
fn matrix(vals: &[V]) -> Option<Affine> {
|
||||
let n: Vec<f64> = vals.iter().filter_map(V::num).collect();
|
||||
self.bm = n.get(n.len().saturating_sub(6)..).and_then(affine).filter(|m| m.determinant().abs() > 1e-12);
|
||||
n.get(n.len().saturating_sub(6)..).and_then(affine).filter(|m| m.determinant().abs() > 1e-12)
|
||||
}
|
||||
|
||||
/// `a b c d tx ty Bm`: one for each pair of stops next to each other.
|
||||
pub fn bm(&mut self, vals: &[V]) {
|
||||
if let Some(m) = Self::matrix(vals) {
|
||||
self.bm = Some((self.bm.map_or(m, |(first, _)| first), m));
|
||||
}
|
||||
}
|
||||
|
||||
/// `a b c d tx ty Xm`.
|
||||
pub fn xm(&mut self, vals: &[V]) {
|
||||
self.xm = Self::matrix(vals);
|
||||
}
|
||||
|
||||
/// `x y angle length Bh`.
|
||||
|
|
@ -215,19 +233,35 @@ impl Instance {
|
|||
pub fn paint(&self, defs: &HashMap<String, Gradient>, to_doc: Affine) -> Option<Paint> {
|
||||
let name = self.name.as_ref()?;
|
||||
let gradient = defs.get(name)?.clone();
|
||||
let m = to_doc * self.bm.unwrap_or(Affine::IDENTITY) * self.bg;
|
||||
let a = self.angle.to_radians();
|
||||
let dir = Vec2::new(a.cos(), a.sin()) * self.length;
|
||||
let start = m * self.origin;
|
||||
let end = m * (self.origin + dir);
|
||||
let radial = gradient.kind == GradientKind::Radial;
|
||||
// The unit vector through `m`, the point across it (for an elliptical radial gradient) and
|
||||
// the focal point.
|
||||
let unit = |m: Affine| (m * Point::ORIGIN, m * Point::new(1.0, 0.0), m * Point::new(0.0, 1.0), m * self.hilight.to_point());
|
||||
let whole = if radial { self.bm.map(|(m, _)| m).or(self.xm) } else { self.xm };
|
||||
let (start, end, across, focal) = match (whole, self.bm) {
|
||||
(Some(m), _) => unit(to_doc * m),
|
||||
// A linear gradient's `Bm`s alone: they run from its first stop to its last.
|
||||
(None, Some((first, last))) => {
|
||||
let (a, b) = (to_doc * first * Point::ORIGIN, to_doc * last * Point::new(1.0, 0.0));
|
||||
let from = gradient.stops.first().map_or(0.0, |s| f64::from(s.offset));
|
||||
let to = gradient.stops.last().map_or(1.0, |s| f64::from(s.offset));
|
||||
let whole = (b - a) / (to - from).max(1e-6);
|
||||
let start = a - whole * from;
|
||||
(start, start + whole, start, start)
|
||||
}
|
||||
// `Bg`'s own numbers, in art space through its matrix.
|
||||
(None, None) => {
|
||||
let m = to_doc * self.bg;
|
||||
let a = self.angle.to_radians();
|
||||
let dir = Vec2::new(a.cos(), a.sin()) * self.length;
|
||||
(m * self.origin, m * (self.origin + dir), m * (self.origin + Vec2::new(-dir.y, dir.x)), m * (self.origin + self.hilight))
|
||||
}
|
||||
};
|
||||
if !(start.is_finite() && end.is_finite()) || start.distance(end) < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
// Across the vector, for an elliptical radial gradient.
|
||||
let across = m * (self.origin + Vec2::new(-dir.y, dir.x));
|
||||
let aspect = (across.distance(start) / start.distance(end)).clamp(0.01, 100.0);
|
||||
let focal = (self.hilight.hypot() > 1e-9).then(|| m * (self.origin + self.hilight)).filter(|p| p.is_finite());
|
||||
let radial = gradient.kind == GradientKind::Radial;
|
||||
let focal = (self.hilight.hypot() > 1e-9).then_some(focal).filter(|p| p.is_finite());
|
||||
let geom = GradientGeom { start, end, aspect: if radial { aspect } else { 1.0 }, focal: if radial { focal } else { None } };
|
||||
let angle = -(end - start).atan2().to_degrees();
|
||||
Some(Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), angle, swatch: Some(name.clone()), ..GradientPaint::new(gradient) })))
|
||||
|
|
@ -308,4 +342,35 @@ mod tests {
|
|||
assert!((geom.aspect - 1.0).abs() < 1e-9);
|
||||
assert!(i.paint(&HashMap::new(), Affine::IDENTITY).is_none());
|
||||
}
|
||||
|
||||
/// A linear gradient of three stops as newer files place it: `Bg` with numbers that aren't in
|
||||
/// art space, `Xm` for the whole ramp and a `Bm` for each pair of stops.
|
||||
#[test]
|
||||
fn a_linear_gradient_runs_its_whole_ramp_not_its_last_part() {
|
||||
let mut d = GradientDef::begin(&[V::Str(b"L".to_vec()), V::Num(0.0), V::Num(3.0)]).unwrap();
|
||||
for ramp in [100.0, 60.0, 20.0] {
|
||||
d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut vec![]);
|
||||
}
|
||||
let defs = HashMap::from([d.finish().unwrap()]);
|
||||
let instance = |xm: bool| {
|
||||
let mut i = Instance::begin(&[]);
|
||||
let mut bg = vec![V::Num(1.0), V::Str(b"L".to_vec())];
|
||||
bg.extend(nums(&[0.3, -0.2, -90.0, 0.5, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0]));
|
||||
i.bg(&bg);
|
||||
if xm {
|
||||
i.xm(&nums(&[0.0, -200.0, -30.0, 0.0, 10.0, 300.0]));
|
||||
}
|
||||
// From the stop at 20 to the one at 60, then on to the one at 100.
|
||||
i.bm(&nums(&[0.0, -80.0, -30.0, 0.0, 10.0, 260.0]));
|
||||
i.bm(&nums(&[0.0, -80.0, -30.0, 0.0, 10.0, 180.0]));
|
||||
let Some(Paint::Gradient(gp)) = i.paint(&defs, Affine::IDENTITY) else { panic!() };
|
||||
gp.geom.unwrap()
|
||||
};
|
||||
for xm in [true, false] {
|
||||
let geom = instance(xm);
|
||||
// (The stops' offsets are `f32`.)
|
||||
assert!(geom.start.distance(Point::new(10.0, 300.0)) < 1e-5 && geom.end.distance(Point::new(10.0, 100.0)) < 1e-5, "{xm}: {geom:?}");
|
||||
assert_eq!((geom.aspect, geom.focal), (1.0, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,7 +338,6 @@ const IGNORED: &[&str] = &[
|
|||
"X=",
|
||||
"X+",
|
||||
"Bc",
|
||||
"Xm",
|
||||
"XP",
|
||||
"Np",
|
||||
// Flatness (`i`): how finely curves print.
|
||||
|
|
@ -921,13 +920,22 @@ impl<'a> Reader<'a> {
|
|||
i.bm(&vals);
|
||||
}
|
||||
}
|
||||
"Xm" => {
|
||||
if let Some(i) = self.instance.as_mut() {
|
||||
i.xm(&vals);
|
||||
}
|
||||
}
|
||||
"Bh" => {
|
||||
if let Some(i) = self.instance.as_mut() {
|
||||
i.bh(&vals);
|
||||
}
|
||||
}
|
||||
"BB" => {
|
||||
if let Some(i) = self.instance.take() {
|
||||
// An instance that closes with no path to paint is an object's own note of its
|
||||
// gradient, written after it: it paints nothing (not the next path either).
|
||||
if let Some(i) = self.instance.take()
|
||||
&& !self.path.elements().is_empty()
|
||||
{
|
||||
self.set_instance(&i);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,6 +281,23 @@ fn gradients() {
|
|||
assert_eq!((geom.start, geom.end), (Point::new(100.0, 50.0), Point::new(150.0, 50.0)));
|
||||
// The gradient was the one object's.
|
||||
assert_eq!(fill(&a[1]).color(), Some(Color::rgb(1.0, 0.0, 0.0)));
|
||||
// A linear gradient with a stop in the middle: `Xm` places its whole ramp, a `Bm` each of its
|
||||
// two parts. The object's own note of its gradient after it paints nothing.
|
||||
let defs = "(L) 0 3 Bd\n%_1 0 50 100 Bs\n%_0.5 0 50 40 Bs\n%_0 0 50 0 Bs\nBD\n";
|
||||
let art_ = format!(
|
||||
"{RED}{}Bb\n1 (L) 0 0 -90 1 1 0 0 1 0 0 1 Bg\n0 -100 -100 0 150 100 Xm\n0 -9000 -100 0 150 9100 Bc\n0 -40 -100 0 150 100 Bm\n\
|
||||
0 -60 -100 0 150 60 Bm\n0 -9000 -100 0 150 0 Bc\nf\n0 BB\n0 1 Xd\nBb\n2 (L) 0 0 -90 1 1 0 0 1 0 0 1 Bg\n0 BB\n6 () XW\n{}f\n",
|
||||
rect_path(50.0, 0.0, 150.0, 100.0),
|
||||
rect_path(0.0, 0.0, 10.0, 10.0),
|
||||
);
|
||||
let doc = read_ok(&format!("{defs}{}", layer(&art_))).doc;
|
||||
let a = art(&doc);
|
||||
let Paint::Gradient(g) = fill(&a[0]) else { panic!("{:?}", fill(&a[0])) };
|
||||
assert_eq!(g.gradient.stops.len(), 3);
|
||||
// Art space has y up from the crop marks' bottom.
|
||||
let geom = g.geom.unwrap();
|
||||
assert!(geom.start.distance(Point::new(150.0, 0.0)) < 1e-9 && geom.end.distance(Point::new(150.0, 100.0)) < 1e-9, "{geom:?}");
|
||||
assert_eq!(fill(&a[1]).color(), Some(Color::rgb(1.0, 0.0, 0.0)), "the note after the object isn't the next path's paint");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue