mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 09:33:54 +00:00
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.
This commit is contained in:
@@ -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<String> {
|
||||
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<String> {
|
||||
|
||||
fn legend(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<String> = match p.get("values") {
|
||||
@@ -125,17 +130,17 @@ fn legend(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<Paint> = 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<Value> {
|
||||
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);
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user