mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
A gradient given to several objects fits each one's own bounds. With span: "selection", a linear or radial gradient without its own vector is placed once on the union of the targets' bounds, at its angle and aspect, and every target gets that vector in document coordinates, so the ramp runs on across them. A solid colour, a swatch, a freeform gradient or an explicit start/end with it is refused before anything changes; the next object drawn still fits the gradient to itself.
153 lines
6.5 KiB
Rust
153 lines
6.5 KiB
Rust
//! `paint.setFill` / `paint.setStroke` `span: "selection"`: one gradient placed on the union of
|
||
//! the targets' bounds, so the ramp runs across them (#2127).
|
||
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_color::{GradientPaint, Paint};
|
||
use vectorcraft_geom::{Affine, Point};
|
||
|
||
use super::*;
|
||
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 400, "height": 200})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> NodeId {
|
||
let r = s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": w, "height": h})).unwrap();
|
||
NodeId(r["id"].as_u64().unwrap())
|
||
}
|
||
|
||
fn fill_gradient(s: &Session, id: NodeId) -> GradientPaint {
|
||
match s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint() {
|
||
Paint::Gradient(g) => *g,
|
||
p => panic!("expected a gradient fill, got {p:?}"),
|
||
}
|
||
}
|
||
|
||
fn stroke_gradient(s: &Session, id: NodeId) -> GradientPaint {
|
||
match &s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().unwrap().paint {
|
||
Paint::Gradient(g) => (**g).clone(),
|
||
p => panic!("expected a gradient stroke, got {p:?}"),
|
||
}
|
||
}
|
||
|
||
fn near(a: Point, b: Point) -> bool {
|
||
(a - b).hypot() < 1e-6
|
||
}
|
||
|
||
fn black_to_white() -> Value {
|
||
json!({"kind": "linear", "stops": [{"offset": 0, "color": "#000000"}, {"offset": 1, "color": "#ffffff"}]})
|
||
}
|
||
|
||
/// The red channel of the rendered document at (x, y).
|
||
fn red_at(s: &Session, x: u32, y: u32) -> u8 {
|
||
let doc = s.doc().unwrap().doc.clone();
|
||
let img = vectorcraft_render::Renderer::new().render(&doc, 400, 200, Affine::IDENTITY, &Default::default());
|
||
img.pixel(x, y)[0]
|
||
}
|
||
|
||
#[test]
|
||
fn a_linear_gradient_spans_the_union_of_the_targets() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 50.0, 100.0, 100.0);
|
||
let b = rect(&mut s, 100.0, 0.0, 100.0, 200.0);
|
||
let c = rect(&mut s, 300.0, 50.0, 100.0, 100.0);
|
||
s.execute("paint.setFill", &json!({"ids": [a.0, b.0, c.0], "gradient": black_to_white(), "span": "selection"})).unwrap();
|
||
// One vector for all three, across the union 0..400 × 0..200 at angle 0.
|
||
for id in [a, b, c] {
|
||
let g = fill_gradient(&s, id);
|
||
let geom = g.geom.expect("placed in document coordinates");
|
||
assert!(near(geom.start, Point::new(0.0, 100.0)) && near(geom.end, Point::new(400.0, 100.0)), "{id:?}: {geom:?}");
|
||
}
|
||
// The ramp runs on across the objects: dark on the left one, light on the right one, and the
|
||
// edge the first two share is the same colour on both sides.
|
||
let (left, right) = (red_at(&s, 5, 100), red_at(&s, 395, 100));
|
||
assert!(left < 20 && right > 235, "{left} {right}");
|
||
let (edge_a, edge_b) = (i32::from(red_at(&s, 98, 100)), i32::from(red_at(&s, 102, 100)));
|
||
assert!((edge_a - edge_b).abs() <= 4 && (50..80).contains(&edge_a), "{edge_a} {edge_b}");
|
||
}
|
||
|
||
#[test]
|
||
fn without_span_each_object_fits_its_own_box() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 50.0, 100.0, 100.0);
|
||
let b = rect(&mut s, 100.0, 50.0, 100.0, 100.0);
|
||
for span in [json!(null), json!("object")] {
|
||
let mut p = json!({"ids": [a.0, b.0], "gradient": black_to_white()});
|
||
if !span.is_null() {
|
||
p["span"] = span;
|
||
}
|
||
s.execute("paint.setFill", &p).unwrap();
|
||
assert_eq!(fill_gradient(&s, a).geom, None);
|
||
// Each ramp starts dark again on its own object.
|
||
assert!(red_at(&s, 102, 100) < 20, "{p}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_selection_is_the_default_target_and_angle_and_radial_are_kept() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||
let b = rect(&mut s, 200.0, 100.0, 100.0, 100.0);
|
||
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
|
||
let mut g = black_to_white();
|
||
g["angle"] = json!(-90);
|
||
s.execute("paint.setFill", &json!({"gradient": g, "span": "selection"})).unwrap();
|
||
// Vertical, top to bottom, over the union 0..300 × 0..200.
|
||
let geom = fill_gradient(&s, b).geom.unwrap();
|
||
assert!(near(geom.start, Point::new(150.0, 0.0)) && near(geom.end, Point::new(150.0, 200.0)), "{geom:?}");
|
||
assert_eq!(fill_gradient(&s, a).geom, Some(geom));
|
||
// Radial: centred on the union, radius half its larger side, aspect kept.
|
||
s.execute("paint.setFill", &json!({"gradient": {"kind": "radial", "aspect": 50, "stops": black_to_white()["stops"]}, "span": "selection"}))
|
||
.unwrap();
|
||
let geom = fill_gradient(&s, a).geom.unwrap();
|
||
assert!(near(geom.start, Point::new(150.0, 100.0)), "{geom:?}");
|
||
assert!(((geom.end - geom.start).hypot() - 150.0).abs() < 1e-6, "{geom:?}");
|
||
assert!((geom.aspect - 0.5).abs() < 1e-9, "{geom:?}");
|
||
assert_eq!(fill_gradient(&s, b).geom, Some(geom));
|
||
}
|
||
|
||
#[test]
|
||
fn strokes_span_the_targets_too() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 50.0, 100.0, 100.0);
|
||
let b = rect(&mut s, 300.0, 50.0, 100.0, 100.0);
|
||
s.execute("paint.setStroke", &json!({"ids": [a.0, b.0], "gradient": black_to_white(), "span": "selection"})).unwrap();
|
||
let (ga, gb) = (stroke_gradient(&s, a).geom.unwrap(), stroke_gradient(&s, b).geom.unwrap());
|
||
assert_eq!(ga, gb);
|
||
assert!(ga.start.x < 0.0 && ga.end.x > 400.0, "the stroke's box, half its weight out: {ga:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn the_new_art_default_stays_unplaced() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||
s.execute("paint.setFill", &json!({"ids": [a.0], "gradient": black_to_white(), "span": "selection"})).unwrap();
|
||
s.execute("select.set", &json!({"ids": []})).unwrap();
|
||
let n = rect(&mut s, 200.0, 0.0, 50.0, 50.0);
|
||
assert_eq!(fill_gradient(&s, n).geom, None, "the next object fits the gradient to itself");
|
||
}
|
||
|
||
#[test]
|
||
fn bad_spans_are_refused_and_change_nothing() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||
let b = rect(&mut s, 200.0, 0.0, 100.0, 100.0);
|
||
let before = s.doc().unwrap().doc.clone();
|
||
let mut with_vector = black_to_white();
|
||
with_vector["start"] = json!([0, 0]);
|
||
with_vector["end"] = json!([10, 0]);
|
||
for bad in [
|
||
json!({"ids": [a.0, b.0], "gradient": black_to_white(), "span": "page"}),
|
||
json!({"ids": [a.0, b.0], "gradient": black_to_white(), "span": true}),
|
||
json!({"ids": [a.0, b.0], "color": "#ff0000", "span": "selection"}),
|
||
json!({"ids": [a.0, b.0], "gradient": {"kind": "freeform"}, "span": "selection"}),
|
||
json!({"ids": [a.0, b.0], "gradient": with_vector, "span": "selection"}),
|
||
] {
|
||
let e = s.execute("paint.setFill", &bad).unwrap_err().to_string();
|
||
assert!(e.contains("span"), "{bad}: {e}");
|
||
assert_eq!(s.doc().unwrap().doc, before, "{bad}");
|
||
}
|
||
}
|