#4919: Create Swatch Legend becomes an add-on under Object › Plug-ins › Color Effects with a dialog

addon.color.swatchLegend replaces swatch.legend. The dialog sets columns, chip size, gap, the values shown
and whether to use only the swatches selected in the Swatches panel, with a preview; duplicate value
kinds are refused. The Swatches panel menu item is removed. Catalog rows in all 26 languages.
This commit is contained in:
Don Omondi
2026-11-02 14:36:08 +07:00
committed by bflatastic
parent 236a3c264d
commit 15a5e64134
40 changed files with 280 additions and 78 deletions
-2
View File
@@ -84,7 +84,6 @@ mod stroke;
mod style;
pub mod stylelib;
mod swatch;
pub mod swatchlegend;
pub mod swatchlib;
pub(crate) mod symboledit;
pub mod symbollib;
@@ -298,7 +297,6 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(paint::proxy_specs());
v.extend(overprint::specs());
v.extend(swatchlib::specs());
v.extend(swatchlegend::specs());
v.extend(freeform::specs());
v.extend(flatten::specs());
v.extend(stylelib::specs());
+7 -2
View File
@@ -2,10 +2,12 @@
//! Duotone and Invert Colors, in Effect › Color Effects) are the effects crate's; here, Apply as
//! Filter writes any colour adjustment into the colours themselves, Select by Color Kind selects the
//! objects that use spot colours, carry a live colour adjustment, or are images of a colour model,
//! Print Ink Fixes ([`print_fixes`]) prepares a CMYK document's inks for print, and Find Nearest
//! Swatch ([`nearest`]) matches colours to a swatch library's. Its panel and its submenu, Object ›
//! Print Ink Fixes ([`print_fixes`]) prepares a CMYK document's inks for print, Find Nearest
//! Swatch ([`nearest`]) matches colours to a swatch library's, and Create Swatch Legend
//! ([`legend`]) draws labelled chips of the document's swatches. Its panel and its submenu, Object ›
//! Plug-ins › Color Effects, are the UI's.
pub mod legend;
mod nearest;
mod print_fixes;
#[cfg(test)]
@@ -71,6 +73,9 @@ pub fn specs() -> Vec<CommandSpec> {
nearest::apply
),
]
.into_iter()
.chain(legend::specs())
.collect()
}
fn select(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1,26 +1,31 @@
//! Create Swatch Legend (#4919): a grid of chips on the artboard, one per colour swatch, each
//! filled with its swatch and labelled with the swatch's name and colour values, as one group made
//! in one undo step.
//! Create Swatch Legend (`addon.color.swatchLegend`; Object › Plug-ins › Color Effects › Create
//! Swatch Legend…, #4919): a grid of chips on the artboard, one per colour swatch, each filled with
//! its swatch and labelled with the swatch's name and colour values, as one group made in one undo
//! step.
use serde_json::{Value, json};
use vectorcraft_color::{Color, Paint, cms::Model};
use vectorcraft_doc::{Appearance, AreaFit, Node, NodeKind, TextKind, TextObject};
use vectorcraft_geom::{Point, Rect, shapes};
use super::{CommandSpec, Result, Session, bad, cmd, has_doc};
use super::super::super::{CommandSpec, bad, cmd, has_doc};
use crate::{Result, Session};
pub fn specs() -> Vec<CommandSpec> {
vec![cmd!(
"swatch.legend",
"addon.color.swatchLegend",
"Create Swatch Legend",
["Window", "Swatches"],
["Object", "Plug-ins", "Color Effects"],
None,
"{names?: [swatch name…] (default: every colour swatch of the document, in panel order), columns?: 1..50 (4), size?: chip side in pt 8..1000 (72), gap?: pt 0..1000 (12), values?: [hex|rgb|cmyk|lab|gray…] (default: each swatch's own colour model; spot swatches also say spot), x?, y? (the legend's top-left; default: the first artboard's, inset by `gap`)} draw a legend of colour swatches: one square chip per swatch filled with it (linked to it), labelled inside with the swatch name and the values asked for, in black on light chips and white on dark ones, as one group in one undo step; the group is selected → {id, count} (#4919)",
"{names?: [swatch name…] (default: every colour swatch of the document, in panel order), columns?: 1..50 (4), size?: chip side in pt 8..1000 (72), gap?: pt 0..1000 (12), values?: [hex|rgb|cmyk|lab|gray…] (each at most once; default: each swatch's own colour model; spot swatches also say spot), x?, y? (the legend's top-left; default: the first artboard's, inset by `gap`)} draw a legend of colour swatches: one square chip per swatch filled with it (linked to it), labelled inside with the swatch name and the values asked for, in black on light chips and white on dark ones, as one group in one undo step; the group is selected → {id, count} (#4919)",
has_doc,
legend
)]
}
/// The command.
pub const COMMAND: &str = "addon.color.swatchLegend";
/// Most swatches one legend draws.
const MAX_CHIPS: usize = 2000;
const VALUES: [&str; 5] = ["hex", "rgb", "cmyk", "lab", "gray"];
@@ -78,7 +83,7 @@ fn value_lines(c: Color, values: &[String], spot: bool) -> Vec<String> {
}
fn legend(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "swatch.legend";
const C: &str = COMMAND;
let columns = number(p, "columns", 4.0, 1.0..=50.0, C)?;
if columns.fract() != 0.0 {
return Err(bad(C, "`columns` must be a whole number 1..50"));
@@ -96,6 +101,10 @@ fn legend(s: &mut Session, p: &Value) -> Result<Value> {
.collect::<Result<_>>()?,
Some(_) => return Err(bad(C, "`values` must be a list")),
};
// Each kind once: every value line goes into every chip, so repeats would only multiply them.
if values.iter().enumerate().any(|(i, v)| values[..i].contains(v)) {
return Err(bad(C, "`values` names a kind twice"));
}
let st = s.doc()?;
let doc = &st.doc;
let colour = |w: &&vectorcraft_color::Swatch| w.paint.color().is_some();
@@ -128,11 +137,12 @@ fn legend(s: &mut Session, p: &Value) -> Result<Value> {
let parent = st.target_parent()?;
let text_size = (size / 7.0).clamp(4.0, 12.0);
let pad = (size / 12.0).clamp(1.0, 8.0);
let style = super::create::new_type_style(s, &json!({ "size": text_size }));
let style = super::super::super::create::new_type_style(s, &json!({ "size": text_size }));
let count = swatches.len();
// Each chip's fill links to its swatch, as paint.setFill {swatch} links art: editing a global or
// spot swatch then recolours the legend too.
let fills: Vec<Paint> = swatches.iter().map(|w| super::paint::swatch_solid(doc, &w.name, None).unwrap_or_else(|_| w.paint.clone())).collect();
let fills: Vec<Paint> =
swatches.iter().map(|w| super::super::super::paint::swatch_solid(doc, &w.name, None).unwrap_or_else(|_| w.paint.clone())).collect();
let gid = s.edit("Create Swatch Legend", |d, sel| {
let mut children = Vec::with_capacity(count * 2);
for (i, (w, fill)) in swatches.iter().zip(fills).enumerate() {
@@ -154,9 +164,9 @@ fn legend(s: &mut Session, p: &Value) -> Result<Value> {
let inner = (size - 2.0 * pad).max(1.0);
t.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(0.0, 0.0, inner, inner)) };
t.area.fit = AreaFit::ShrinkText { min_percent: AreaFit::MIN_PERCENT };
t.para = super::create::new_type_para();
t.para = super::super::super::create::new_type_para();
t.system_layout = system_layout;
super::typecmd::refresh_bounds(&mut t);
super::super::super::typecmd::refresh_bounds(&mut t);
children.push(std::sync::Arc::new(Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)))));
}
let gid = d.alloc_id();
@@ -33,7 +33,7 @@ fn a_legend_draws_a_labelled_chip_per_colour_swatch_in_one_undo_step() {
s.execute("swatch.new", &json!({"name": "Paper", "color": "#f8f4e8"})).unwrap();
let colours = s.doc().unwrap().doc.swatches_iter().filter(|w| w.paint.color().is_some()).count();
let undo = s.doc().unwrap().history.undo.len();
let r = s.execute("swatch.legend", &json!({"columns": 3, "size": 60, "gap": 10})).unwrap();
let r = s.execute("addon.color.swatchLegend", &json!({"columns": 3, "size": 60, "gap": 10})).unwrap();
assert_eq!(r["count"].as_u64().unwrap() as usize, colours);
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
assert_eq!(s.doc().unwrap().selection.objects, [NodeId(r["id"].as_u64().unwrap())], "the legend is selected");
@@ -65,7 +65,8 @@ fn a_legend_draws_a_labelled_chip_per_colour_swatch_in_one_undo_step() {
fn a_legend_takes_names_values_and_a_position() {
let mut s = session();
s.execute("swatch.new", &json!({"name": "Signal", "color": "cmyk 0 100 100 0", "spot": true})).unwrap();
let r = s.execute("swatch.legend", &json!({"names": ["Signal"], "values": ["hex", "cmyk", "lab", "gray"], "x": 100, "y": 50})).unwrap();
let r =
s.execute("addon.color.swatchLegend", &json!({"names": ["Signal"], "values": ["hex", "cmyk", "lab", "gray"], "x": 100, "y": 50})).unwrap();
assert_eq!(r["count"], 1);
let g = group(&s, r["id"].as_u64().unwrap());
let (text, _) = labels(&g).remove(0);
@@ -87,8 +88,9 @@ fn a_legend_takes_names_values_and_a_position() {
json!({"gap": -1}),
json!({"x": f64::MAX}),
json!({"names": []}),
json!({"values": ["rgb", "rgb"]}),
] {
assert!(s.execute("swatch.legend", &bad).is_err(), "{bad}");
assert!(s.execute("addon.color.swatchLegend", &bad).is_err(), "{bad}");
}
assert_eq!(s.doc().unwrap().history.undo.len(), before);
}
@@ -99,7 +101,9 @@ fn a_legend_takes_names_values_and_a_position() {
fn chips_stay_linked_and_labels_stay_on_their_chips() {
let mut s = session();
s.execute("swatch.new", &json!({"name": "A Rather Long Brand Colour Name", "color": "#2050a0", "global": true})).unwrap();
let r = s.execute("swatch.legend", &json!({"names": ["A Rather Long Brand Colour Name"], "size": 40, "values": ["hex", "rgb", "cmyk"]})).unwrap();
let r = s
.execute("addon.color.swatchLegend", &json!({"names": ["A Rather Long Brand Colour Name"], "size": 40, "values": ["hex", "rgb", "cmyk"]}))
.unwrap();
let id = r["id"].as_u64().unwrap();
s.execute("swatch.edit", &json!({"name": "A Rather Long Brand Colour Name", "color": "#a02050"})).unwrap();
let g = group(&s, id);
+4 -1
View File
@@ -3,8 +3,10 @@
//! filter, into the colours themselves (`addon.color.filter`); below, under Inks, it opens Duotone's
//! dialog and Print Ink Fixes' (`addon.color.printFixes`). The submenu applies the add-on's live
//! effects, Duotone and Vector Halftone among them (Effect › Color Effects has them too), opens
//! Print Ink Fixes and Find Nearest Swatch ([`nearest`]) and opens the panel.
//! Print Ink Fixes, Find Nearest Swatch ([`nearest`]) and Create Swatch Legend ([`legend`]) and
//! opens the panel.
pub mod legend;
pub mod nearest;
use egui::Ui;
@@ -100,6 +102,7 @@ pub fn menu() -> Vec<Item> {
Item::Sep,
cmd(print_fixes()),
Item::Cmd("Find Nearest Swatch…", nearest::COMMAND, Value::Null),
Item::Cmd("Create Swatch Legend…", legend::COMMAND, Value::Null),
Item::Sep,
Item::Sub(
"Select by Color Kind",
+93
View File
@@ -0,0 +1,93 @@
//! Create Swatch Legend in the interface (Object › Plug-ins › Color Effects › Create Swatch
//! Legend…, #4919): a dialog for the columns, the chip size, the gap and which colour values the
//! labels show, and whether only the swatches selected in the Swatches panel are drawn. It
//! previews on the canvas and runs `addon.color.swatchLegend` on OK.
use egui::Ui;
use serde_json::{Value, json};
use crate::VectorcraftApp;
use crate::dialogs::{DialogSpec, form};
use crate::state::Dialog;
use crate::widgets;
/// The command, and its dialog's kind.
pub const COMMAND: &str = "addon.color.swatchLegend";
pub const KIND: &str = "swatchLegend";
const LABEL_W: f32 = 80.0;
const FIELD_W: f32 = 90.0;
/// The values a label can show: (the command's name, the dialog's label).
pub const VALUES: [(&str, &str); 5] = [("hex", "HEX"), ("rgb", "RGB"), ("cmyk", "CMYK"), ("lab", "Lab"), ("gray", "Grayscale")];
/// The dialog's fields to start with: the command's defaults, each swatch's own values.
pub(crate) fn fields() -> Value {
json!({ "columns": 4, "size": 72, "gap": 12, "selectedOnly": false })
}
pub(crate) const SPEC: DialogSpec =
DialogSpec { heading: |_| tl!("Create Swatch Legend").to_string(), body, confirm, preview: true, min_width: 300.0, ..DialogSpec::FORM };
/// The colour swatches selected in the Swatches panel.
fn selected_swatches(app: &VectorcraftApp, ctx: &egui::Context) -> Vec<String> {
let picked: Vec<String> = crate::panels::pstate(ctx, "swatch-selected");
app.session
.active()
.map(|st| st.doc.swatches_iter().filter(|w| w.paint.color().is_some() && picked.contains(&w.name)).map(|w| w.name.clone()).collect())
.unwrap_or_default()
}
/// The command's params from the dialog's fields.
pub(crate) fn params(d: &Dialog) -> Value {
let mut p = json!({ "columns": d.f64("columns", 4.0).round(), "size": d.f64("size", 72.0), "gap": d.f64("gap", 12.0) });
let values: Vec<&str> = VALUES.iter().filter(|(v, _)| d.bool(v)).map(|(v, _)| *v).collect();
if !values.is_empty() {
p["values"] = json!(values);
}
if d.bool("selectedOnly")
&& let Some(names) = d.fields.get("names").filter(|n| n.as_array().is_some_and(|a| !a.is_empty()))
{
p["names"] = names.clone();
}
p
}
fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let unit = app.session.general_unit();
widgets::label_row(ui, tl!("Columns:"), LABEL_W, |ui| {
if let Some(v) = widgets::range_field(ui, ("swatch-legend", "columns"), d.f64("columns", 4.0), 1.0..=50.0, "", 0, FIELD_W) {
d.fields.insert("columns".into(), json!(v.round()));
}
});
widgets::label_row(ui, tl!("Size:"), LABEL_W, |ui| {
form::length_within(ui, d, "size", unit, FIELD_W, Some(8.0..=1000.0));
});
widgets::label_row(ui, tl!("Gap:"), LABEL_W, |ui| {
form::length_within(ui, d, "gap", unit, FIELD_W, Some(0.0..=1000.0));
});
ui.add_space(4.0);
// None ticked: each swatch shows the values of its own colour model.
ui.label(tl!("Show:"));
ui.horizontal_wrapped(|ui| {
for (v, label) in VALUES {
form::check(ui, d, v, tl!(label));
}
});
// Only the swatches selected in the Swatches panel, when some are.
let names = selected_swatches(app, ui.ctx());
if names.is_empty() {
d.fields.insert("selectedOnly".into(), json!(false));
} else {
form::check(ui, d, "selectedOnly", tl!("Selected Swatches Only"));
}
d.fields.insert("names".into(), json!(names));
form::preview(app, ui, d, "Create Swatch Legend", COMMAND, params(d));
false
}
/// OK: run the command (keep its preview) and close.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
form::commit_preview(app, COMMAND, params(d))
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,58 @@
use serde_json::json;
use vectorcraft_engine::Session;
use crate::tests_labels::painted_text;
use crate::{VectorcraftApp, menus};
/// #4919: Object › Plug-ins › Color Effects › Create Swatch Legend… opens a dialog with the
/// columns, chip size, gap and values; it previews, Cancel rolls it back, and OK draws the legend
/// with the values picked as one undo step. With swatches selected in the Swatches panel, it can
/// draw only those.
#[test]
fn create_swatch_legend_opens_its_dialog_previews_and_runs_on_ok() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 600, "height": 400})).unwrap();
app.run("swatch.new", json!({"name": "Ink", "color": "#123456"})).unwrap();
let entries = menus::menu_entries(&app);
let item = entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Color Effects"] && e.label == "Create Swatch Legend…").expect("menu item");
let (cmd, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params);
menus::invoke(&mut app, &cmd, p);
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(super::KIND));
let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
for s in ["Create Swatch Legend", "Columns:", "Size:", "Gap:", "HEX", "CMYK", "Grayscale", "Preview"] {
assert!(shown.contains(s), "{s} in {shown}");
}
let count = |app: &VectorcraftApp| app.session.doc().unwrap().doc.layers[0].children().map_or(0, Vec::len);
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("preview".into(), json!(true));
painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
assert_eq!(count(&app), 1, "previewed: one legend group");
crate::dialogs::cancel(&mut app);
assert_eq!(count(&app), 0, "rolled back");
// OK with HEX and CMYK and 2 columns.
menus::invoke(&mut app, &cmd, serde_json::Value::Null);
let n = app.session.doc().unwrap().history.undo.len();
let d = app.ui.dialog.as_mut().unwrap();
for (k, v) in [("hex", json!(true)), ("cmyk", json!(true)), ("columns", json!(2))] {
d.fields.insert(k.into(), v);
}
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(app.session.doc().unwrap().history.undo.len(), n + 1);
let st = app.session.doc().unwrap();
let g = st.doc.layers[0].children().unwrap().last().unwrap();
let label = g.children().unwrap().iter().find_map(|c| match &c.kind {
vectorcraft_doc::NodeKind::Text(t) if t.plain_text().starts_with("Ink") => Some(t.plain_text()),
_ => None,
});
let label = label.expect("Ink's label");
assert!(label.contains("#123456") && label.contains("C="), "{label}");
// Only the swatch selected in the Swatches panel.
let ctx = egui::Context::default();
crate::panels::set_pstate(&ctx, "swatch-selected", vec!["Ink".to_string()]);
assert_eq!(super::selected_swatches(&app, &ctx), ["Ink"]);
let mut d = crate::state::Dialog::new(super::KIND, super::fields());
d.fields.insert("selectedOnly".into(), json!(true));
d.fields.insert("names".into(), json!(["Ink"]));
assert_eq!(super::params(&d)["names"], json!(["Ink"]));
}
+1
View File
@@ -407,6 +407,7 @@ registry! {
PathToolOptions: [crate::addons::pathtools::KIND] => crate::addons::pathtools::OPTIONS_SPEC,
DivideDashes: [crate::addons::pathtools::dashes::KIND] => crate::addons::pathtools::dashes::SPEC,
NearestSwatch: [crate::addons::color::nearest::KIND] => crate::addons::color::nearest::SPEC,
SwatchLegend: [crate::addons::color::legend::KIND] => crate::addons::color::legend::SPEC,
ArrangeTools: [crate::addons::arrange::GRID, crate::addons::arrange::REPLACE, crate::addons::arrange::SIZE, crate::addons::arrange::OPACITY, crate::addons::arrange::SORT] => crate::addons::arrange::SPEC,
ShortcutImport: [shortcut_import::KIND] => DialogSpec::window(shortcut_import::show, shortcut_import::confirm),
}
+3
View File
@@ -6091,3 +6091,6 @@ breakText Glyphs Глифи
Small to Large От малко към голямо
Large to Small От голямо към малко
Distribute Evenly Равномерно разпределение
Create Swatch Legend Създаване на легенда на образците
Create Swatch Legend… Създаване на легенда на образците…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6122,3 +6122,6 @@ shape Open Otvoren
Small to Large Od manjeg prema većem
Large to Small Od većeg prema manjem
Distribute Evenly Rasporedi ravnomjerno
Create Swatch Legend Kreiraj legendu uzoraka boja
Create Swatch Legend… Kreiraj legendu uzoraka boja…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6216,3 +6216,6 @@ breakText Glyphs Glifs
Small to Large De petit a gran
Large to Small De gran a petit
Distribute Evenly Distribueix uniformement
Create Swatch Legend Crea una llegenda de mostres
Create Swatch Legend… Crea una llegenda de mostres…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6228,3 +6228,6 @@ breakText Glyphs Glyfy
Small to Large Od nejmenšího po největší
Large to Small Od největšího po nejmenší
Distribute Evenly Rozmístit rovnoměrně
Create Swatch Legend Vytvořit legendu vzorníku
Create Swatch Legend… Vytvořit legendu vzorníku…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6106,3 +6106,6 @@ breakText Glyphs Glyffer
Small to Large Fra lille til stor
Large to Small Fra stor til lille
Distribute Evenly Fordel jævnt
Create Swatch Legend Opret farveprøveforklaring
Create Swatch Legend… Opret farveprøveforklaring…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5814,3 +5814,6 @@ breakText Glyphs Glyphen
Small to Large Von klein nach groß
Large to Small Von groß nach klein
Distribute Evenly Gleichmäßig verteilen
Create Swatch Legend Farbfeldlegende erstellen
Create Swatch Legend… Farbfeldlegende erstellen…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6180,3 +6180,6 @@ breakText Glyphs Γλύφοι
Small to Large Από μικρό σε μεγάλο
Large to Small Από μεγάλο σε μικρό
Distribute Evenly Ομοιόμορφη κατανομή
Create Swatch Legend Δημιουργία υπομνήματος δειγμάτων χρωμάτων
Create Swatch Legend… Δημιουργία υπομνήματος δειγμάτων χρωμάτων…
HEX HEX
Can't render this file because it contains an unexpected character in line 163 and column 5.
+3
View File
@@ -6170,3 +6170,6 @@ breakText Glyphs Glifos
Small to Large De menor a mayor
Large to Small De mayor a menor
Distribute Evenly Distribuir uniformemente
Create Swatch Legend Crear leyenda de muestras
Create Swatch Legend… Crear leyenda de muestras…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6258,3 +6258,6 @@ breakText Glyphs Merkit
Small to Large Pienestä suureen
Large to Small Suuresta pieneen
Distribute Evenly Jaa tasaisesti
Create Swatch Legend Luo värimallien selite
Create Swatch Legend… Luo värimallien selite…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6223,3 +6223,6 @@ breakText Glyphs Glyphes
Small to Large Du plus petit au plus grand
Large to Small Du plus grand au plus petit
Distribute Evenly Répartir uniformément
Create Swatch Legend Créer une légende des nuances
Create Swatch Legend… Créer une légende des nuances…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6212,3 +6212,6 @@ breakText Glyphs Glifák
Small to Large Kicsitől a nagyig
Large to Small Nagytól a kicsiig
Distribute Evenly Egyenletes elosztás
Create Swatch Legend Színmintajelmagyarázat létrehozása
Create Swatch Legend… Színmintajelmagyarázat létrehozása…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6106,3 +6106,6 @@ breakText Glyphs Glif
Small to Large Kecil ke besar
Large to Small Besar ke kecil
Distribute Evenly Distribusikan merata
Create Swatch Legend Buat Legenda Swatch
Create Swatch Legend… Buat Legenda Swatch…
HEX HEX
Can't render this file because it contains an unexpected character in line 2206 and column 5.
+3
View File
@@ -5848,3 +5848,6 @@ breakText Glyphs Glifi
Small to Large Dal più piccolo al più grande
Large to Small Dal più grande al più piccolo
Distribute Evenly Distribuisci uniformemente
Create Swatch Legend Crea legenda campioni
Create Swatch Legend… Crea legenda campioni…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5913,3 +5913,6 @@ breakText Glyphs 字形
Small to Large 小さい順
Large to Small 大きい順
Distribute Evenly 均等に分布
Create Swatch Legend スウォッチの凡例を作成
Create Swatch Legend… スウォッチの凡例を作成…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6216,3 +6216,6 @@ breakText Glyphs გლიფები
Small to Large პატარიდან დიდისკენ
Large to Small დიდიდან პატარისკენ
Distribute Evenly თანაბრად განაწილება
Create Swatch Legend ფერის ნიმუშების ლეგენდის შექმნა
Create Swatch Legend… ფერის ნიმუშების ლეგენდის შექმნა…
HEX HEX
Can't render this file because it is too large.
+3
View File
@@ -5809,3 +5809,6 @@ breakText Glyphs 글리프
Small to Large 작은 것부터
Large to Small 큰 것부터
Distribute Evenly 균등 분포
Create Swatch Legend 견본 범례 만들기
Create Swatch Legend… 견본 범례 만들기…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6150,3 +6150,6 @@ breakText Glyphs Glyphs
Small to Large Van klein naar groot
Large to Small Van groot naar klein
Distribute Evenly Gelijkmatig verdelen
Create Swatch Legend Stalenlegenda maken
Create Swatch Legend… Stalenlegenda maken…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6083,3 +6083,6 @@ breakText Glyphs Glify
Small to Large Od najmniejszego do największego
Large to Small Od największego do najmniejszego
Distribute Evenly Rozmieść równomiernie
Create Swatch Legend Utwórz legendę próbek
Create Swatch Legend… Utwórz legendę próbek…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6301,3 +6301,6 @@ swatch Yellow Green Verde-amarelado
Small to Large Do menor para o maior
Large to Small Do maior para o menor
Distribute Evenly Distribuir uniformemente
Create Swatch Legend Criar Legenda de Amostras
Create Swatch Legend… Criar Legenda de Amostras…
HEX HEX
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -6205,3 +6205,6 @@ breakText Glyphs Глифы
Small to Large От меньшего к большему
Large to Small От большего к меньшему
Distribute Evenly Распределить равномерно
Create Swatch Legend Создать легенду образцов
Create Swatch Legend… Создать легенду образцов…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6280,3 +6280,6 @@ breakText Glyphs Glyfer
Small to Large Från liten till stor
Large to Small Från stor till liten
Distribute Evenly Fördela jämnt
Create Swatch Legend Skapa färgrutelegend
Create Swatch Legend… Skapa färgrutelegend…
HEX HEX
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -6185,3 +6185,6 @@ breakText Glyphs Glifler
Small to Large Küçükten Büyüğe
Large to Small Büyükten Küçüğe
Distribute Evenly Eşit Dağıt
Create Swatch Legend Renk Örneği Açıklaması Oluştur
Create Swatch Legend… Renk Örneği Açıklaması Oluştur…
HEX HEX
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -5828,3 +5828,6 @@ breakText Glyphs Гліфи
Small to Large Від меншого до більшого
Large to Small Від більшого до меншого
Distribute Evenly Розподілити рівномірно
Create Swatch Legend Створити легенду зразків
Create Swatch Legend… Створити легенду зразків…
HEX HEX
Can't render this file because it contains an unexpected character in line 177 and column 5.
+3
View File
@@ -5829,3 +5829,6 @@ breakText Glyphs Glyph
Small to Large Từ nhỏ đến lớn
Large to Small Từ lớn đến nhỏ
Distribute Evenly Phân bố đều
Create Swatch Legend Tạo chú giải mẫu màu
Create Swatch Legend… Tạo chú giải mẫu màu…
HEX HEX
Can't render this file because it contains an unexpected character in line 199 and column 5.
+3
View File
@@ -6069,3 +6069,6 @@ breakText Glyphs 字形
Small to Large 从小到大
Large to Small 从大到小
Distribute Evenly 均匀分布
Create Swatch Legend 创建色板图例
Create Swatch Legend… 创建色板图例…
HEX HEX
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5866,3 +5866,6 @@ breakText Glyphs 字符
Small to Large 從小到大
Large to Small 從大到小
Distribute Evenly 均勻分佈
Create Swatch Legend 建立色票圖例
Create Swatch Legend… 建立色票圖例…
HEX HEX
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -70,6 +70,7 @@ pub(super) fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static st
"object.vectorHalftone" => (crate::dialogs::halftone::KIND, crate::dialogs::halftone::fields()),
crate::addons::pathtools::dashes::COMMAND => (crate::addons::pathtools::dashes::KIND, crate::addons::pathtools::dashes::fields()),
crate::addons::color::nearest::COMMAND => (crate::addons::color::nearest::KIND, crate::addons::color::nearest::fields()),
crate::addons::color::legend::COMMAND => (crate::addons::color::legend::KIND, crate::addons::color::legend::fields()),
"artboard.rearrange" => (crate::dialogs::rearrange_artboards::KIND, crate::dialogs::rearrange_artboards::open_fields(app)),
_ => return None,
})
@@ -164,19 +164,6 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
if menu_item(ui, tl!("Add Used Colors"), true, false) {
app.run("swatch.addUsedColors", json!({})).ok();
}
// Create Swatch Legend (#4919): the selected colour swatches, else every one.
if menu_item(ui, tl!("Create Swatch Legend"), app.session.active().is_some(), false) {
let picked = deletable(&selected);
let colours: Vec<String> = app
.session
.active()
.map(|st| st.doc.swatches_iter().filter(|w| w.paint.color().is_some() && picked.contains(&w.name)).map(|w| w.name.clone()).collect())
.unwrap_or_default();
let p = if colours.is_empty() { json!({}) } else { json!({ "names": colours }) };
if let Err(e) = app.run("swatch.legend", p) {
app.status(e);
}
}
ui.separator();
if menu_item(ui, tl!("Sort by Name"), true, false) {
app.run("swatch.sortByName", json!({})).ok();
@@ -770,40 +770,3 @@ fn lab_spot_tiles_show_the_colour_the_spot_colors_options_pick() {
assert_eq!(tile(&app), app.session.active().unwrap().doc.global_color("Ink").unwrap());
assert!(matches!(tile(&app), Color::Cmyk { .. }));
}
/// One frame of the panel menu at `time`; its painted labels.
fn menu_labels(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>, time: f64) -> Vec<(String, Rect)> {
let input =
egui::RawInput { events, time: Some(time), screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(280.0, 900.0))), ..Default::default() };
let mut out = ctx.run_ui(input, |ui| menu(app, ui));
out.textures_delta.clear();
crate::tests_removeanchors::shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::<Vec<_>>())
}
/// #4919: the menu's Create Swatch Legend draws the selected colour swatches, or every one when
/// none is selected.
#[test]
fn create_swatch_legend_draws_the_selected_swatches_or_all() {
let mut app = app();
app.run("swatch.new", json!({"name": "Ink 185", "color": "#e4002b", "spot": true})).unwrap();
let ctx = context();
let click = |app: &mut VectorcraftApp, ctx: &egui::Context, t: f64| {
let labels = menu_labels(app, ctx, vec![], t);
let at = labels.iter().find(|(l, _)| l.trim() == "Create Swatch Legend").map(|(_, r)| r.center()).expect("Create Swatch Legend");
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
menu_labels(app, ctx, vec![Event::PointerMoved(at), button(true)], t + 0.1);
menu_labels(app, ctx, vec![button(false)], t + 0.2);
};
let count = |app: &VectorcraftApp| {
let st = app.session.active().unwrap();
let id = *st.selection.objects.first().expect("the legend is selected");
st.doc.node(id).unwrap().children().unwrap().len() / 2
};
set_pstate(&ctx, "swatch-selected", vec!["Ink 185".to_string(), "[None]".to_string()]);
click(&mut app, &ctx, 0.0);
assert_eq!(count(&app), 1, "the selected colour swatch alone");
set_pstate(&ctx, "swatch-selected", Vec::<String>::new());
click(&mut app, &ctx, 1.0);
let colours = app.session.active().unwrap().doc.swatches_iter().filter(|w| w.paint.color().is_some()).count();
assert_eq!(count(&app), colours, "every colour swatch");
}
+5 -4
View File
@@ -2108,16 +2108,17 @@ swatch in the library panel does). The pattern library Geometric Patterns (`geom
{"name":"run_command","arguments":{"command":"swatch.library.get","params":{"library":"earth-tones"}}}
{"name":"run_command","arguments":{"command":"swatch.library.add","params":{"library":"earth-tones","names":["Clay"]}}}
```
`swatch.legend {names?, columns?, size?, gap?, values?, x?, y?}` draws a legend of colour swatches on the page as one
`addon.color.swatchLegend {names?, columns?, size?, gap?, values?, x?, y?}` draws a legend of colour swatches on the page as one
group, in one undo step, and selects it: a square chip per swatch (`size` pt, 72 by default, `columns` to a row, 4 by
default, `gap` pt apart, 12 by default) filled with the swatch and linked to it, labelled inside with the swatch's name
and its values (`values`: any of `hex`, `rgb`, `cmyk`, `lab`, `gray`; by default the swatch's own colour model; spot
swatches also say Spot), in black on light chips and white on dark ones. `names` picks swatches (default: every colour
swatch, in panel order); `x`, `y` place the top-left (default: the first artboard's, inset by `gap`). The Swatches
panel menu's Create Swatch Legend draws the selected colour swatches, else every one (#4919). → `{id, count}`
swatch, in panel order); `x`, `y` place the top-left (default: the first artboard's, inset by `gap`). Object ›
Plug-ins › Color Effects › Create Swatch Legend… sets these in a dialog with a preview, and can limit it to the
swatches selected in the Swatches panel (#4919). → `{id, count}`
```json
{"name":"run_command","arguments":{"command":"swatch.legend","params":{"columns":6,"values":["hex","cmyk"]}}}
{"name":"run_command","arguments":{"command":"addon.color.swatchLegend","params":{"columns":6,"values":["hex","cmyk"]}}}
```
`swatch.library.save {path?, format?: "vcswatches"|"gpl"|"ase"|"css", names?, name?, user?}` writes the document's
swatches as a library (`.vcswatches` keeps colour models, global, spot, gradients and colour
+2 -2
View File
@@ -1501,14 +1501,14 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov
A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Color Effects › Find Nearest Swatch… lists each colour of the selection (fills and strokes, as ticked; solid colours and gradient stops, inside groups and type) with the three swatches of the chosen library closest to it by CIEDE2000 difference, with that difference (`addon.color.nearestSwatch`, #4923). The library is the document's swatches, a built-in library, or a library or colour book the user saved or loaded (no colour book data ships with VectorCraft). Apply gives each colour its nearest swatch as one undo step (`addon.color.applyNearestSwatch`): global and spot swatches link, and a library's swatches the colours take are added to the document first, so a spot colour stays spot. Remaining: typing a colour to look up in the dialog (the command takes `color`), and applying a match other than the nearest. Tests: `cmd::addons::color::nearest::tests` (engine), `addons::color::nearest::tests::find_nearest_swatch_lists_the_matches_and_applies_the_nearest` (ui-egui).
## Create Swatch Legend (2026-11-01)
A VectorCraft addition (it replaces a script some users ran), so no area score moves. The Swatches panel menu's Create Swatch Legend (`swatch.legend`) draws a chip per colour swatch (the selected ones, else all) filled with and linked to its swatch, labelled with its name and values (HEX, RGB, CMYK, Lab or Grayscale; by default its own model; spot swatches marked), black on light chips and white on dark ones, in a grid of chosen columns, chip size and spacing, as one selected group in one undo step (#4919). Tests: `cmd::swatchlegend::tests` (engine), `panels::swatches::tests::create_swatch_legend_draws_the_selected_swatches_or_all` (ui-egui).
A VectorCraft addition (it replaces a script some users ran), so no area score moves. Object › Plug-ins › Color Effects › Create Swatch Legend… (`addon.color.swatchLegend`) opens a dialog (columns, chip size, gap, values, selected swatches only, with a preview) and draws a chip per colour swatch (the selected ones, else all) filled with and linked to its swatch, labelled with its name and values (HEX, RGB, CMYK, Lab or Grayscale; by default its own model; spot swatches marked), black on light chips and white on dark ones, in a grid of chosen columns, chip size and spacing, as one selected group in one undo step (#4919). Tests: `cmd::addons::color::legend::tests` (engine), `addons::color::legend::tests::create_swatch_legend_opens_its_dialog_previews_and_runs_on_ok` (ui-egui).
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-11-01 | minor | Color Effects add-on (a VectorCraft addition): Find Nearest Swatch (#4923); evidence added; scores unchanged. |
| 2026-11-01 | minor | Create Swatch Legend in the Swatches panel menu (`swatch.legend`, #4919); evidence added; scores unchanged. |
| 2026-11-01 | minor | Create Swatch Legend add-on under Object › Plug-ins › Color Effects (`addon.color.swatchLegend`, #4919); evidence added; scores unchanged. |
| 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): Export All Open Documents as PDF or SVG (#4920); evidence added; scores unchanged. |
| 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Expand Dashes into Paths (#4922); evidence added; scores unchanged. |
| 2026-11-01 | minor | Type core: the Glyphs panel highlights the character selected with the Type tool and scrolls it into view (#4908); evidence added; scores unchanged. |
+1 -1
View File
@@ -401,7 +401,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|---|---|---|
| 2026-11-01 | minor | Color Effects add-on (a VectorCraft addition): Find Nearest Swatch (#4923); scores and estimates unchanged |
| 2026-11-01 | minor | Artboards panel: a row dragged above or below another reorders the artboards (#4687); scores and estimates unchanged |
| 2026-11-01 | minor | Swatches panel menu: Create Swatch Legend draws labelled chips of the selected (or all) colour swatches as one group (#4919); scores and estimates unchanged |
| 2026-11-01 | minor | Object › Plug-ins › Color Effects › Create Swatch Legend…: a dialog that draws labelled chips of the colour swatches (all, or the ones selected in the Swatches panel) as one group (#4919); scores and estimates unchanged |
| 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): Export All Open Documents as PDF or SVG (#4920); scores and estimates unchanged |
| 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Expand Dashes into Paths (#4922); scores and estimates unchanged |
| 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Randomize Opacity (#4925); scores and estimates unchanged |