mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
Merge wp/A1
This commit is contained in:
@@ -257,7 +257,7 @@ fn convert_mode(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
n
|
||||
});
|
||||
for sw in &mut d.swatches {
|
||||
for sw in d.swatches_iter_mut() {
|
||||
match &mut sw.paint {
|
||||
Paint::Solid { color, .. } => *color = conv(color),
|
||||
Paint::Gradient(g) => {
|
||||
@@ -486,14 +486,14 @@ fn set_spot(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.setSpot";
|
||||
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing `name`"))?.to_string();
|
||||
let spot = bool_or(p, "spot", true);
|
||||
let sw = s.doc()?.doc.swatches.iter().find(|w| w.name == name).ok_or_else(|| bad(C, format!("no swatch `{name}`")))?;
|
||||
let sw = s.doc()?.doc.swatch(&name).ok_or_else(|| bad(C, format!("no swatch `{name}`")))?;
|
||||
if !matches!(sw.paint, Paint::Solid { .. }) {
|
||||
return Err(bad(C, "only solid-colour swatches can be spot colours"));
|
||||
}
|
||||
if sw.spot != spot {
|
||||
let n = name.clone();
|
||||
s.edit("Swatch Options", |d, _| {
|
||||
if let Some(w) = d.swatches.iter_mut().find(|w| w.name == n) {
|
||||
if let Some(w) = d.swatch_mut(&n) {
|
||||
w.spot = spot;
|
||||
if spot {
|
||||
w.global = true;
|
||||
|
||||
@@ -113,7 +113,7 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"images": images,
|
||||
"symbols": symbols,
|
||||
"spotColors": spot,
|
||||
"swatches": d.swatches.len(),
|
||||
"swatches": d.swatches_iter().count(),
|
||||
"graphicStyles": d.graphic_styles.len(),
|
||||
"characterStyles": d.char_styles.len(),
|
||||
"paragraphStyles": d.para_styles.len(),
|
||||
|
||||
@@ -469,7 +469,7 @@ fn color_mode(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for l in &mut d.layers {
|
||||
map_node_colors(Arc::make_mut(l), &f, true, true);
|
||||
}
|
||||
for sw in &mut d.swatches {
|
||||
for sw in d.swatches_iter_mut() {
|
||||
if let vectorcraft_color::Paint::Solid { color, .. } = &mut sw.paint {
|
||||
*color = f(*color);
|
||||
}
|
||||
|
||||
+330
-45
@@ -2,7 +2,9 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint, Swatch, SwatchGroup};
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_doc::pattern::pattern_paint;
|
||||
use vectorcraft_doc::swatches::{color_name, node_colors};
|
||||
use vectorcraft_doc::{Document, NodeId};
|
||||
|
||||
use super::paint::paint_from;
|
||||
use super::*;
|
||||
@@ -10,92 +12,222 @@ use crate::EngineError;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!("swatch.new", "New Swatch", ["Window", "Swatches"], None, "{name?, color?, global?} (default: current fill)", has_doc, swatch_new),
|
||||
cmd!("swatch.delete", "Delete Swatch", ["Window", "Swatches"], None, "{name}", has_doc, swatch_delete),
|
||||
cmd!(
|
||||
"swatch.new",
|
||||
"New Swatch",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name?, color? | swatch? | gradient? | pattern?: name (default: the current fill), mode?: \"gray\"|\"rgb\"|\"hsb\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (a spot colour, always global), group?: colour group name (solid colours only; default: ungrouped)} save a colour, gradient or pattern as a swatch. 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\") → {name}",
|
||||
has_doc,
|
||||
swatch_new
|
||||
),
|
||||
cmd!(
|
||||
"swatch.delete",
|
||||
"Delete Swatch",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name? | names?: [swatch or colour group names] (a group goes with its swatches), unlink?: true (art using a deleted global swatch keeps its colour, unlinked; false keeps the stale link)} delete in one undo step → {deleted, unlinked: paints unlinked}",
|
||||
has_doc,
|
||||
swatch_delete
|
||||
),
|
||||
cmd!(
|
||||
"swatch.newGroup",
|
||||
"New Color Group",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name?, swatches?: [names] (moved into the group), colors?: [colour] (added as new swatches)}",
|
||||
"{name?, swatches?: [names] (solid colours move into the group; gradients, patterns and None stay), colors?: [colour] (added as new swatches), fromArtwork?: false (add the unique colours of the selected art: fills, strokes, text, gradient stops and mesh points; a colour linked to a global swatch moves that swatch into the group), toGlobal?: true (with fromArtwork: the new swatches are global and the selected art's matching unlinked colours link to them), includeTints?: false (with fromArtwork: tints of global swatches also get swatches of their own)} → {name, swatches: [names in the group], linked: paints linked}",
|
||||
has_doc,
|
||||
swatch_new_group
|
||||
),
|
||||
cmd!("swatch.duplicate", "Duplicate Swatch", ["Window", "Swatches"], None, "{name}", has_doc, swatch_duplicate),
|
||||
cmd!("swatch.sortByName", "Sort by Name", ["Window", "Swatches"], None, "{}", has_doc, swatch_sort),
|
||||
cmd!(
|
||||
"swatch.edit",
|
||||
"Swatch Options",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name, newName?, color?, mode?: \"gray\"|\"rgb\"|\"hsb\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (spot colours are always global)} edit a swatch in any colour group, as one undo step. Fills, strokes and text linked to a global swatch take its new colour and name; turning Global off unlinks them (they keep their colour). Colour, mode and spot apply to solid colours only → {name, relinked: paints changed}",
|
||||
has_doc,
|
||||
swatch_edit
|
||||
),
|
||||
cmd!(
|
||||
query "swatch.list",
|
||||
"Swatches",
|
||||
[],
|
||||
None,
|
||||
"{group?: name (only that colour group's swatches)} → {swatches: [{name, kind: \"none\"|\"color\"|\"gradient\"|\"pattern\", group, global, spot, color?, hex?, gradient?, pattern?}], groups: [{name, swatches: [names]}]}",
|
||||
has_doc,
|
||||
swatch_list
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn swatch_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let paint = match paint_from(s, p)? {
|
||||
Some(x) => x,
|
||||
None => s.paint.fill.clone(),
|
||||
const C: &str = "swatch.new";
|
||||
let paint = match str_param(p, "pattern") {
|
||||
Some(name) if s.doc()?.doc.pattern(name).is_some() => pattern_paint(name),
|
||||
Some(name) => return Err(bad(C, format!("no pattern `{name}`"))),
|
||||
None => paint_from(s, p)?.unwrap_or_else(|| s.paint.fill.clone()),
|
||||
};
|
||||
let name = match str_param(p, "name") {
|
||||
Some(n) => n.to_string(),
|
||||
None => match &paint {
|
||||
Paint::Solid { color, .. } => rgb_name(*color),
|
||||
_ => format!("Swatch {}", s.doc()?.doc.swatches.len() + 1),
|
||||
},
|
||||
let spot = bool_or(p, "spot", false);
|
||||
let global = spot || bool_or(p, "global", false);
|
||||
let mode = str_param(p, "mode");
|
||||
let group = str_param(p, "group");
|
||||
let paint = match paint {
|
||||
// The swatch holds the colour itself, not a link to the swatch it may have come from.
|
||||
Paint::Solid { color, .. } => Paint::solid(match mode {
|
||||
Some(m) => convert_to_mode(color, m, C)?,
|
||||
None => color,
|
||||
}),
|
||||
Paint::None => return Err(bad(C, "a swatch needs a colour, gradient or pattern")),
|
||||
_ if spot || mode.is_some() || group.is_some() => return Err(bad(C, "spot, mode and group apply to solid colours only")),
|
||||
other => other,
|
||||
};
|
||||
let global = bool_or(p, "global", false);
|
||||
s.edit("New Swatch", |d, _| {
|
||||
d.swatches.push(Swatch { name: name.clone(), paint, global, spot: false });
|
||||
Ok(())
|
||||
let requested = name_param(p, "name");
|
||||
let name = s.edit("New Swatch", |d, _| {
|
||||
let name = match requested {
|
||||
Some(n) => d.free_swatch_name(&n),
|
||||
None => d.new_swatch_name(&paint),
|
||||
};
|
||||
let swatch = Swatch { name: name.clone(), paint, global, spot };
|
||||
match group {
|
||||
Some(g) => {
|
||||
d.swatch_groups.iter_mut().find(|x| x.name == g).ok_or_else(|| bad(C, format!("no colour group `{g}`")))?.swatches.push(swatch)
|
||||
}
|
||||
None => d.swatches.push(swatch),
|
||||
}
|
||||
Ok(name)
|
||||
})?;
|
||||
Ok(json!({ "name": name }))
|
||||
}
|
||||
|
||||
fn swatch_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("swatch.delete", "missing name"))?.to_string();
|
||||
s.edit("Delete Swatch", |d, _| {
|
||||
d.swatches.retain(|sw| sw.name != name);
|
||||
for g in &mut d.swatch_groups {
|
||||
g.swatches.retain(|sw| sw.name != name);
|
||||
const C: &str = "swatch.delete";
|
||||
let mut names: Vec<String> = vec![];
|
||||
for n in str_list(p, "names").into_iter().chain(str_param(p, "name").map(str::to_string)) {
|
||||
if !names.contains(&n) {
|
||||
names.push(n);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
if names.is_empty() {
|
||||
return Err(bad(C, "give `name` or `names`"));
|
||||
}
|
||||
let d = &s.doc()?.doc;
|
||||
// Global swatches going away (named ones and those in named groups): their links are dropped.
|
||||
let mut global: Vec<String> = vec![];
|
||||
for n in &names {
|
||||
let swatches: Vec<&Swatch> = match d.swatch_groups.iter().find(|g| g.name == *n) {
|
||||
Some(g) => g.swatches.iter().collect(),
|
||||
None => vec![d.swatch(n).ok_or_else(|| bad(C, format!("no swatch or colour group `{n}`")))?],
|
||||
};
|
||||
if swatches.iter().any(|w| w.paint.is_none()) {
|
||||
return Err(bad(C, format!("`{n}` can't be deleted")));
|
||||
}
|
||||
global.extend(swatches.iter().filter(|w| w.global).map(|w| w.name.clone()));
|
||||
}
|
||||
if !bool_or(p, "unlink", true) {
|
||||
global.clear();
|
||||
}
|
||||
let mut unlink = |_: &mut Color, link: &mut Option<String>| link.as_ref().is_some_and(|l| global.contains(l)) && link.take().is_some();
|
||||
let unlinked = s.edit("Delete Swatch", |d, _| {
|
||||
d.swatch_groups.retain(|g| !names.contains(&g.name));
|
||||
for n in &names {
|
||||
d.remove_swatch(n);
|
||||
}
|
||||
Ok(d.map_solid_paints(&mut unlink))
|
||||
})?;
|
||||
ok()
|
||||
map_default_paints(s, &mut unlink);
|
||||
Ok(json!({"deleted": names, "unlinked": unlinked}))
|
||||
}
|
||||
|
||||
/// The default name of a new solid swatch: its 8-bit RGB values.
|
||||
fn rgb_name(c: Color) -> String {
|
||||
let [r, g, b] = c.to_rgb();
|
||||
format!("R={} G={} B={}", (r * 255.0).round(), (g * 255.0).round(), (b * 255.0).round())
|
||||
}
|
||||
|
||||
/// A swatch or colour-group name not used yet in `d`.
|
||||
fn free_name(d: &Document, base: &str) -> String {
|
||||
unique_name(base, |n| d.swatch(n).is_some() || d.swatch_groups.iter().any(|g| g.name == n))
|
||||
/// The colours of the art `ids` for a new colour group, in paint order: the swatches that linked
|
||||
/// colours belong to (added to `swatches` once each) and the other colours (added to `colors` once
|
||||
/// each). A tint (a linked colour that differs from its swatch's) brings its swatch, and with
|
||||
/// `tints` its own colour too. Links to missing swatches count as unlinked.
|
||||
fn artwork_colors(d: &Document, ids: &[NodeId], tints: bool, swatches: &mut Vec<String>, colors: &mut Vec<Color>) {
|
||||
for n in ids.iter().filter_map(|id| d.node(*id)) {
|
||||
node_colors(n, &mut |c, link| {
|
||||
let sw = link.and_then(|l| d.swatch(l)).filter(|w| w.paint.color().is_some());
|
||||
if let Some(w) = sw
|
||||
&& !swatches.contains(&w.name)
|
||||
{
|
||||
swatches.push(w.name.clone());
|
||||
}
|
||||
let own = sw.is_none_or(|w| tints && w.paint.color() != Some(*c));
|
||||
if own && !colors.contains(c) {
|
||||
colors.push(*c);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn swatch_new_group(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let names: Vec<String> =
|
||||
p.get("swatches").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default();
|
||||
const C: &str = "swatch.newGroup";
|
||||
let mut names = str_list(p, "swatches");
|
||||
let colors: Vec<Color> = p.get("colors").and_then(Value::as_array).map(|a| a.iter().filter_map(color_value).collect()).unwrap_or_default();
|
||||
let requested = str_param(p, "name").map(str::to_string);
|
||||
let name = s.edit("New Color Group", |d, _| {
|
||||
let name = free_name(d, requested.as_deref().unwrap_or("Color Group"));
|
||||
let from_art = bool_or(p, "fromArtwork", false);
|
||||
let to_global = from_art && bool_or(p, "toGlobal", true);
|
||||
let tints = bool_or(p, "includeTints", false);
|
||||
let requested = name_param(p, "name");
|
||||
let (name, members, linked) = s.edit("New Color Group", |d, sel| {
|
||||
// New swatches: (colour, global).
|
||||
let mut new: Vec<(Color, bool)> = colors.iter().map(|c| (*c, false)).collect();
|
||||
if from_art {
|
||||
if sel.objects.is_empty() {
|
||||
return Err(bad(C, "select artwork to make a group of its colours"));
|
||||
}
|
||||
let mut art = vec![];
|
||||
artwork_colors(d, &sel.objects, tints, &mut names, &mut art);
|
||||
new.extend(art.into_iter().map(|c| (c, to_global)));
|
||||
}
|
||||
let name = d.free_swatch_name(requested.as_deref().unwrap_or("Color Group"));
|
||||
let mut group = SwatchGroup { name: name.clone(), swatches: vec![] };
|
||||
// Colour groups hold solid colours only.
|
||||
for n in &names {
|
||||
if let Some(pos) = d.swatches.iter().position(|sw| &sw.name == n) {
|
||||
group.swatches.push(d.swatches.remove(pos));
|
||||
if d.swatch(n).is_some_and(|sw| matches!(sw.paint, Paint::Solid { .. }))
|
||||
&& let Some(sw) = d.remove_swatch(n)
|
||||
{
|
||||
group.swatches.push(sw);
|
||||
}
|
||||
}
|
||||
for c in &colors {
|
||||
group.swatches.push(Swatch { name: rgb_name(*c), paint: Paint::solid(*c), global: false, spot: false });
|
||||
// The global swatches made from the art's colours, which its unlinked paints link to.
|
||||
let mut links: Vec<(Color, String)> = vec![];
|
||||
for (c, global) in new {
|
||||
let nm = unique_name(&color_name(c), |n| n == name || d.swatch_name_taken(n) || group.swatches.iter().any(|sw| sw.name == n));
|
||||
if global {
|
||||
links.push((c, nm.clone()));
|
||||
}
|
||||
group.swatches.push(Swatch { name: nm, paint: Paint::solid(c), global, spot: false });
|
||||
}
|
||||
let members: Vec<String> = group.swatches.iter().map(|w| w.name.clone()).collect();
|
||||
d.swatch_groups.push(group);
|
||||
Ok(name)
|
||||
let linked = if links.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let live: Vec<String> = d.swatches_iter().map(|w| w.name.clone()).collect();
|
||||
d.map_solid_paints_in(&sel.objects, &mut |c, link| {
|
||||
if link.as_ref().is_some_and(|l| live.contains(l)) {
|
||||
return false;
|
||||
}
|
||||
match links.iter().find(|(lc, _)| lc == c) {
|
||||
Some((_, n)) => {
|
||||
*link = Some(n.clone());
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
};
|
||||
Ok((name, members, linked))
|
||||
})?;
|
||||
Ok(json!({"name": name}))
|
||||
Ok(json!({"name": name, "swatches": members, "linked": linked}))
|
||||
}
|
||||
|
||||
fn swatch_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("swatch.duplicate", "missing name"))?.to_string();
|
||||
let new = s.edit("Duplicate Swatch", |d, _| {
|
||||
let src = d.swatch(&name).cloned().ok_or_else(|| EngineError::Other(format!("no swatch `{name}`")))?;
|
||||
let nm = free_name(d, &format!("{name} copy"));
|
||||
let nm = d.free_swatch_name(&format!("{name} copy"));
|
||||
let copy = Swatch { name: nm.clone(), ..src };
|
||||
if let Some(pos) = d.swatches.iter().position(|sw| sw.name == name) {
|
||||
d.swatches.insert(pos + 1, copy);
|
||||
@@ -108,6 +240,159 @@ fn swatch_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"name": new}))
|
||||
}
|
||||
|
||||
/// A list-of-strings parameter (missing or non-string entries are skipped).
|
||||
fn str_list(p: &Value, key: &str) -> Vec<String> {
|
||||
p.get(key).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A trimmed, non-empty name parameter.
|
||||
fn name_param(p: &Value, key: &str) -> Option<String> {
|
||||
str_param(p, key).map(str::trim).filter(|n| !n.is_empty()).map(str::to_string)
|
||||
}
|
||||
|
||||
/// `c` converted to a Swatch Options colour mode through the colour settings (HSB is an RGB view;
|
||||
/// Web snaps each channel to a multiple of 0x33).
|
||||
fn convert_to_mode(c: Color, mode: &str, cmd: &str) -> Result<Color> {
|
||||
use vectorcraft_color::cms::Model;
|
||||
let cms = vectorcraft_color::cms::active();
|
||||
let to = |m: Model| cms.convert(&c, m, cms.settings().intent);
|
||||
Ok(match mode.to_ascii_lowercase().as_str() {
|
||||
"gray" | "grayscale" => to(Model::Gray),
|
||||
"rgb" | "hsb" => to(Model::Rgb),
|
||||
"cmyk" => to(Model::Cmyk),
|
||||
"web" => {
|
||||
let snap = |v: f32| (v.clamp(0.0, 1.0) * 5.0).round() / 5.0;
|
||||
let [r, g, b] = to(Model::Rgb).to_rgb_uncalibrated();
|
||||
Color::rgb(snap(r), snap(g), snap(b))
|
||||
}
|
||||
other => return Err(bad(cmd, format!("unknown mode `{other}` (gray, rgb, hsb, cmyk or web)"))),
|
||||
})
|
||||
}
|
||||
|
||||
/// How a swatch edit reaches the solid paints linked to swatch `from`.
|
||||
struct Relink {
|
||||
from: String,
|
||||
/// The swatch's name after the edit.
|
||||
to: String,
|
||||
/// The new colour of a global swatch (linked paints take it).
|
||||
color: Option<Color>,
|
||||
/// false: drop the links (the swatch stopped being global); paints keep their colour.
|
||||
keep: bool,
|
||||
}
|
||||
|
||||
impl Relink {
|
||||
/// Apply to one paint; true when it changed.
|
||||
fn apply(&self, c: &mut Color, link: &mut Option<String>) -> bool {
|
||||
if link.as_deref() != Some(self.from.as_str()) {
|
||||
return false;
|
||||
}
|
||||
if !self.keep {
|
||||
*link = None;
|
||||
return true;
|
||||
}
|
||||
let renamed = self.to != self.from;
|
||||
if renamed {
|
||||
*link = Some(self.to.clone());
|
||||
}
|
||||
let recolored = self.color.is_some_and(|n| n != *c);
|
||||
if let Some(n) = self.color {
|
||||
*c = n;
|
||||
}
|
||||
renamed || recolored
|
||||
}
|
||||
|
||||
fn defaults(&self, s: &mut Session) {
|
||||
map_default_paints(s, &mut |c, l| self.apply(c, l));
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a swatch-link rewrite to the default fill and stroke for new art, except during a live
|
||||
/// preview (Cancel rolls the document back, not the session).
|
||||
fn map_default_paints(s: &mut Session, f: &mut dyn FnMut(&mut Color, &mut Option<String>) -> bool) {
|
||||
if s.in_interaction() {
|
||||
return;
|
||||
}
|
||||
for p in [&mut s.paint.fill, &mut s.paint.stroke] {
|
||||
if let Paint::Solid { color, swatch } = p {
|
||||
f(color, swatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.edit";
|
||||
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing `name`"))?.to_string();
|
||||
let d = &s.doc()?.doc;
|
||||
let sw = d.swatch(&name).cloned().ok_or_else(|| bad(C, format!("no swatch `{name}`")))?;
|
||||
if sw.paint.is_none() {
|
||||
return Err(bad(C, format!("`{name}` can't be edited")));
|
||||
}
|
||||
let given = |k: &str| p.get(k).is_some_and(|v| !v.is_null());
|
||||
let solid = sw.paint.color();
|
||||
if solid.is_none() && (given("color") || given("mode") || bool_or(p, "spot", false)) {
|
||||
return Err(bad(C, "colour, mode and spot apply to solid-colour swatches only"));
|
||||
}
|
||||
let mut color = match p.get("color").filter(|v| !v.is_null()) {
|
||||
Some(v) => Some(color_value(v).ok_or_else(|| bad(C, format!("bad color {v}")))?),
|
||||
None => solid,
|
||||
};
|
||||
if let (Some(c), Some(m)) = (color, str_param(p, "mode")) {
|
||||
color = Some(convert_to_mode(c, m, C)?);
|
||||
}
|
||||
let spot = bool_or(p, "spot", sw.spot);
|
||||
let global = spot || bool_or(p, "global", sw.global);
|
||||
let to = name_param(p, "newName").filter(|n| *n != name).map_or_else(|| name.clone(), |n| d.free_swatch_name(&n));
|
||||
let relink = Relink { from: name.clone(), to: to.clone(), color: color.filter(|_| global), keep: global };
|
||||
let relinked = s.edit("Swatch Options", |d, _| {
|
||||
let w = d.swatch_mut(&name).ok_or_else(|| bad(C, format!("no swatch `{name}`")))?;
|
||||
w.name = to.clone();
|
||||
w.spot = spot;
|
||||
w.global = global;
|
||||
if let (Some(c), Paint::Solid { color: wc, .. }) = (color, &mut w.paint) {
|
||||
*wc = c;
|
||||
}
|
||||
Ok(d.map_solid_paints(&mut |c, l| relink.apply(c, l)))
|
||||
})?;
|
||||
relink.defaults(s);
|
||||
Ok(json!({"name": to, "relinked": relinked}))
|
||||
}
|
||||
|
||||
fn swatch_json(sw: &Swatch, group: Option<&str>) -> Value {
|
||||
let mut v = json!({"name": sw.name, "group": group, "global": sw.global, "spot": sw.spot});
|
||||
match &sw.paint {
|
||||
Paint::None => v["kind"] = json!("none"),
|
||||
Paint::Solid { color, .. } => {
|
||||
v["kind"] = json!("color");
|
||||
v["color"] = json!(color);
|
||||
v["hex"] = json!(color.to_hex());
|
||||
}
|
||||
Paint::Gradient(g) => {
|
||||
v["kind"] = json!("gradient");
|
||||
v["gradient"] = json!(g.gradient.kind.label().to_lowercase());
|
||||
}
|
||||
Paint::Pattern { pattern, .. } => {
|
||||
v["kind"] = json!("pattern");
|
||||
v["pattern"] = json!(pattern);
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn swatch_list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let only = str_param(p, "group");
|
||||
let groups: Vec<&SwatchGroup> = match only {
|
||||
Some(g) => vec![d.swatch_groups.iter().find(|x| x.name == g).ok_or_else(|| bad("swatch.list", format!("no colour group `{g}`")))?],
|
||||
None => d.swatch_groups.iter().collect(),
|
||||
};
|
||||
let ungrouped = d.swatches.iter().filter(|_| only.is_none()).map(|w| swatch_json(w, None));
|
||||
let grouped = groups.iter().flat_map(|g| g.swatches.iter().map(|w| swatch_json(w, Some(&g.name))));
|
||||
let swatches: Vec<Value> = ungrouped.chain(grouped).collect();
|
||||
let groups: Vec<Value> =
|
||||
groups.iter().map(|g| json!({"name": g.name, "swatches": g.swatches.iter().map(|w| &w.name).collect::<Vec<_>>()})).collect();
|
||||
Ok(json!({"swatches": swatches, "groups": groups}))
|
||||
}
|
||||
|
||||
fn swatch_sort(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
s.edit("Sort Swatches", |d, _| {
|
||||
// [None] and other bracketed specials stay first, like the reference app.
|
||||
|
||||
@@ -733,6 +733,8 @@ mod tests_pattern;
|
||||
#[cfg(test)]
|
||||
mod tests_prefs;
|
||||
#[cfg(test)]
|
||||
mod tests_swatches;
|
||||
#[cfg(test)]
|
||||
mod tests_textedit;
|
||||
#[cfg(test)]
|
||||
mod tests_xform;
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
//! Swatches: colour groups as first-class swatch homes (colour mode, spot plates, PDF, Document
|
||||
//! Info), naming and kinds, Swatch Options edits reaching linked art, deleting, new swatches in
|
||||
//! groups and colour groups made from artwork.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
|
||||
}
|
||||
|
||||
fn rect(s: &mut Session) -> NodeId {
|
||||
NodeId(run(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn doc(s: &Session) -> &vectorcraft_doc::Document {
|
||||
&s.doc().unwrap().doc
|
||||
}
|
||||
|
||||
/// Add a global solid swatch to colour group `group`.
|
||||
fn grouped_swatch(s: &mut Session, group: &str, name: &str, color: Color) {
|
||||
assert_eq!(run(s, "swatch.new", json!({"name": name, "color": color, "global": true, "group": group}))["name"], name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_color_mode_converts_grouped_swatches() {
|
||||
let mut s = session();
|
||||
grouped_swatch(&mut s, "Grays", "Ink", Color::rgb(0.2, 0.4, 0.6));
|
||||
run(&mut s, "object.convertDocumentColorMode", json!({"mode": "cmyk"}));
|
||||
for g in &doc(&s).swatch_groups {
|
||||
for sw in &g.swatches {
|
||||
assert!(!matches!(sw.paint.color(), Some(Color::Rgb { .. })), "{} in {} is still RGB", sw.name, g.name);
|
||||
}
|
||||
}
|
||||
assert!(matches!(doc(&s).swatch("Ink").unwrap().paint.color(), Some(Color::Cmyk { .. })));
|
||||
// The File menu's mode switch converts them as well.
|
||||
let mut s = session();
|
||||
run(&mut s, "file.documentColorMode", json!({"mode": "cmyk"}));
|
||||
assert!(matches!(doc(&s).swatch("Bright Red").unwrap().paint.color(), Some(Color::Cmyk { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouped_spot_swatches_print_on_their_own_plate() {
|
||||
let mut s = session();
|
||||
grouped_swatch(&mut s, "Brights", "Signal Orange", Color::cmyk(0.0, 0.6, 1.0, 0.0));
|
||||
assert_eq!(run(&mut s, "swatch.setSpot", json!({"name": "Signal Orange"})), json!({"name": "Signal Orange", "spot": true}));
|
||||
assert!(doc(&s).swatch("Signal Orange").is_some_and(|w| w.spot && w.global));
|
||||
let plates = run(&mut s, "color.plates", json!({}));
|
||||
assert!(plates["plates"].as_array().unwrap().iter().any(|p| p["name"] == "Signal Orange" && p["spot"] == true), "{plates}");
|
||||
let a = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"ids": [a.0], "swatch": "Signal Orange"}));
|
||||
let bytes = vectorcraft_pdf::export(doc(&s), &vectorcraft_pdf::PdfOptions { compress: false, created: Some(0), ..Default::default() }).unwrap();
|
||||
let pdf = String::from_utf8_lossy(&bytes);
|
||||
assert!(pdf.contains("/Separation") && pdf.contains("Signal#20Orange"), "a grouped spot swatch is a Separation");
|
||||
let info = run(&mut s, "document.info", json!({}));
|
||||
assert_eq!(info["spotColors"], json!(["Signal Orange"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_info_counts_grouped_swatches() {
|
||||
let mut s = session();
|
||||
let d = doc(&s);
|
||||
let all = d.swatches.len() + d.swatch_groups.iter().map(|g| g.swatches.len()).sum::<usize>();
|
||||
assert!(all > d.swatches.len());
|
||||
assert_eq!(run(&mut s, "document.info", json!({}))["swatches"], json!(all));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_swatches_are_named_by_their_colour_model() {
|
||||
let mut s = session();
|
||||
let name = |s: &mut Session, p: Value| run(s, "swatch.new", p)["name"].as_str().unwrap().to_string();
|
||||
assert_eq!(name(&mut s, json!({"color": {"c": 10, "m": 20, "y": 30, "k": 0}})), "C=10 M=20 Y=30 K=0");
|
||||
assert_eq!(name(&mut s, json!({"color": "#ff8000"})), "R=255 G=128 B=0");
|
||||
assert_eq!(name(&mut s, json!({"color": {"gray": 40}})), "Gray K=40");
|
||||
// The same colour again (or an explicit name in use) gets the next free number, document-wide.
|
||||
assert_eq!(name(&mut s, json!({"color": {"c": 10, "m": 20, "y": 30, "k": 0}})), "C=10 M=20 Y=30 K=0 2");
|
||||
assert_eq!(name(&mut s, json!({"name": "Grays", "color": "#000000"})), "Grays 2");
|
||||
assert_eq!(name(&mut s, json!({"name": "Bright Red", "color": "#000000"})), "Bright Red 2");
|
||||
// A spot swatch is always global; saving a linked colour stores the colour, not the link.
|
||||
let spot = name(&mut s, json!({"name": "Ink", "color": "#336699", "spot": true}));
|
||||
assert!(doc(&s).swatch(&spot).is_some_and(|w| w.spot && w.global));
|
||||
let copy = name(&mut s, json!({"swatch": "Ink"}));
|
||||
assert_eq!(doc(&s).swatch(©).unwrap().paint, Paint::solid(Color::from_hex("#336699").unwrap()));
|
||||
assert!(s.execute("swatch.new", &json!({"none": true})).is_err(), "None is not a swatch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_swatch_saves_the_active_pattern_or_gradient() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s);
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
run(&mut s, "object.pattern.make", json!({}));
|
||||
run(&mut s, "object.pattern.done", json!({}));
|
||||
let pattern = doc(&s).patterns[0].name.clone();
|
||||
let name = run(&mut s, "swatch.new", json!({"pattern": pattern}))["name"].as_str().unwrap().to_string();
|
||||
assert_eq!(name, "New Pattern Swatch 1");
|
||||
assert!(matches!(&doc(&s).swatch(&name).unwrap().paint, Paint::Pattern { pattern: p, .. } if *p == pattern));
|
||||
assert!(s.execute("swatch.new", &json!({"pattern": "Nope"})).is_err());
|
||||
let g = json!({"gradient": {"kind": "radial", "stops": [{"offset": 0, "color": "#ffffff"}, {"offset": 1, "color": "#000000"}]}});
|
||||
let name = run(&mut s, "swatch.new", g)["name"].as_str().unwrap().to_string();
|
||||
assert_eq!(name, "New Gradient Swatch 1");
|
||||
assert!(matches!(doc(&s).swatch(&name).unwrap().paint, Paint::Gradient(_)));
|
||||
assert!(s.execute("swatch.new", &json!({"pattern": pattern, "spot": true})).is_err(), "only colours are spot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colour_groups_hold_solid_colours_only() {
|
||||
let mut s = session();
|
||||
let g = run(&mut s, "swatch.newGroup", json!({"name": "Mix", "swatches": ["Sunset", "Red", "[None]", "Bright Blue"], "colors": ["#00ff00"]}));
|
||||
let d = doc(&s);
|
||||
let group = d.swatch_groups.iter().find(|x| x.name == g["name"]).unwrap();
|
||||
let names: Vec<&str> = group.swatches.iter().map(|w| w.name.as_str()).collect();
|
||||
assert_eq!(names, ["Red", "Bright Blue", "R=0 G=255 B=0"]);
|
||||
assert!(d.swatches.iter().any(|w| w.name == "Sunset") && d.swatches.iter().any(|w| w.name == "[None]"), "gradients and None stay put");
|
||||
assert!(d.swatch_groups.iter().find(|x| x.name == "Brights").unwrap().swatches.iter().all(|w| w.name != "Bright Blue"), "moved, not copied");
|
||||
}
|
||||
|
||||
fn fill_of(s: &Session, id: NodeId) -> Paint {
|
||||
doc(s).node(id).unwrap().appearance.fill_paint()
|
||||
}
|
||||
|
||||
fn linked(hex: &str, name: &str) -> Paint {
|
||||
Paint::Solid { color: Color::from_hex(hex).unwrap(), swatch: Some(name.into()) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_a_global_swatch_recolours_linked_art_in_one_step() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Brand", "color": "#2a6fb0", "global": true}));
|
||||
let (a, b) = (rect(&mut s), rect(&mut s));
|
||||
run(&mut s, "paint.setFill", json!({"ids": [a.0], "swatch": "Brand"}));
|
||||
run(&mut s, "paint.setFill", json!({"ids": [b.0], "color": "#2a6fb0"}));
|
||||
let t = NodeId(run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Hi"}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "paint.setFill", json!({"ids": [t.0], "swatch": "Brand"}));
|
||||
run(&mut s, "select.set", json!({"ids": []}));
|
||||
run(&mut s, "paint.setStroke", json!({"swatch": "Brand"}));
|
||||
let undo_depth = s.doc().unwrap().history.undo.len();
|
||||
let r = run(&mut s, "swatch.edit", json!({"name": "Brand", "color": "#ff0000"}));
|
||||
assert_eq!(r, json!({"name": "Brand", "relinked": 2}));
|
||||
assert_eq!(fill_of(&s, a), linked("#ff0000", "Brand"));
|
||||
assert_eq!(fill_of(&s, b), Paint::solid(Color::from_hex("#2a6fb0").unwrap()), "unlinked art keeps its colour");
|
||||
let text_fill = |s: &Session| match &doc(s).node(t).unwrap().kind {
|
||||
vectorcraft_doc::NodeKind::Text(tx) => tx.runs[0].style.fill.clone(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(text_fill(&s), linked("#ff0000", "Brand"), "text runs follow the swatch");
|
||||
assert_eq!(s.paint.stroke, linked("#ff0000", "Brand"), "so does the default stroke");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo_depth + 1, "one undo step");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(fill_of(&s, a), linked("#2a6fb0", "Brand"));
|
||||
assert_eq!(doc(&s).swatch("Brand").unwrap().paint.color(), Color::from_hex("#2a6fb0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renaming_relinks_and_spot_forces_global() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Brand", "color": "#2a6fb0", "global": true}));
|
||||
let a = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"ids": [a.0], "swatch": "Brand"}));
|
||||
// A name in use gets the next free number.
|
||||
let r = run(&mut s, "swatch.edit", json!({"name": "Brand", "newName": "Red"}));
|
||||
assert_eq!(r, json!({"name": "Red 2", "relinked": 1}));
|
||||
assert_eq!(fill_of(&s, a), linked("#2a6fb0", "Red 2"));
|
||||
assert!(doc(&s).swatch("Brand").is_none());
|
||||
run(&mut s, "swatch.edit", json!({"name": "Red 2", "spot": true, "global": false}));
|
||||
assert!(doc(&s).swatch("Red 2").is_some_and(|w| w.spot && w.global), "spot colours stay global");
|
||||
// Turning Global off (no longer spot) unlinks the art, which keeps its colour.
|
||||
let r = run(&mut s, "swatch.edit", json!({"name": "Red 2", "spot": false, "global": false, "color": "#000000"}));
|
||||
assert_eq!(r["relinked"], 1);
|
||||
assert_eq!(fill_of(&s, a), Paint::solid(Color::from_hex("#2a6fb0").unwrap()));
|
||||
assert!(doc(&s).swatch("Red 2").is_some_and(|w| !w.global && w.paint.color() == Some(Color::BLACK)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouped_swatches_edit_and_convert_modes() {
|
||||
let mut s = session();
|
||||
let r = run(&mut s, "swatch.edit", json!({"name": "Bright Red", "mode": "cmyk", "global": true}));
|
||||
assert_eq!(r["name"], "Bright Red");
|
||||
let w = doc(&s).swatch("Bright Red").unwrap();
|
||||
assert!(w.global && matches!(w.paint.color(), Some(Color::Cmyk { .. })));
|
||||
assert_eq!(doc(&s).swatch_group_of("Bright Red").map(|g| doc(&s).swatch_groups[g].name.clone()), Some("Brights".into()));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Bright Red", "color": "#123456", "mode": "web"}));
|
||||
assert_eq!(doc(&s).swatch("Bright Red").unwrap().paint.color().unwrap().to_hex(), "#003366");
|
||||
run(&mut s, "swatch.edit", json!({"name": "Bright Red", "mode": "gray"}));
|
||||
assert!(matches!(doc(&s).swatch("Bright Red").unwrap().paint.color(), Some(Color::Gray { .. })));
|
||||
// Gradients can be renamed but have no colour, mode or spot; None can't be edited.
|
||||
assert!(s.execute("swatch.edit", &json!({"name": "Sunset", "color": "#000000"})).is_err());
|
||||
assert!(s.execute("swatch.edit", &json!({"name": "Sunset", "spot": true})).is_err());
|
||||
assert!(s.execute("swatch.edit", &json!({"name": "[None]", "newName": "x"})).is_err());
|
||||
assert!(s.execute("swatch.edit", &json!({"name": "Bright Red", "mode": "lab"})).is_err());
|
||||
assert_eq!(run(&mut s, "swatch.edit", json!({"name": "Sunset", "newName": "Dusk"}))["name"], "Dusk");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swatch_list_reports_kinds_and_groups() {
|
||||
let mut s = session();
|
||||
let all = run(&mut s, "swatch.list", json!({}));
|
||||
let list = all["swatches"].as_array().unwrap();
|
||||
assert_eq!(list.len(), doc(&s).swatches_iter().count());
|
||||
let find = |n: &str| list.iter().find(|w| w["name"] == n).unwrap().clone();
|
||||
assert_eq!(find("[None]")["kind"], "none");
|
||||
assert_eq!(find("Red")["kind"], "color");
|
||||
assert_eq!(find("Red")["hex"], "#ed1c24");
|
||||
assert_eq!(find("Sunset")["kind"], "gradient");
|
||||
assert_eq!(find("K=50")["group"], "Grays");
|
||||
let grays = run(&mut s, "swatch.list", json!({"group": "Grays"}));
|
||||
assert_eq!(grays["swatches"].as_array().unwrap().len(), 9);
|
||||
assert_eq!(grays["groups"][0]["name"], "Grays");
|
||||
assert!(s.execute("swatch.list", &json!({"group": "Nope"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_a_global_swatch_unlinks_art_that_keeps_its_colour() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Brand", "color": "#2a6fb0", "global": true}));
|
||||
let a = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"ids": [a.0], "swatch": "Brand"}));
|
||||
run(&mut s, "select.set", json!({"ids": []}));
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Brand"}));
|
||||
let before = doc(&s).clone();
|
||||
assert_eq!(run(&mut s, "swatch.delete", json!({"name": "Brand"})), json!({"deleted": ["Brand"], "unlinked": 1}));
|
||||
assert!(doc(&s).swatch("Brand").is_none());
|
||||
assert_eq!(fill_of(&s, a), Paint::solid(Color::from_hex("#2a6fb0").unwrap()));
|
||||
assert_eq!(s.paint.fill, Paint::solid(Color::from_hex("#2a6fb0").unwrap()), "the default fill is unlinked too");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(*doc(&s), before, "one undo step restores swatch and link");
|
||||
// unlink: false keeps the stale link.
|
||||
assert_eq!(run(&mut s, "swatch.delete", json!({"name": "Brand", "unlink": false}))["unlinked"], 0);
|
||||
assert_eq!(fill_of(&s, a), linked("#2a6fb0", "Brand"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_several_swatches_and_groups() {
|
||||
let mut s = session();
|
||||
let r = run(&mut s, "swatch.delete", json!({"names": ["Red", "Brights", "Sunset"]}));
|
||||
assert_eq!(r["deleted"], json!(["Red", "Brights", "Sunset"]));
|
||||
let d = doc(&s);
|
||||
assert!(d.swatch("Red").is_none() && d.swatch("Sunset").is_none() && d.swatch("Bright Red").is_none());
|
||||
assert!(d.swatch_groups.iter().all(|g| g.name != "Brights") && d.swatch_groups.iter().any(|g| g.name == "Grays"));
|
||||
assert!(s.execute("swatch.delete", &json!({"name": "[None]"})).is_err(), "None stays");
|
||||
assert!(s.execute("swatch.delete", &json!({"names": ["Orange", "Nope"]})).is_err(), "unknown names fail the whole call");
|
||||
assert!(doc(&s).swatch("Orange").is_some());
|
||||
assert!(s.execute("swatch.delete", &json!({})).is_err());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(doc(&s).swatch("Bright Red").is_some() && doc(&s).swatch("Red").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_swatches_go_into_groups_convert_modes_and_can_be_spot() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Ink", "color": "#ff0000", "spot": true, "mode": "cmyk", "group": "Brights"}));
|
||||
let d = doc(&s);
|
||||
let ink = d.swatch("Ink").unwrap();
|
||||
assert!(ink.spot && ink.global, "spot colours are global");
|
||||
assert!(matches!(ink.paint.color(), Some(Color::Cmyk { .. })), "mode converts the colour");
|
||||
assert_eq!(d.swatch_group_of("Ink").map(|g| d.swatch_groups[g].name.as_str()), Some("Brights"));
|
||||
assert!(s.execute("swatch.new", &json!({"color": "#00ff00", "group": "Nope"})).is_err());
|
||||
assert!(s.execute("swatch.new", &json!({"swatch": "Sunset", "group": "Brights"})).is_err(), "groups hold solid colours only");
|
||||
assert!(s.execute("swatch.new", &json!({"color": "#00ff00", "mode": "lab"})).is_err());
|
||||
}
|
||||
|
||||
/// Two rectangles: `a` filled red and stroked green, `b` filled red and stroked with global swatch
|
||||
/// "Brand"; both selected.
|
||||
fn art(s: &mut Session) -> (NodeId, NodeId) {
|
||||
run(s, "swatch.new", json!({"name": "Brand", "color": "#2a6fb0", "global": true}));
|
||||
let (a, b) = (rect(s), rect(s));
|
||||
run(s, "paint.setFill", json!({"ids": [a.0, b.0], "color": "#ff0000"}));
|
||||
run(s, "paint.setStroke", json!({"ids": [a.0], "color": "#00ff00"}));
|
||||
run(s, "paint.setStroke", json!({"ids": [b.0], "swatch": "Brand"}));
|
||||
run(s, "select.set", json!({"ids": [a.0, b.0]}));
|
||||
(a, b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_from_artwork_holds_its_unique_colours_and_links_the_art() {
|
||||
let mut s = session();
|
||||
let (a, b) = art(&mut s);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let r = run(&mut s, "swatch.newGroup", json!({"name": "Art", "fromArtwork": true}));
|
||||
assert_eq!(r["swatches"], json!(["Brand", "R=255 G=0 B=0", "R=0 G=255 B=0"]), "the linked swatch moves in, the rest are new");
|
||||
assert_eq!(r["linked"], 3, "both red fills and the green stroke");
|
||||
let d = doc(&s);
|
||||
assert!(d.swatch("R=255 G=0 B=0").unwrap().global);
|
||||
assert_eq!(d.swatch_group_of("Brand").map(|g| d.swatch_groups[g].name.as_str()), Some("Art"));
|
||||
assert_eq!(fill_of(&s, a), linked("#ff0000", "R=255 G=0 B=0"));
|
||||
assert_eq!(fill_of(&s, b), linked("#ff0000", "R=255 G=0 B=0"));
|
||||
assert_eq!(doc(&s).node(b).unwrap().appearance.stroke_paint(), linked("#2a6fb0", "Brand"));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
// Without Convert to Global the swatches are process colours and the art stays unlinked.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let r = run(&mut s, "swatch.newGroup", json!({"fromArtwork": true, "toGlobal": false}));
|
||||
assert_eq!((r["name"].as_str(), r["linked"].as_u64()), (Some("Color Group"), Some(0)));
|
||||
assert!(!doc(&s).swatch("R=0 G=255 B=0").unwrap().global);
|
||||
assert_eq!(fill_of(&s, a), Paint::solid(Color::from_hex("#ff0000").unwrap()));
|
||||
run(&mut s, "select.set", json!({"ids": []}));
|
||||
assert!(s.execute("swatch.newGroup", &json!({"fromArtwork": true})).is_err(), "needs selected art");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tints_of_global_swatches_get_swatches_only_when_asked() {
|
||||
let mut s = session();
|
||||
let (_, b) = art(&mut s);
|
||||
// A 50% tint of Brand on b's stroke.
|
||||
let tint = Color::from_hex("#95b7d8").unwrap();
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).map_solid_paints_in(&[b], &mut |c, l| {
|
||||
let hit = l.as_deref() == Some("Brand");
|
||||
if hit {
|
||||
*c = tint;
|
||||
}
|
||||
hit
|
||||
});
|
||||
let without = run(&mut s, "swatch.newGroup", json!({"fromArtwork": true}));
|
||||
assert_eq!(without["swatches"], json!(["Brand", "R=255 G=0 B=0", "R=0 G=255 B=0"]), "a tint brings its swatch");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let with = run(&mut s, "swatch.newGroup", json!({"fromArtwork": true, "includeTints": true}));
|
||||
assert_eq!(with["swatches"], json!(["Brand", "R=255 G=0 B=0", "R=0 G=255 B=0", "R=149 G=183 B=216"]));
|
||||
assert_eq!(doc(&s).node(b).unwrap().appearance.stroke_paint(), Paint::Solid { color: tint, swatch: Some("Brand".into()) }, "tints stay linked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_names_are_never_blank_and_never_shared_with_their_swatches() {
|
||||
let mut s = session();
|
||||
art(&mut s);
|
||||
// A blank name falls back to the default; a swatch never takes its own group's name.
|
||||
let r = run(&mut s, "swatch.newGroup", json!({"name": " ", "colors": ["#0000ff"]}));
|
||||
assert_eq!(r["name"], "Color Group");
|
||||
let r = run(&mut s, "swatch.newGroup", json!({"name": "R=255 G=0 B=0", "fromArtwork": true}));
|
||||
assert_eq!(r["name"], "R=255 G=0 B=0");
|
||||
assert_eq!(r["swatches"], json!(["Brand", "R=255 G=0 B=0 2", "R=0 G=255 B=0"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_from_a_mesh_holds_its_point_colours() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"ids": [a.0], "color": "#ff0000"}));
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
run(&mut s, "object.mesh.create", json!({"rows": 2, "cols": 2, "appearance": "center", "highlight": 100}));
|
||||
let r = run(&mut s, "swatch.newGroup", json!({"fromArtwork": true}));
|
||||
let names = r["swatches"].as_array().unwrap();
|
||||
assert!(names.contains(&json!("R=255 G=0 B=0")) && names.contains(&json!("R=255 G=255 B=255")), "{r}");
|
||||
}
|
||||
Reference in New Issue
Block a user