From aa71acc29f95d21481226dd77a838ee402815e5b Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:25:12 +0300 Subject: [PATCH] Swatch legend: linked chips, labels that fit, exact Gray Each chip's fill links to its swatch as paint.setFill {swatch} does, so editing a global or spot swatch recolours the legend. Labels are area type the size of the chip that shrinks text to fit. Gray values use the app's own Gray conversion, and fractional column counts are refused. --- crates/engine/src/cmd/swatchlegend.rs | 30 ++++++++++++++------- crates/engine/src/cmd/swatchlegend/tests.rs | 23 +++++++++++++++- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/cmd/swatchlegend.rs b/crates/engine/src/cmd/swatchlegend.rs index e9964de6f..348c9f34a 100644 --- a/crates/engine/src/cmd/swatchlegend.rs +++ b/crates/engine/src/cmd/swatchlegend.rs @@ -4,7 +4,7 @@ use serde_json::{Value, json}; use vectorcraft_color::{Color, Paint, cms::Model}; -use vectorcraft_doc::{Appearance, Node, NodeKind, TextObject}; +use vectorcraft_doc::{Appearance, AreaFit, Node, NodeKind, TextKind, TextObject}; use vectorcraft_geom::{Point, Rect, shapes}; use super::{CommandSpec, Result, Session, bad, cmd, has_doc}; @@ -62,7 +62,8 @@ fn value_lines(c: Color, values: &[String], spot: bool) -> Vec { format!("L={} a={} b={}", l.l.round() as i32, l.a.round() as i32, l.b.round() as i32) } _ => { - let k = match c { + // As Edit Colors › Convert to Grayscale and the Color panel convert it. + let k = match c.in_model(Model::Gray) { Color::Gray { k } => k, _ => 1.0 - c.to_lab().l / 100.0, }; @@ -78,7 +79,11 @@ fn value_lines(c: Color, values: &[String], spot: bool) -> Vec { fn legend(s: &mut Session, p: &Value) -> Result { const C: &str = "swatch.legend"; - let columns = number(p, "columns", 4.0, 1.0..=50.0, C)? as usize; + 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")); + } + let columns = columns as usize; let size = number(p, "size", 72.0, 8.0..=1000.0, C)?; let gap = number(p, "gap", 12.0, 0.0..=1000.0, C)?; let values: Vec = match p.get("values") { @@ -125,17 +130,17 @@ fn legend(s: &mut Session, p: &Value) -> Result { let pad = (size / 12.0).clamp(1.0, 8.0); let style = 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 = swatches.iter().map(|w| 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) in swatches.iter().enumerate() { + for (i, (w, fill)) in swatches.iter().zip(fills).enumerate() { let Some(c) = w.paint.color() else { continue }; let (col, row) = (i % columns, i / columns); let (x, y) = (x0 + col as f64 * (size + gap), y0 + row as f64 * (size + gap)); - let mut chip = Node::path( - d.alloc_id(), - shapes::rectangle(Rect::new(x, y, x + size, y + size)), - Appearance::basic(w.paint.clone(), Paint::None, 0.0), - ); + let mut chip = + Node::path(d.alloc_id(), shapes::rectangle(Rect::new(x, y, x + size, y + size)), Appearance::basic(fill, Paint::None, 0.0)); chip.name = Some(w.name.clone()); children.push(std::sync::Arc::new(chip)); let mut lines = vec![w.name.clone()]; @@ -143,7 +148,12 @@ fn legend(s: &mut Session, p: &Value) -> Result { let mut label_style = style.clone(); label_style.fill = Paint::solid(if c.to_lab().l < 50.0 { Color::WHITE } else { Color::BLACK }); label_style.stroke = Paint::None; - let mut t = TextObject::point(Point::new(x + pad, y + pad + text_size), &lines.join("\n"), label_style); + // Area type the size of the chip (less its inset) that shrinks text to fit, so a long + // name or value line stays on its chip. + let mut t = TextObject::point(Point::new(x + pad, y + pad), &lines.join("\n"), label_style); + 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.system_layout = system_layout; super::typecmd::refresh_bounds(&mut t); diff --git a/crates/engine/src/cmd/swatchlegend/tests.rs b/crates/engine/src/cmd/swatchlegend/tests.rs index 280220798..decee176e 100644 --- a/crates/engine/src/cmd/swatchlegend/tests.rs +++ b/crates/engine/src/cmd/swatchlegend/tests.rs @@ -45,7 +45,7 @@ fn a_legend_draws_a_labelled_chip_per_colour_swatch_in_one_undo_step() { let chips: Vec<&Node> = kids.iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).map(|n| &**n).collect(); let doc = &s.doc().unwrap().doc; for (chip, w) in chips.iter().zip(doc.swatches_iter().filter(|w| w.paint.color().is_some())) { - assert_eq!(chip.appearance.fill_paint(), w.paint, "{}", w.name); + assert_eq!(chip.appearance.fill_paint().color(), w.paint.color(), "{}", w.name); assert_eq!(chip.name.as_deref(), Some(w.name.as_str())); } let b = |i: usize| chips[i].geometric_bounds().unwrap(); @@ -82,6 +82,7 @@ fn a_legend_takes_names_values_and_a_position() { json!({"names": "Signal"}), json!({"values": ["xyz"]}), json!({"columns": 0}), + json!({"columns": 2.5}), json!({"size": 2}), json!({"gap": -1}), json!({"x": f64::MAX}), @@ -91,3 +92,23 @@ fn a_legend_takes_names_values_and_a_position() { } assert_eq!(s.doc().unwrap().history.undo.len(), before); } + +/// A chip of a global swatch is linked to it: editing the swatch recolours the chip. Each label is +/// area type inside its chip that shrinks a long name to fit (#4919 review). +#[test] +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 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); + let kids = g.children().unwrap(); + let chip = kids.iter().find(|n| matches!(n.kind, NodeKind::Path { .. })).unwrap(); + assert_eq!(chip.appearance.fill_paint().color().map(|c| c.to_hex()), Some("#a02050".into()), "the edit reached the chip"); + let label = kids.iter().find(|n| matches!(n.kind, NodeKind::Text(_))).unwrap(); + let NodeKind::Text(t) = &label.kind else { unreachable!() }; + assert!(matches!(t.kind, vectorcraft_doc::TextKind::Area { .. }) && matches!(t.area.fit, vectorcraft_doc::AreaFit::ShrinkText { .. })); + let (c, l) = (chip.geometric_bounds().unwrap(), label.geometric_bounds().unwrap()); + assert!(l.x0 >= c.x0 - 1e-6 && l.y0 >= c.y0 - 1e-6 && l.x1 <= c.x1 + 1e-6 && l.y1 <= c.y1 + 1e-6, "label {l:?} inside chip {c:?}"); +}