mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
.ai import: a gradient with stops in the middle runs its whole ramp (follow-up to #1264)
The editing data places a gradient with matrices after Bg: Xm for a linear gradient's whole ramp and one Bm for each pair of stops next to each other. The reader kept the last Bm, so a gradient of three stops drew only its last part. It now uses Xm (or, without one, the span of the Bm's from the first stop to the last), and a radial gradient's one Bm as before. The angle and length are in the matrices, so Bg's own numbers are used only without them. An object's own note of its gradient after it (Bb, Bg, BB with no path) no longer becomes the fill of the next path.
This commit is contained in:
1 parent
885a6e38cd
commit
8ce63aa1d1
4 files changed
+109
-18
No files matched your search
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user