mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 20:36:02 +00:00
Merge pull request #7591 from dotCooCoo/fix/swatch-edit-gradient-aspect
Swatches: a replacement gradient or pattern is stored the way a new swatch stores it
This commit is contained in:
@@ -19,7 +19,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New Swatch",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name?, color? | colors?: [colour] (one swatch each, in one undo step; #rrggbbaa and #rgba are refused) | swatch? | gradient? | pattern?: name (default: the current fill), tint?: 0..100 (% of the global or spot `swatch`, or of the one the current fill links to), mode?: \"gray\"|\"rgb\"|\"hsb\"|\"lab\"|\"cmyk\"|\"web\" (convert the colour; lab: CIE L*a*b*, how spot colours are usually defined), global?, spot? (a spot colour, always global), group?: colour group name (solid colours only; default: ungrouped), none?: true (the None paint), paint?: paint.setFill params, keepModel?: bool (as paint.setFill)} save a colour, gradient or pattern as a swatch. A tint of a global or spot colour (below 100%, without mode, global or spot) becomes a tint swatch, \"Name 40%\", linked to its base: it follows edits to the base and applies as that tint of it. A gradient swatch holds no link to the swatch its gradient came from, and a pattern swatch holds the pattern untransformed. Names are unique (\"Sky 2\"); a colour's default name is its values (\"C=10 M=20 Y=30 K=0\", \"R=255 G=128 B=0\", \"Gray K=40\", \"L=55 a=60 b=40\") → {name, names: [every new swatch]}",
|
||||
"{name?, color? | colors?: [colour] (one swatch each, in one undo step; #rrggbbaa and #rgba are refused) | swatch? | gradient? | pattern?: name (default: the current fill), tint?: 0..100 (% of the global or spot `swatch`, or of the one the current fill links to), mode?: \"gray\"|\"rgb\"|\"hsb\"|\"lab\"|\"cmyk\"|\"web\" (convert the colour; lab: CIE L*a*b*, how spot colours are usually defined), global?, spot? (a spot colour, always global), group?: colour group name (solid colours only; default: ungrouped), none?: true (the None paint), paint?: paint.setFill params, keepModel?: bool (as paint.setFill)} save a colour, gradient or pattern as a swatch. A tint of a global or spot colour (below 100%, without mode, global or spot) becomes a tint swatch, \"Name 40%\", linked to its base: it follows edits to the base and applies as that tint of it. A gradient swatch keeps the gradient's aspect (with or without start/end) and holds no link to the swatch its gradient came from, and a pattern swatch holds the pattern untransformed. Names are unique (\"Sky 2\"); a colour's default name is its values (\"C=10 M=20 Y=30 K=0\", \"R=255 G=128 B=0\", \"Gray K=40\", \"L=55 a=60 b=40\") → {name, names: [every new swatch]}",
|
||||
has_doc,
|
||||
swatch_new
|
||||
),
|
||||
@@ -48,7 +48,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Swatch Options",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name, newName?, color? (e.g. {l, a, b} for a Lab colour; #rrggbbaa and #rgba are refused), keepModel?: bool (as paint.setFill; the swatch's own color and a color given with mode keep their model), mode?: \"gray\"|\"rgb\"|\"hsb\"|\"lab\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (spot colours are always global), paint?: paint.setFill params ({color}, {gradient}, {swatch} or {pattern}) replacing the swatch's colour; gradient or pattern with one of the same kind} edit a swatch in any colour group, as one undo step. Fills, strokes, text, gradient stops and tint swatches linked to a global swatch take its new colour (at their own tint) and name; turning Global off unlinks them (they keep their colour). Colour, mode and spot apply to solid colours only; a tint swatch only takes a new name (edit its base) → {name, relinked: paints changed}",
|
||||
"{name, newName?, color? (e.g. {l, a, b} for a Lab colour; #rrggbbaa and #rgba are refused), keepModel?: bool (as paint.setFill; the swatch's own color and a color given with mode keep their model), mode?: \"gray\"|\"rgb\"|\"hsb\"|\"lab\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (spot colours are always global), paint?: paint.setFill params ({color}, {gradient} or {swatch}) or {pattern: name} replacing the swatch's colour; gradient or pattern with one of the same kind (stored as swatch.new stores it: a gradient keeps its aspect with or without start/end and holds no swatch link; a pattern is untransformed)} edit a swatch in any colour group, as one undo step. Fills, strokes, text, gradient stops and tint swatches linked to a global swatch take its new colour (at their own tint) and name; turning Global off unlinks them (they keep their colour). Colour, mode and spot apply to solid colours only; a tint swatch only takes a new name (edit its base) → {name, relinked: paints changed}",
|
||||
has_doc,
|
||||
swatch_edit
|
||||
),
|
||||
@@ -187,11 +187,7 @@ fn swatch_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})),
|
||||
Paint::None => Err(bad(C, "a swatch needs a colour, gradient or pattern")),
|
||||
_ if spot || mode.is_some() || group.is_some() => Err(bad(C, "spot, mode and group apply to solid colours only")),
|
||||
// The swatch holds the gradient itself, not a link to the swatch it may have come from.
|
||||
// It keeps `geom`, which holds the aspect the swatch is applied with.
|
||||
Paint::Gradient(g) => Ok(Paint::Gradient(Box::new(vectorcraft_color::GradientPaint { swatch: None, ..*g }))),
|
||||
// The swatch holds the pattern untransformed, not the placement of the art it came from.
|
||||
Paint::Pattern { pattern, .. } => Ok(pattern_paint(&pattern)),
|
||||
paint @ (Paint::Gradient(_) | Paint::Pattern { .. }) => Ok(stored_paint(paint, p)),
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
// A name applies to a single new swatch.
|
||||
@@ -220,6 +216,27 @@ fn swatch_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "name": names[0], "names": names }))
|
||||
}
|
||||
|
||||
/// A gradient or pattern `paint`, parsed from the paint params `p`, as a new or edited swatch
|
||||
/// stores it.
|
||||
fn stored_paint(paint: Paint, p: &Value) -> Paint {
|
||||
match paint {
|
||||
// A gradient given an `aspect` but no vector is placed on a box so that the swatch keeps
|
||||
// the aspect, as `paint.setFill` places it on each object.
|
||||
Paint::Gradient(ref g) if g.geom.is_none() && g.freeform.is_none() => {
|
||||
match super::gradient::place_paint(&paint, p, Some(vectorcraft_geom::Rect::new(0.0, 0.0, 100.0, 100.0))) {
|
||||
Paint::Gradient(g) => Paint::Gradient(Box::new(vectorcraft_color::GradientPaint { swatch: None, ..*g })),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
// The swatch holds the gradient itself, not a link to the swatch it may have come from.
|
||||
// It keeps `geom`, which holds the aspect the swatch is applied with.
|
||||
Paint::Gradient(g) => Paint::Gradient(Box::new(vectorcraft_color::GradientPaint { swatch: None, ..*g })),
|
||||
// The swatch holds the pattern untransformed, not the placement of the art it came from.
|
||||
Paint::Pattern { pattern, .. } => pattern_paint(&pattern),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `names` list and `name` parameters, without repeats; at least one is required.
|
||||
pub(super) fn names_param(p: &Value, cmd: &str) -> Result<Vec<String>> {
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
@@ -866,12 +883,21 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
// A replacement paint of the swatch's kind (a dropped colour or edited gradient).
|
||||
let paint = match p.get("paint").filter(|v| !v.is_null()) {
|
||||
Some(v) => match (if given("mode") { paint_from(s, &keeping_model(v))? } else { paint_from(s, v)? }, &sw.paint) {
|
||||
Some(v) => match (
|
||||
// A pattern by name, as `swatch.new` takes it.
|
||||
match str_param(v, "pattern") {
|
||||
Some(pat) if d.pattern(pat).is_some() => Some(pattern_paint(pat)),
|
||||
Some(pat) => return Err(bad(C, format!("no pattern `{pat}`"))),
|
||||
None if given("mode") => paint_from(s, &keeping_model(v))?,
|
||||
None => paint_from(s, v)?,
|
||||
},
|
||||
&sw.paint,
|
||||
) {
|
||||
(Some(Paint::Solid { color, .. }), Paint::Solid { .. }) => Some(Paint::solid(color)),
|
||||
(Some(Paint::Gradient(g)), Paint::Gradient(_)) => {
|
||||
Some(Paint::Gradient(Box::new(vectorcraft_color::GradientPaint { geom: None, swatch: None, ..*g })))
|
||||
// Stored as `swatch.new` stores it.
|
||||
(Some(new @ Paint::Gradient(_)), Paint::Gradient(_)) | (Some(new @ Paint::Pattern { .. }), Paint::Pattern { .. }) => {
|
||||
Some(stored_paint(new, v))
|
||||
}
|
||||
(Some(new @ Paint::Pattern { .. }), Paint::Pattern { .. }) => Some(new),
|
||||
_ => return Err(bad(C, format!("`{name}` takes a paint of its own kind (colour, gradient or pattern)"))),
|
||||
},
|
||||
None => None,
|
||||
|
||||
@@ -636,6 +636,114 @@ fn gradient_libraries_are_listed_and_gradient_swatches_rename_and_take_new_gradi
|
||||
assert_eq!(doc(&s).swatch("Red").unwrap().paint.color().map(|c| c.to_hex()), Some("#00ff00".into()));
|
||||
}
|
||||
|
||||
/// A radial gradient from red to blue, with `extra` keys.
|
||||
fn radial_gradient(extra: Value) -> Value {
|
||||
let mut g = json!({"kind": "radial", "stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]});
|
||||
if let (Some(g), Some(extra)) = (g.as_object_mut(), extra.as_object()) {
|
||||
g.extend(extra.clone());
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// The gradient swatch `name` keeps an aspect of 50%, holds no swatch link, and applies with that
|
||||
/// aspect, fitted to the object it is applied to.
|
||||
fn assert_swatch_applies_with_half_aspect(s: &mut Session, name: &str) {
|
||||
let Paint::Gradient(sw) = &doc(s).swatch(name).unwrap().paint else { panic!("{name}: a gradient swatch") };
|
||||
assert!(sw.geom.is_some_and(|g| (g.aspect - 0.5).abs() < 1e-9), "{name} keeps the aspect: {:?}", sw.geom);
|
||||
assert_eq!(sw.swatch, None, "{name} holds no link to the swatch its gradient came from");
|
||||
let r = run(s, "shape.rectangle", json!({"x": 100, "y": 0, "width": 100, "height": 60}));
|
||||
let id = r["id"].as_u64().unwrap();
|
||||
run(s, "paint.setFill", json!({"ids": [id], "swatch": name}));
|
||||
let fill = doc(s).node(vectorcraft_doc::NodeId(id)).unwrap().appearance.fill_paint();
|
||||
let Paint::Gradient(g) = fill else { panic!("{name}: a gradient fill") };
|
||||
let geom = g.geom.unwrap();
|
||||
assert!(
|
||||
geom.start.distance(vectorcraft_geom::Point::new(150.0, 30.0)) < 1e-9 && (geom.aspect - 0.5).abs() < 1e-9,
|
||||
"{name} fits the object with its aspect: {geom:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// An edited gradient with a vector and an aspect that replaces a gradient swatch keeps its aspect,
|
||||
/// as a new swatch made from it does.
|
||||
#[test]
|
||||
fn a_gradient_swatch_takes_an_edited_gradient_with_its_aspect() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Edited", "gradient": radial_gradient(json!({}))}));
|
||||
let placed = radial_gradient(json!({"start": [25, 25], "end": [50, 25], "aspect": 50}));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Edited", "paint": {"gradient": placed}}));
|
||||
assert_swatch_applies_with_half_aspect(&mut s, "Edited");
|
||||
}
|
||||
|
||||
/// A gradient swatch Alt-dropped on another gradient swatch gives it its gradient with its aspect.
|
||||
#[test]
|
||||
fn a_gradient_swatch_takes_a_dropped_swatch_with_its_aspect() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Dropped", "gradient": radial_gradient(json!({}))}));
|
||||
let placed = radial_gradient(json!({"start": [25, 25], "end": [50, 25], "aspect": 50}));
|
||||
run(&mut s, "swatch.new", json!({"name": "Oval", "gradient": placed}));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Dropped", "paint": {"swatch": "Oval"}}));
|
||||
assert_swatch_applies_with_half_aspect(&mut s, "Dropped");
|
||||
}
|
||||
|
||||
/// A gradient given an aspect but no vector keeps the aspect in a new swatch and in a replacement,
|
||||
/// as `paint.setFill` keeps it on the object.
|
||||
#[test]
|
||||
fn a_gradient_without_a_vector_keeps_its_aspect_in_a_swatch() {
|
||||
let mut s = session();
|
||||
let oval = radial_gradient(json!({"aspect": 50}));
|
||||
run(&mut s, "swatch.new", json!({"name": "New", "gradient": oval}));
|
||||
assert_swatch_applies_with_half_aspect(&mut s, "New");
|
||||
run(&mut s, "swatch.new", json!({"name": "Edited", "gradient": radial_gradient(json!({}))}));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Edited", "paint": {"gradient": oval}}));
|
||||
assert_swatch_applies_with_half_aspect(&mut s, "Edited");
|
||||
// Without an aspect the swatch stays unplaced.
|
||||
run(&mut s, "swatch.new", json!({"name": "Round", "gradient": radial_gradient(json!({}))}));
|
||||
let Paint::Gradient(round) = &doc(&s).swatch("Round").unwrap().paint else { panic!("a gradient swatch") };
|
||||
assert_eq!(round.geom, None);
|
||||
}
|
||||
|
||||
/// A pattern that replaces a pattern swatch is stored untransformed, as a new swatch stores it, so
|
||||
/// art the swatch is applied to keeps its own tile placement.
|
||||
#[test]
|
||||
fn a_pattern_swatch_takes_a_new_pattern_untransformed() {
|
||||
let mut s = session();
|
||||
for (name, color) in [("Dots", "#ff0000"), ("Bars", "#0000ff")] {
|
||||
let r = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}));
|
||||
run(&mut s, "paint.setFill", json!({"ids": [r["id"]], "color": color}));
|
||||
run(&mut s, "select.set", json!({"ids": [r["id"]]}));
|
||||
run(&mut s, "object.pattern.make", json!({"name": name, "width": 20, "height": 20}));
|
||||
run(&mut s, "object.pattern.done", json!({}));
|
||||
}
|
||||
// A pattern swatch holding a tile transform, as documents saved by older versions can.
|
||||
let moved = vectorcraft_geom::Affine::new([2.0, 0.0, 0.0, 2.0, 15.0, 10.0]);
|
||||
let st = s.doc_mut().unwrap();
|
||||
let Some(Paint::Pattern { xf, .. }) = std::sync::Arc::make_mut(&mut st.doc).swatch_mut("Bars").map(|w| &mut w.paint) else {
|
||||
panic!("the Bars pattern swatch")
|
||||
};
|
||||
*xf = moved;
|
||||
run(&mut s, "swatch.edit", json!({"name": "Dots", "paint": {"swatch": "Bars"}}));
|
||||
assert_eq!(doc(&s).swatch("Dots").unwrap().paint, vectorcraft_doc::pattern::pattern_paint("Bars"));
|
||||
}
|
||||
|
||||
/// A pattern swatch takes a pattern given by name, as an Alt-drop of a pattern fill sends it, and
|
||||
/// an unknown pattern is an error that leaves the swatch as it was.
|
||||
#[test]
|
||||
fn a_pattern_swatch_takes_a_pattern_by_name() {
|
||||
let mut s = session();
|
||||
for (name, color) in [("Dots", "#ff0000"), ("Bars", "#0000ff")] {
|
||||
let r = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}));
|
||||
run(&mut s, "paint.setFill", json!({"ids": [r["id"]], "color": color}));
|
||||
run(&mut s, "select.set", json!({"ids": [r["id"]]}));
|
||||
run(&mut s, "object.pattern.make", json!({"name": name, "width": 20, "height": 20}));
|
||||
run(&mut s, "object.pattern.done", json!({}));
|
||||
}
|
||||
run(&mut s, "swatch.edit", json!({"name": "Dots", "paint": {"pattern": "Bars"}}));
|
||||
assert_eq!(doc(&s).swatch("Dots").unwrap().paint, vectorcraft_doc::pattern::pattern_paint("Bars"));
|
||||
let before = doc(&s).swatch("Dots").unwrap().paint.clone();
|
||||
assert!(s.execute("swatch.edit", &json!({"name": "Dots", "paint": {"pattern": "Nope"}})).is_err());
|
||||
assert_eq!(doc(&s).swatch("Dots").unwrap().paint, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn organic_fades_add_with_their_method_midpoints_and_opacities_in_rgb_and_cmyk() {
|
||||
let lib = vectorcraft_color::libraries::builtin_library("organic-fades").unwrap();
|
||||
|
||||
+6
-4
@@ -892,13 +892,15 @@ Errors (unknown tool, bad arguments, a disabled or failing command) come back as
|
||||
`isError: true` and a message. The model can read the message and retry.
|
||||
|
||||
Swatches go through `run_command`: `swatch.list` lists every swatch with its colour group, kind and colour;
|
||||
`swatch.edit {name, newName?, color?, mode?, global?, spot?}` edits one (fills, strokes and text linked to a
|
||||
`swatch.edit {name, newName?, color?, mode?, global?, spot?, paint?}` edits one (fills, strokes and text linked to a
|
||||
global or spot swatch follow its colour and name, as one undo step, in the art, opacity masks, symbols, graph
|
||||
designs, pattern tiles, graphic styles and the art of brushes); `swatch.delete {names, unlink?}` deletes
|
||||
designs, pattern tiles, graphic styles and the art of brushes). A replacement colour, gradient or pattern given as
|
||||
`paint` (`{color}`, `{gradient}`, `{swatch}` or `{pattern: name}`) is stored as `swatch.new` stores it;
|
||||
`swatch.delete {names, unlink?}` deletes
|
||||
swatches and colour groups in one undo step (art using a deleted global swatch keeps its colour, unlinked).
|
||||
`swatch.new {color?, mode?, spot?, global?, group?}` saves a colour (into a colour group with `group`), gradient
|
||||
or pattern (a gradient swatch holds no link to the swatch its gradient came from, and a pattern swatch holds the
|
||||
pattern untransformed); `swatch.newGroup {fromArtwork: true, toGlobal?, includeTints?}` makes a colour group of the selected
|
||||
or pattern (a gradient swatch keeps the gradient's aspect, with or without `start` and `end`, and holds no link to
|
||||
the swatch its gradient came from, and a pattern swatch holds the pattern untransformed); `swatch.newGroup {fromArtwork: true, toGlobal?, includeTints?}` makes a colour group of the selected
|
||||
art's colours (by default as global swatches the art links to).
|
||||
`swatch.move {names, to?, group?}` reorders swatches or moves them into or out of a colour group (as dragging
|
||||
them in the Swatches panel does); give colour group names instead to reorder the groups. Dropping a swatch on art
|
||||
|
||||
Reference in New Issue
Block a user