mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 05:07:51 -06:00
M3.14: gradient swatches and new art fit a placed gradient to themselves
Lossless gradient params carried a saved gradient's vector (document coordinates of the art it came from) into swatch applications and the default paint, so a swatch applied elsewhere, or the next shape drawn, got a gradient placed off the object. Applying a gradient swatch now fits it to each object (keeping its aspect, on type in text space), and the default for new art keeps all but the placement. Saving a swatch stays lossless. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d3dc4a2af3
commit
cca1d11032
@@ -167,17 +167,37 @@ fn aspect_param(p: &Value) -> Parsed<Option<f64>> {
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// `paint` as applied to an object with `bounds`: a gradient given an `aspect` but no vector is
|
||||
/// placed on the bounds so the aspect sticks.
|
||||
pub(crate) fn place_paint(paint: &Paint, p: &Value, bounds: Option<Rect>) -> Paint {
|
||||
match (paint, p.get("gradient").and_then(|g| g.get("aspect"))) {
|
||||
(Paint::Gradient(g), Some(a)) if g.geom.is_none() => {
|
||||
apply_gradient_edit(paint, &json!({ "aspect": a }), bounds).unwrap_or_else(|_| paint.clone())
|
||||
}
|
||||
/// `paint` without the placement it had on the art it came from (a swatch, or the default paint
|
||||
/// for new art): a placed gradient keeps its angle and fits each object it lands on.
|
||||
pub(crate) fn unplaced(paint: &Paint) -> Paint {
|
||||
match paint {
|
||||
Paint::Gradient(g) if g.geom.is_some() => Paint::Gradient(Box::new(GradientPaint { geom: None, ..(**g).clone() })),
|
||||
_ => paint.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `p` applying a swatch (whose gradient placement belongs to the art it was saved from)?
|
||||
fn applies_swatch(p: &Value) -> bool {
|
||||
p.get("swatch").is_some()
|
||||
}
|
||||
|
||||
/// `paint` as applied to an object with `bounds`: a gradient given an `aspect` but no vector is
|
||||
/// placed on the bounds so the aspect sticks, and a gradient swatch fits the object (keeping its
|
||||
/// aspect) instead of the art it was saved from.
|
||||
pub(crate) fn place_paint(paint: &Paint, p: &Value, bounds: Option<Rect>) -> Paint {
|
||||
let Paint::Gradient(g) = paint else { return paint.clone() };
|
||||
let aspect = match g.geom {
|
||||
Some(geom) if applies_swatch(p) => (geom.aspect != 1.0).then(|| json!(geom.aspect * 100.0)),
|
||||
None => p.get("gradient").and_then(|g| g.get("aspect")).cloned(),
|
||||
Some(_) => return paint.clone(),
|
||||
};
|
||||
let fitted = unplaced(paint);
|
||||
match aspect {
|
||||
Some(a) => apply_gradient_edit(&fitted, &json!({ "aspect": a }), bounds).unwrap_or(fitted),
|
||||
None => fitted,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the gradient edits in `p` to `paint` (pure; unit-tested).
|
||||
pub(crate) fn apply_gradient_edit(paint: &Paint, p: &Value, bounds: Option<Rect>) -> std::result::Result<Paint, String> {
|
||||
let mut gp = match paint {
|
||||
@@ -308,6 +328,7 @@ pub(crate) fn place_run_paint(paint: &Paint, p: &Value, xf: Affine, bounds: Rect
|
||||
let mut out = place_paint(paint, p, Some(bounds));
|
||||
if let (Paint::Gradient(src), Paint::Gradient(g)) = (paint, &mut out)
|
||||
&& src.geom.is_some()
|
||||
&& !applies_swatch(p)
|
||||
&& let Some(inv) = invert(xf)
|
||||
{
|
||||
g.transform(inv);
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, NodeKind};
|
||||
|
||||
use super::gradient::{place_paint, place_run_paint, run_paint_mut};
|
||||
use super::gradient::{place_paint, place_run_paint, run_paint_mut, unplaced};
|
||||
use super::*;
|
||||
use crate::EngineError;
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Fill",
|
||||
[],
|
||||
None,
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}, none?: true, swatch?: name, gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87}] (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}, ids?} sets selection fill and the default",
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}, none?: true, swatch?: name (a gradient swatch fits each object, keeping its aspect), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87}] (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}, ids?} sets selection fill and the default (new art fits a gradient to itself)",
|
||||
has_doc,
|
||||
|s, p| set_paint(s, p, true)
|
||||
),
|
||||
@@ -75,10 +75,11 @@ pub(crate) fn paint_from(s: &Session, p: &Value) -> Result<Option<Paint>> {
|
||||
|
||||
fn set_paint(s: &mut Session, p: &Value, fill: bool) -> Result<Value> {
|
||||
let paint = paint_from(s, p)?.ok_or_else(|| bad("paint.setFill", "give color, none, swatch or gradient"))?;
|
||||
// New art gets the paint fitted to itself, not placed where this one is.
|
||||
if fill {
|
||||
s.paint.fill = paint.clone();
|
||||
s.paint.fill = unplaced(&paint);
|
||||
} else {
|
||||
s.paint.stroke = paint.clone();
|
||||
s.paint.stroke = unplaced(&paint);
|
||||
}
|
||||
s.fill_active = fill;
|
||||
let ids = paint_targets(s, p)?;
|
||||
|
||||
@@ -61,8 +61,9 @@ fn gradient_params_round_trip_through_fill_and_swatches() {
|
||||
assert_eq!(got.gradient.stops, g.gradient.stops);
|
||||
let (a, b) = (got.geom.unwrap(), g.geom.unwrap());
|
||||
assert!(a.start.distance(b.start) < 1e-9 && a.end.distance(b.end) < 1e-9 && (a.aspect - b.aspect).abs() < 1e-9, "{a:?}");
|
||||
// The default proxy keeps it too, and saving it as a swatch is lossless.
|
||||
assert_eq!(s.paint.fill, Paint::Gradient(Box::new(got.clone())));
|
||||
// The default for new art keeps all but the placement (new art fits it to itself), and saving
|
||||
// it as a swatch is lossless.
|
||||
assert_eq!(s.paint.fill, Paint::Gradient(Box::new(GradientPaint { geom: None, ..got.clone() })));
|
||||
let mut q = gradient_param(&g);
|
||||
q["name"] = json!("Three");
|
||||
s.execute("swatch.new", &q).unwrap();
|
||||
@@ -346,3 +347,30 @@ fn the_selected_stop_belongs_to_its_gradient() {
|
||||
s.execute("paint.toggleActive", &json!({})).unwrap();
|
||||
assert_eq!(s.selected_stop(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swatches_and_new_art_fit_a_placed_gradient_to_themselves() {
|
||||
let mut s = session();
|
||||
rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let mut g = three_stops();
|
||||
g.gradient.kind = GradientKind::Radial;
|
||||
let mut q = gradient_param(&g);
|
||||
s.execute("paint.setFill", &q).unwrap();
|
||||
q["name"] = json!("Placed");
|
||||
s.execute("swatch.new", &q).unwrap();
|
||||
// New art drawn elsewhere fits the gradient to itself rather than to the first rectangle.
|
||||
let b = rect(&mut s, 500.0, 300.0, 100.0, 100.0);
|
||||
assert_eq!(fill_gradient(&s, b).geom, None);
|
||||
// Applying the swatch fits its gradient to each object, keeping the aspect it was saved with.
|
||||
s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"swatch": "Placed"})).unwrap();
|
||||
let geom = fill_geom(&s, b);
|
||||
assert_eq!(geom.start, Point::new(550.0, 350.0));
|
||||
assert!((geom.aspect - 0.5).abs() < 1e-9 && (geom.length() - 50.0).abs() < 1e-9, "{geom:?}");
|
||||
// On type, in text space.
|
||||
let t = text(&mut s, 100.0, 400.0);
|
||||
s.execute("paint.setFill", &json!({"swatch": "Placed"})).unwrap();
|
||||
let (_, doc) = node(&s, t).proxy_gradient(false).unwrap();
|
||||
let tb = node(&s, t).geometric_bounds().unwrap();
|
||||
assert!(doc.start.distance(tb.center()) < 1e-9 && (doc.aspect - 0.5).abs() < 1e-9, "{doc:?} on {tb:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user