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:?}"); +}