mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
M3.98: radial annotator extent ellipse, aspect handle and off-centre focal point
Radial gradients show a dashed extent ellipse: its handle across the bar sets the aspect ratio, the ellipse elsewhere rotates it, and the dot inside the centre ring drags the focal point (GradientGeom.focal) for an off-centre radial. paint.setGradientGeom takes aspect (%) and focal ([x,y] | null), and start/end may be left out; the focal point keeps its place in the ellipse as the vector, aspect and object transform. It renders as a two-point radial, exports and imports as SVG fx/fy and PDF two-point radial shadings, expands to off-centre rings, and travels in gradient params and the native format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b8793eeaef
commit
d8eff17b9f
@@ -229,7 +229,7 @@ fn flare(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let stop = |offset: f32, opacity: f32| GradientStop { opacity, ..GradientStop::new(offset, vectorcraft_color::Color::WHITE) };
|
||||
let radial = |centre: Point, radius: f64, stops: Vec<GradientStop>| {
|
||||
let mut g = GradientPaint::new(Gradient { kind: GradientKind::Radial, stops });
|
||||
g.geom = Some(GradientGeom { start: centre, end: centre + vectorcraft_geom::Vec2::new(radius, 0.0), aspect: 1.0 });
|
||||
g.geom = Some(GradientGeom { start: centre, end: centre + vectorcraft_geom::Vec2::new(radius, 0.0), aspect: 1.0, focal: None });
|
||||
Paint::Gradient(Box::new(g))
|
||||
};
|
||||
let circle = |centre: Point, radius: f64| shapes::ellipse(Rect::from_center_size(centre, (radius * 2.0, radius * 2.0)));
|
||||
|
||||
@@ -314,10 +314,11 @@ fn breaks(g: &Gradient, t0: f64, t1: f64, max_gap: f64) -> Vec<(f64, (Color, f32
|
||||
/// A linear or radial gradient's geometry over an object's box: its start, the vector to its
|
||||
/// end, the vector across it (radial: the ellipse's other axis), and the parameter ranges that
|
||||
/// cover the box (linear: along the vector `t0..t1` and across it `s0..s1` in points; radial:
|
||||
/// `0..t1`).
|
||||
/// `0..t1`, each level's ellipse centred on the way from the focal point to the start).
|
||||
struct Frame {
|
||||
radial: bool,
|
||||
start: Point,
|
||||
focal: Point,
|
||||
along: Vec2,
|
||||
across: Vec2,
|
||||
t0: f64,
|
||||
@@ -344,13 +345,17 @@ impl Frame {
|
||||
}
|
||||
// Keep a sliver of span so the art maps back invertibly.
|
||||
t1 = t1.max(t0 + 1e-6);
|
||||
Self { radial, start: geom.start, along, across, t0, t1, s0, s1 }
|
||||
Self { radial, start: geom.start, focal: geom.focal_point(), along, across, t0, t1, s0, s1 }
|
||||
}
|
||||
|
||||
/// Linear: the point at parameter `t` along and `s` across. Radial: the point at parameter `t`
|
||||
/// and angle `s` (radians).
|
||||
fn at(&self, t: f64, s: f64) -> Point {
|
||||
if self.radial { self.start + (self.along * s.cos() + self.across * s.sin()) * t } else { self.start + self.along * t + self.across * s }
|
||||
if self.radial {
|
||||
self.focal + (self.start - self.focal + self.along * s.cos() + self.across * s.sin()) * t
|
||||
} else {
|
||||
self.start + self.along * t + self.across * s
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient mesh covering the box: columns (linear) or rings (radial) where the colour
|
||||
@@ -371,7 +376,7 @@ impl Frame {
|
||||
|
||||
/// `n` solid shapes covering the box in paint order, each coloured as the gradient in the
|
||||
/// middle of its band: for a linear gradient, rectangles from each band's start to the far
|
||||
/// end, for a radial one, concentric ellipses (largest first). Each paints over the ones
|
||||
/// end, for a radial one, nested ellipses (largest first). Each paints over the ones
|
||||
/// below except its band, so no seam shows between bands. With translucent stops each shape
|
||||
/// is just its band, so nothing doubles up.
|
||||
fn pieces(&self, g: &Gradient, n: usize) -> Vec<(PathData, (Color, f32))> {
|
||||
@@ -380,7 +385,8 @@ impl Frame {
|
||||
let opaque = g.stops.iter().all(|s| s.opacity >= 1.0);
|
||||
if self.radial {
|
||||
let ellipse = |t: f64| {
|
||||
let m = Affine::new([self.along.x * t, self.along.y * t, self.across.x * t, self.across.y * t, self.start.x, self.start.y]);
|
||||
let c = self.focal + (self.start - self.focal) * t;
|
||||
let m = Affine::new([self.along.x * t, self.along.y * t, self.across.x * t, self.across.y * t, c.x, c.y]);
|
||||
shapes::ellipse(Rect::new(-1.0, -1.0, 1.0, 1.0)).transformed(m)
|
||||
};
|
||||
return (0..n)
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Gradient Vector",
|
||||
[],
|
||||
None,
|
||||
"{start: [x,y], end: [x,y] (document coordinates), ids?, stroke?: bool (default: the targeted item's kind, else the active proxy), item?: fill/stroke item index|null (alias: index; omitted: the Appearance panel's active item when it is of the edited kind)} set the gradient vector (solid paints become the default gradient; type objects set it on their runs, in text space; the aspect ratio is kept)",
|
||||
"{start?: [x,y], end?: [x,y] (document coordinates; both or neither: omitted, the vector stays), aspect?: % (radial: the extent ellipse's height / width; default: kept), focal?: [x,y] (document coordinates) | null (radial: the focal point, where the first stop sits, pulled inside the extent ellipse; null centres it; default: it keeps its place in the ellipse), ids?, stroke?: bool (default: the targeted item's kind, else the active proxy), item?: fill/stroke item index|null (alias: index; omitted: the Appearance panel's active item when it is of the edited kind)} set the gradient vector, aspect ratio and focal point (solid paints become the default gradient; type objects set it on their runs, in text space)",
|
||||
has_doc,
|
||||
set_gradient_geom
|
||||
),
|
||||
@@ -181,16 +181,12 @@ pub(crate) fn parse_gradient(g: &Value) -> Parsed<GradientPaint> {
|
||||
let mut gp = GradientPaint::new(Gradient { kind, stops });
|
||||
gp.angle = f64_or(g, "angle", 0.0);
|
||||
gp.swatch = str_param(g, "swatch").map(str::to_string);
|
||||
let aspect = aspect_param(g)?;
|
||||
let point = |k: &str| g.get(k).map(|_| point_param(g, k).ok_or_else(|| format!("`{k}` must be [x, y]"))).transpose();
|
||||
match (point("start")?, point("end")?) {
|
||||
(Some(start), Some(end)) => {
|
||||
let geom = GradientGeom { start, end, aspect: aspect.unwrap_or(1.0) };
|
||||
gp.angle = geom.angle_deg();
|
||||
gp.geom = Some(geom);
|
||||
}
|
||||
(None, None) => {}
|
||||
_ => return Err("give both `start` and `end` (or neither)".into()),
|
||||
let geom = GeomEdit::parse(g)?;
|
||||
if let Some((start, end)) = geom.vector {
|
||||
let mut g = GradientGeom { start, end, aspect: geom.aspect.unwrap_or(1.0), focal: None };
|
||||
g.set_focal(geom.focal.flatten());
|
||||
gp.angle = g.angle_deg();
|
||||
gp.geom = Some(g);
|
||||
}
|
||||
if let Some(f) = g.get("freeform") {
|
||||
gp.freeform = Some(super::freeform::parse_freeform(f)?);
|
||||
@@ -282,6 +278,8 @@ pub(crate) fn apply_gradient_edit_in(
|
||||
gp.gradient.stops[i].midpoint = if i + 1 < n { 1.0 - mids[n - 2 - i] } else { 0.5 };
|
||||
}
|
||||
}
|
||||
// Angle and aspect edits keep the focal point in its place in the extent ellipse.
|
||||
let before = gp.geom;
|
||||
if let Some(a) = p.get("angle").and_then(Value::as_f64) {
|
||||
let a = ((a + 180.0).rem_euclid(360.0)) - 180.0;
|
||||
gp.angle = a;
|
||||
@@ -308,6 +306,9 @@ pub(crate) fn apply_gradient_edit_in(
|
||||
g.aspect = asp;
|
||||
}
|
||||
}
|
||||
if let (Some(b), Some(g)) = (before, &mut gp.geom) {
|
||||
g.keep_focal_from(&b);
|
||||
}
|
||||
if gp.gradient.kind == GradientKind::Freeform {
|
||||
// Points already shown (automatic ones fitted to the box) stay put; a fresh freeform
|
||||
// gradient gets its points inside the shape.
|
||||
@@ -428,10 +429,35 @@ fn invert(a: Affine) -> Option<Affine> {
|
||||
(a.determinant().abs() > 1e-12).then(|| a.inverse())
|
||||
}
|
||||
|
||||
/// `cur` as a gradient whose vector runs from `start` to `end` in the document. `to_doc` maps the
|
||||
/// paint's space (text space for type runs) to the document and `bounds` (in that space) fits an
|
||||
/// unplaced gradient, so the aspect ratio the document shows carries over.
|
||||
fn vector_paint(cur: &Paint, start: Point, end: Point, to_doc: Affine, bounds: Option<Rect>) -> Option<Paint> {
|
||||
/// What `paint.setGradientGeom` sets, in document coordinates (None: kept).
|
||||
struct GeomEdit {
|
||||
vector: Option<(Point, Point)>,
|
||||
aspect: Option<f64>,
|
||||
/// Some(None) centres the focal point.
|
||||
focal: Option<Option<Point>>,
|
||||
}
|
||||
|
||||
impl GeomEdit {
|
||||
fn parse(p: &Value) -> std::result::Result<Self, String> {
|
||||
let point = |k: &str| p.get(k).map(|_| point_param(p, k).ok_or_else(|| format!("`{k}` must be [x, y]"))).transpose();
|
||||
let vector = match (point("start")?, point("end")?) {
|
||||
(Some(s), Some(e)) => Some((s, e)),
|
||||
(None, None) => None,
|
||||
_ => return Err("give both `start` and `end` [x, y] (or neither)".into()),
|
||||
};
|
||||
let focal = match p.get("focal") {
|
||||
None => None,
|
||||
Some(Value::Null) => Some(None),
|
||||
Some(_) => Some(point("focal")?),
|
||||
};
|
||||
Ok(Self { vector, aspect: aspect_param(p)?, focal })
|
||||
}
|
||||
}
|
||||
|
||||
/// `cur` with the vector, aspect and focal point of `e` (given in the document). `to_doc` maps
|
||||
/// the paint's space (text space for type runs) to the document and `bounds` (in that space) fits
|
||||
/// an unplaced gradient, so what the document shows carries over where `e` keeps it.
|
||||
fn vector_paint(cur: &Paint, e: &GeomEdit, to_doc: Affine, bounds: Option<Rect>) -> Option<Paint> {
|
||||
let from_doc = invert(to_doc)?;
|
||||
let mut gp = match cur {
|
||||
Paint::Gradient(g) => (**g).clone(),
|
||||
@@ -444,8 +470,17 @@ fn vector_paint(cur: &Paint, start: Point, end: Point, to_doc: Affine, bounds: O
|
||||
}
|
||||
g
|
||||
};
|
||||
let aspect = gp.geom.or_else(|| bounds.map(|b| gp.resolve(b))).map_or(1.0, |g| map(g, to_doc).aspect);
|
||||
let geom = map(GradientGeom { start, end, aspect }, from_doc);
|
||||
let shown = gp.geom.or_else(|| bounds.map(|b| gp.resolve(b))).map(|g| map(g, to_doc));
|
||||
let (start, end) = e.vector.or_else(|| shown.map(|g| (g.start, g.end)))?;
|
||||
let aspect = e.aspect.or_else(|| shown.map(|g| g.aspect)).unwrap_or(1.0);
|
||||
let mut geom = GradientGeom { start, end, aspect, focal: None };
|
||||
match (e.focal, shown) {
|
||||
_ if kind != GradientKind::Radial => {}
|
||||
(Some(f), _) => geom.set_focal(f),
|
||||
(None, Some(b)) => geom.keep_focal_from(&b),
|
||||
(None, None) => {}
|
||||
}
|
||||
let geom = map(geom, from_doc);
|
||||
gp.angle = geom.angle_deg();
|
||||
gp.geom = Some(geom);
|
||||
Some(Paint::Gradient(Box::new(gp)))
|
||||
@@ -453,8 +488,7 @@ fn vector_paint(cur: &Paint, start: Point, end: Point, to_doc: Affine, bounds: O
|
||||
|
||||
fn set_gradient_geom(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "paint.setGradientGeom";
|
||||
let start = point_param(p, "start").ok_or_else(|| bad(C, "missing start [x,y]"))?;
|
||||
let end = point_param(p, "end").ok_or_else(|| bad(C, "missing end [x,y]"))?;
|
||||
let edit = GeomEdit::parse(p).map_err(|e| bad(C, e))?;
|
||||
// `index` is an alias of `item`.
|
||||
let mut p = p.clone();
|
||||
if let (None, Some(i)) = (p.get("item"), p.get("index").cloned()) {
|
||||
@@ -481,14 +515,14 @@ fn set_gradient_geom(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
run_stroke_weight(&mut r.style);
|
||||
}
|
||||
let (paint, b) = run_paint_mut(r, stroke, lb);
|
||||
if let Some(np) = vector_paint(paint, start, end, xf, Some(b)) {
|
||||
if let Some(np) = vector_paint(paint, &edit, xf, Some(b)) {
|
||||
*paint = np;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let b = item_paint_bounds(n, index, !stroke);
|
||||
if let Some(np) = vector_paint(n.appearance.paint_at(index, !stroke).unwrap_or(&Paint::None), start, end, Affine::IDENTITY, b) {
|
||||
if let Some(np) = vector_paint(n.appearance.paint_at(index, !stroke).unwrap_or(&Paint::None), &edit, Affine::IDENTITY, b) {
|
||||
n.appearance.set_paint_at(index, !stroke, np);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Fill",
|
||||
[],
|
||||
None,
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray)} sets the selection's fill and the default (new art fits a gradient to itself)",
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), focal?: [x,y] (radial, with start/end: the focal point, where the first stop sits), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray)} sets the selection's fill and the default (new art fits a gradient to itself)",
|
||||
has_doc,
|
||||
|s, p| set_paint(s, p, true)
|
||||
),
|
||||
|
||||
@@ -875,6 +875,8 @@ mod tests_flatpreview;
|
||||
#[cfg(test)]
|
||||
mod tests_flatten;
|
||||
#[cfg(test)]
|
||||
mod tests_focal;
|
||||
#[cfg(test)]
|
||||
mod tests_freeform;
|
||||
#[cfg(test)]
|
||||
mod tests_gradient;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Radial gradients' extent and focal point (M3.98): `paint.setGradientGeom {aspect, focal}`,
|
||||
//! focal points through transforms, params and the native format, and the radial annotator.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_color::{GradientGeom, Paint};
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A 200 pt square at (100, 100) with a radial fill centred on it, radius 100.
|
||||
fn radial() -> (Session, NodeId) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
let r = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 200})).unwrap();
|
||||
let id = NodeId(r["id"].as_u64().unwrap());
|
||||
s.execute("paint.setFill", &json!({"gradient": {"kind": "radial", "start": [200, 200], "end": [300, 200]}})).unwrap();
|
||||
(s, id)
|
||||
}
|
||||
|
||||
fn geom(s: &Session, id: NodeId) -> GradientGeom {
|
||||
match s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint() {
|
||||
Paint::Gradient(g) => g.geom.expect("placed"),
|
||||
p => panic!("not a gradient: {p:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn close(a: Point, b: Point) -> bool {
|
||||
a.distance(b) < 1e-6
|
||||
}
|
||||
|
||||
fn undo_len(s: &Session) -> usize {
|
||||
s.doc().unwrap().history.undo.len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_gradient_geom_sets_aspect_and_focal_in_one_step() {
|
||||
let (mut s, id) = radial();
|
||||
let n = undo_len(&s);
|
||||
s.execute("paint.setGradientGeom", &json!({"focal": [160, 210], "aspect": 50})).unwrap();
|
||||
let g = geom(&s, id);
|
||||
assert_eq!((g.start, g.end, g.aspect, g.focal), (Point::new(200.0, 200.0), Point::new(300.0, 200.0), 0.5, Some(Point::new(160.0, 210.0))));
|
||||
assert_eq!(undo_len(&s), n + 1);
|
||||
// Out of the extent ellipse it is pulled in; null centres it.
|
||||
s.execute("paint.setGradientGeom", &json!({"focal": [200, 400]})).unwrap();
|
||||
assert!(close(geom(&s, id).focal.unwrap(), Point::new(200.0, 249.5)), "{:?}", geom(&s, id));
|
||||
s.execute("paint.setGradientGeom", &json!({"focal": null})).unwrap();
|
||||
assert_eq!(geom(&s, id).focal, None);
|
||||
// One end without the other is an error; linear gradients have no focal point.
|
||||
assert!(s.execute("paint.setGradientGeom", &json!({"end": [0, 0]})).is_err());
|
||||
s.execute("paint.editGradient", &json!({"kind": "linear"})).unwrap();
|
||||
s.execute("paint.setGradientGeom", &json!({"start": [100, 200], "end": [300, 200], "focal": [150, 200]})).unwrap();
|
||||
assert_eq!(geom(&s, id).focal, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_focal_point_keeps_its_place_as_the_gradient_moves_and_reshapes() {
|
||||
let (mut s, id) = radial();
|
||||
s.execute("paint.setGradientGeom", &json!({"focal": [160, 210]})).unwrap();
|
||||
// A new vector: moved 10 right and turned to point down.
|
||||
s.execute("paint.setGradientGeom", &json!({"start": [210, 200], "end": [210, 300]})).unwrap();
|
||||
assert!(close(geom(&s, id).focal.unwrap(), Point::new(200.0, 160.0)), "{:?}", geom(&s, id));
|
||||
// The aspect ratio (Gradient panel) squashes it with the ellipse.
|
||||
s.execute("paint.editGradient", &json!({"aspect": 50})).unwrap();
|
||||
assert!(close(geom(&s, id).focal.unwrap(), Point::new(205.0, 160.0)), "{:?}", geom(&s, id));
|
||||
// Transforming the object maps it.
|
||||
s.execute("object.move", &json!({"dx": 5, "dy": 7})).unwrap();
|
||||
assert!(close(geom(&s, id).focal.unwrap(), Point::new(210.0, 167.0)), "{:?}", geom(&s, id));
|
||||
s.execute("object.transform", &json!({"matrix": [0, 1, -1, 0, 0, 0]})).unwrap();
|
||||
assert!(close(geom(&s, id).focal.unwrap(), Point::new(-167.0, 210.0)), "{:?}", geom(&s, id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_focal_point_round_trips_through_params_and_the_native_format() {
|
||||
let (mut s, id) = radial();
|
||||
s.execute("paint.setGradientGeom", &json!({"focal": [170, 190], "aspect": 80})).unwrap();
|
||||
let want = geom(&s, id);
|
||||
// Gradient params (what swatches and agents read) carry it, and give it back.
|
||||
let Paint::Gradient(g) = s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint() else { unreachable!() };
|
||||
let params = vectorcraft_tools::params::gradient_params(&g);
|
||||
assert_eq!(params["focal"], json!([170.0, 190.0]));
|
||||
s.execute("paint.setFill", &json!({"gradient": {"kind": "linear"}})).unwrap();
|
||||
s.execute("paint.setFill", &json!({ "gradient": params })).unwrap();
|
||||
assert_eq!(geom(&s, id), want);
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
let Paint::Gradient(g) = back.node(id).unwrap().appearance.fill_paint() else { panic!("gradient lost") };
|
||||
assert_eq!(g.geom, Some(want));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_radial_annotator_drags_the_focal_dot_the_aspect_handle_and_the_ring() {
|
||||
let (mut s, id) = radial();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("gradient", v).unwrap();
|
||||
let drag = |s: &mut Session, pts: &[(f64, f64)]| {
|
||||
let n = undo_len(s);
|
||||
for (i, (x, y)) in pts.iter().enumerate() {
|
||||
let kind = [PointerKind::Down, PointerKind::Drag][i.min(1)];
|
||||
s.pointer(&PointerEvent::new(kind, *x, *y).with_mods(Mods::default()), v).unwrap();
|
||||
}
|
||||
let (x, y) = pts[pts.len() - 1];
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, x, y), v).unwrap();
|
||||
assert_eq!(undo_len(s), n + 1, "one undo step");
|
||||
};
|
||||
// The focal dot sits on the centre: dragging it moves only the focal point.
|
||||
drag(&mut s, &[(200.0, 200.0), (180.0, 195.0), (170.0, 190.0)]);
|
||||
let g = geom(&s, id);
|
||||
assert_eq!((g.start, g.focal), (Point::new(200.0, 200.0), Some(Point::new(170.0, 190.0))));
|
||||
// Back onto the centre, it centres.
|
||||
drag(&mut s, &[(170.0, 190.0), (190.0, 195.0), (200.5, 200.5)]);
|
||||
assert_eq!(geom(&s, id).focal, None);
|
||||
// The handle across the bar (100 pt above the centre) sets the aspect ratio.
|
||||
drag(&mut s, &[(200.0, 100.0), (200.0, 120.0), (200.0, 150.0)]);
|
||||
assert!((geom(&s, id).aspect - 0.5).abs() < 1e-9, "{:?}", geom(&s, id));
|
||||
// The ellipse elsewhere rotates it, keeping its radius: from its bottom round to its left
|
||||
// turns the vector a quarter clockwise, to point down.
|
||||
drag(&mut s, &[(200.0, 250.0), (170.0, 240.0), (150.0, 200.0)]);
|
||||
let g = geom(&s, id);
|
||||
assert!(close(g.end, Point::new(200.0, 300.0)) && (g.aspect - 0.5).abs() < 1e-9, "{g:?}");
|
||||
}
|
||||
@@ -41,7 +41,7 @@ fn three_stops() -> GradientPaint {
|
||||
stop(1.0, Color::gray(0.25), 0.75, 0.5),
|
||||
],
|
||||
});
|
||||
g.geom = Some(GradientGeom { start: Point::new(10.0, 20.0), end: Point::new(110.0, 70.0), aspect: 0.5 });
|
||||
g.geom = Some(GradientGeom { start: Point::new(10.0, 20.0), end: Point::new(110.0, 70.0), aspect: 0.5, focal: None });
|
||||
g.angle = g.geom.unwrap().angle_deg();
|
||||
g
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user