//! Commands the panels need beyond the core set: artboard reorder, duplicate, copy and cut, and the advanced //! Character/Paragraph attributes. use serde_json::{Value, json}; use vectorcraft_doc::{Composer, JustificationOptions, NodeKind, Scaling, SingleWordJustify}; use vectorcraft_geom::{Rect, Vec2}; use super::*; use crate::{Clipboard, EngineError}; pub fn specs() -> Vec { vec![ cmd!("artboard.reorder", "Move Artboard Up/Down", [], None, "{index, to} change an artboard's number", has_doc, artboard_reorder), cmd!( "artboard.duplicate", "Duplicate Artboard", ["Window", "Artboards"], None, "{index?: n | indices?: [n…] (default: the artboards the Artboard tool has selected), art?: bool (default: the Artboard tool's moveArt option), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} a copy of each artboard, the copies placed together right of the last artboard as they lay, with copies of the art fully inside each when `art` (locked and hidden art only with prefs moveLockedWithArtboard), in one undo step → {index: the first copy's index, indices: the copies' indices, ids: the art's copies}", has_doc, artboard_duplicate ), cmd!( "artboard.copy", "Copy Artboard", [], None, "{index? | indices?, art?} (defaults as artboard.duplicate) put the artboards, and the art fully inside each when `art`, on the clipboard: edit.paste adds a copy of each, together right of the last artboard as they lay, edit.pasteInPlace/InFront/InBack where they were, in this or another document (one undo step; edit.pasteOnAllArtboards pastes only the art); edit.copy runs this while the Artboard tool is chosen → {copied: objects}", has_doc, |s, p| artboard_copy(s, p, false) ), cmd!( "artboard.cut", "Cut Artboard", [], None, "{index? | indices?, art?} artboard.copy, then delete the artboards and that art in one undo step (one artboard is always left: the cut fails when it names every artboard); edit.cut runs this while the Artboard tool is chosen → {copied: objects}", has_doc, |s, p| artboard_copy(s, p, true) ), cmd!( "text.setFormat", "Character / Paragraph", [], None, "{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), wordSpacing?: % 0..1000 (80, 100, 133), letterSpacing?: % of a word space -100..500 (0, 0, 0), glyphScaling?: % 50..200 (100, 100, 100): each [min, desired, max] or {min?, desired?, max?} (Paragraph panel menu › Justification…: Desired applies to every line, Minimum and Maximum bound what justified lines may use; values are clamped to their range, and Minimum ≤ Desired ≤ Maximum must hold afterwards or nothing changes), autoLeading?: % of the size 0..500 (120; the leading of characters whose leading is Auto), singleWordJustification?: \"fullJustify\"|\"alignLeft\"|\"alignCenter\"|\"alignRight\" (a justified line holding one word), mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)", has_doc, set_format ), ] } // ---------- artboards ---------- fn artboard_reorder(s: &mut Session, p: &Value) -> Result { const C: &str = "artboard.reorder"; let i = artboard_index(&s.doc()?.doc, p, C)?; let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(C, "missing to"))? as usize; s.edit("Reorder Artboards", |d, _| { if i >= d.artboards.len() { return Err(EngineError::Other("no such artboard".into())); } let a = d.artboards.remove(i); let to = to.min(d.artboards.len()); d.artboards.insert(to, a); Ok(()) })?; ok() } /// The artboards (`index` or `indices`) and whether their art comes along (`art`) for the artboard /// copy commands: by default the artboards the Artboard tool has selected (its active one when none /// of them is in the document) and its Move/Copy Artwork with Artboard option. fn boards_and_art(s: &Session, p: &Value, cmd: &str) -> Result<(Vec, bool)> { let t = s.tool_options_of("artboard"); let d = &s.doc()?.doc; let indices = if p.get("index").is_some() || p.get("indices").is_some() { artboard_indices(d, p, cmd)? } else { let mut picked: Vec = t["selected"] .as_array() .into_iter() .flatten() .filter_map(Value::as_u64) .filter(|i| usize::try_from(*i).is_ok_and(|i| i < d.artboards.len())) .collect(); if picked.is_empty() { picked.push(t["active"].as_u64().unwrap_or(0)); } artboard_indices(d, &json!({ "indices": picked }), cmd)? }; Ok((indices, bool_or(p, "art", t["moveArt"].as_bool().unwrap_or(true)))) } /// The one artboard `cmd` acts on: `index`, an artboard of `d`, with the errors of /// [`artboard_indices`] (`missing index`, `no such artboard`). pub(crate) fn artboard_index(d: &vectorcraft_doc::Document, p: &Value, cmd: &str) -> Result { let i = p.get("index").ok_or_else(|| bad(cmd, "missing index"))?; artboard_indices(d, &json!({ "index": i }), cmd)?.into_iter().next().ok_or_else(|| EngineError::Other("no such artboard".into())) } /// The artboards `cmd` acts on: `indices` (several), else `index` (one), each an artboard of `d`, /// in order and once each. pub(crate) fn artboard_indices(d: &vectorcraft_doc::Document, p: &Value, cmd: &str) -> Result> { let listed = match (p.get("indices"), p.get("index")) { (Some(v), _) => point_indices(v), (None, Some(i)) => point_indices(&json!([i])), (None, None) => return Err(bad(cmd, "missing index")), }; let mut indices = listed.filter(|v| !v.is_empty() && v.iter().all(|i| *i < d.artboards.len())).ok_or_else(|| EngineError::Other("no such artboard".into()))?; indices.sort_unstable(); indices.dedup(); Ok(indices) } /// The art that goes with artboard `i` when `art`: the objects fully inside it (locked and hidden /// ones only with prefs moveLockedWithArtboard), in paint order. pub(crate) fn artboard_art(s: &Session, i: usize, art: bool) -> Result> { let d = &s.doc()?.doc; let rect = d.artboards.get(i).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?; Ok(if art { d.art_on_artboard(rect, s.prefs.move_locked_with_artboard) } else { vec![] }) } /// Each of `indices` with the art that goes with it when `art` ([`artboard_art`]), an object /// counted once, with the first of them it is on (artboards can overlap). pub(crate) fn artboards_art(s: &Session, indices: &[usize], art: bool) -> Result)>> { let mut seen = std::collections::HashSet::new(); let mut arts = vec![]; for &i in indices { let art = artboard_art(s, i, art)?.into_iter().filter(|id| seen.insert(*id)).collect(); arts.push((i, art)); } Ok(arts) } /// How far a copy of `rect` moves to sit right of the last artboard (Duplicate Artboards, Paste). pub(crate) fn beside_artboards(d: &vectorcraft_doc::Document, rect: Rect) -> Vec2 { let right = d.artboards.iter().map(|a| a.rect.x1).fold(f64::MIN, f64::max); Vec2::new(right + 20.0 - rect.x0, 0.0) } /// Duplicate Artboards: a copy of each artboard with its art, in one undo step. The copies keep /// their places relative to each other, together right of the last artboard. fn artboard_duplicate(s: &mut Session, p: &Value) -> Result { let (indices, art) = boards_and_art(s, p, "artboard.duplicate")?; let arts = artboards_art(s, &indices, art)?; let all: Vec = arts.iter().flat_map(|(_, a)| a.iter().copied()).collect(); let sc = Scaling { patterns: super::object::transform_patterns(s, p, &all)?, ..Scaling::default() }; let (index, ids) = s.edit("Duplicate Artboard", |d, _| { let dv = arts .iter() .filter_map(|(i, _)| d.artboards.get(*i)) .map(|a| a.rect) .reduce(|a, b| a.union(b)) .map(|r| beside_artboards(d, r)) .unwrap_or_default(); let (mut index, mut ids) = (vec![], vec![]); for (i, art) in &arts { let (at, copies) = copy_artboard(d, *i, dv, art, sc)?; index.push(at); ids.extend(copies); } Ok((index, ids)) })?; Ok(json!({"index": index.first(), "indices": index, "ids": ids.iter().map(|i| i.0).collect::>()})) } /// Artboard Copy / Cut: the artboards and their art onto the clipboard (pasting brings them all); /// a cut then deletes them. pub(crate) fn artboard_copy(s: &mut Session, p: &Value, cut: bool) -> Result { let (indices, art) = boards_and_art(s, p, if cut { "artboard.cut" } else { "artboard.copy" })?; let arts = artboards_art(s, &indices, art)?; let st = s.doc()?; if cut && st.doc.artboards.len() <= indices.len() { return Err(EngineError::Other("a document needs at least one artboard".into())); } let art = st.doc.paint_order(arts.into_iter().flat_map(|(_, a)| a)); let boards: Vec<_> = indices.iter().filter_map(|i| st.doc.artboards.get(*i).cloned()).collect(); let clip = Clipboard { source_artboard: boards.first().map(|a| a.rect), artboards: boards, ..Clipboard::copy(st, &art) }; let copied = clip.nodes.len(); if cut { s.edit("Cut Artboard", |d, sel| { if indices.iter().any(|i| *i >= d.artboards.len()) { return Err(EngineError::Other("no such artboard".into())); } // The last first, so the others keep their places; their guides go with them. for &i in indices.iter().rev() { let gone = d.artboards.remove(i).id; d.retain_guides(sel, |_, g| g.artboard != Some(gone)); } d.remove_many(&art)?; sel.prune(d); Ok(()) })?; if s.tool_id() == "artboard" { s.set_tool_option("active", &json!(indices.first().map_or(0, |i| i.saturating_sub(1)))); } } s.clipboard = clip; Ok(json!({"copied": copied})) } /// Add a copy of artboard `i` moved by `dv`, with copies of its guides and of its `art` (each just /// above its original) moved along → (the copy's index, the art's copies). When `sc.patterns`, the /// pattern tiles of the art's copies move with them. Duplicate Artboards and Alt-drag. pub(crate) fn copy_artboard(d: &mut vectorcraft_doc::Document, i: usize, dv: Vec2, art: &[NodeId], sc: Scaling) -> Result<(usize, Vec)> { let src = d.artboards.get(i).cloned().ok_or_else(|| EngineError::Other("no such artboard".into()))?; let index = push_artboard_copy(d, &src, src.rect + dv); // Its guides come along. if let Some(id) = d.artboards.get(index).map(|a| a.id) { d.copy_artboard_guides(src.id, id, dv); } let copies = super::edit::copy_above(d, art, |_, _, c| c.transform(Affine::translate(dv), sc))?; Ok((index, copies)) } /// Add a copy of artboard `src` at `rect`, with an id of its own and its name, or ` copy` /// (` copy 2`…) when that is taken → its index. pub(crate) fn push_artboard_copy(d: &mut vectorcraft_doc::Document, src: &vectorcraft_doc::Artboard, rect: Rect) -> usize { let mut a = src.clone(); a.id = d.next_artboard_id(); let taken = |name: &str| d.artboards.iter().any(|a| a.name == name); a.name = [src.name.clone(), format!("{} copy", src.name)] .into_iter() .chain((2u64..).map(|i| format!("{} copy {i}", src.name))) .find(|name| !taken(name)) .unwrap_or_default(); a.rect = rect; d.artboards.push(a); d.artboards.len() - 1 } // ---------- text ---------- /// Largest indent or paragraph spacing (points; Illustrator's limit). const MAX_PARA: f64 = 1296.0; /// A justification spacing of `text.setFormat` (`wordSpacing`, `letterSpacing`, `glyphScaling`): /// `[min, desired, max]` or `{min?, desired?, max?}`, in percent. The values not given are kept. fn spacing_param(p: &Value, key: &str, c: &str) -> Result; 3]>> { let number = |v: &Value| v.as_f64().filter(|v| v.is_finite()); let err = || bad(c, format!("`{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent")); match p.get(key) { None | Some(Value::Null) => Ok(None), Some(Value::Array(a)) if a.len() == 3 => { let mut out = [None; 3]; for (o, v) in out.iter_mut().zip(a) { *o = Some(number(v).ok_or_else(err)?); } Ok(Some(out)) } Some(Value::Object(o)) => { let mut out = [None; 3]; for (k, v) in o { let i = ["min", "desired", "max"].iter().position(|n| n == k).ok_or_else(err)?; if let Some(slot) = out.get_mut(i) { *slot = Some(number(v).ok_or_else(err)?); } } Ok(Some(out)) } Some(_) => Err(err()), } } fn set_format(s: &mut Session, p: &Value) -> Result { const C: &str = "text.setFormat"; let ids = super::typecmd::text_targets(s, p, C)?; let num = |k: &str| p.get(k).and_then(Value::as_f64); let flag = |k: &str| p.get(k).and_then(Value::as_bool); let kerning = match p.get("kerning") { None | Some(Value::Null) => None, Some(Value::String(a)) if a.eq_ignore_ascii_case("auto") => Some(None), Some(v) => Some(Some(v.as_f64().ok_or_else(|| bad(C, "kerning must be a number or \"auto\""))?.clamp(-1000.0, 10000.0))), }; let keys = [ "kerning", "baselineShift", "hScale", "vScale", "rotation", "underline", "strikethrough", "allCaps", "smallCaps", "position", "leftIndent", "rightIndent", "firstLineIndent", "spaceBefore", "spaceAfter", "hyphenate", "wordsLongerThan", "afterFirst", "beforeLast", "hyphenLimit", "hyphenationZone", "hyphenateCapitalized", "wordSpacing", "letterSpacing", "glyphScaling", "autoLeading", "singleWordJustification", "mojikumi", "kinsoku", "direction", "leadingModel", "charAlign", "proportionalMetrics", "digits", "fontAxes", "burasagari", "romanHangingPunctuation", "composer", ]; if !keys.iter().any(|k| p.get(*k).is_some()) { return Err(bad(C, "nothing to change")); } let composer = match p.get("composer") { None | Some(Value::Null) => None, Some(v) => Some(match v.as_str() { Some("singleLine") => Composer::SingleLine, Some("everyLine") => Composer::EveryLine, _ => return Err(bad(C, "composer must be \"singleLine\" or \"everyLine\"")), }), }; let (position, small_caps) = super::docsetup::script_params(p, &s.doc()?.doc.setup, C)?; let char_align = super::textedit::char_align_param(p, C)?; let digits = super::textedit::digits_param(p, C)?; let font_axes = super::textedit::font_axes_param(p, C)?; let mojikumi = match p.get("mojikumi") { None => None, Some(v) => Some(match v.as_str() { Some("none") => vectorcraft_doc::Mojikumi::None, Some("lineEndHalf") => vectorcraft_doc::Mojikumi::LineEndHalf, _ => return Err(bad(C, "`mojikumi` must be \"none\" or \"lineEndHalf\"")), }), }; let kinsoku = match p.get("kinsoku") { None => None, Some(v) => Some(match v.as_str() { Some("none") => vectorcraft_doc::Kinsoku::None, Some("hard") => vectorcraft_doc::Kinsoku::Hard, Some("soft") => vectorcraft_doc::Kinsoku::Soft, _ => return Err(bad(C, "`kinsoku` must be \"none\", \"hard\" or \"soft\"")), }), }; let direction = match p.get("direction") { None => None, Some(v) => Some(match v.as_str() { Some("auto") => None, Some("leftToRight") => Some(vectorcraft_doc::ParaDirection::LeftToRight), Some("rightToLeft") => Some(vectorcraft_doc::ParaDirection::RightToLeft), _ => return Err(bad(C, "`direction` must be \"auto\", \"leftToRight\" or \"rightToLeft\"")), }), }; let leading_model = match p.get("leadingModel") { None => None, Some(v) => Some(match v.as_str() { Some("romanBaseline") => vectorcraft_doc::LeadingModel::RomanBaseline, Some("emBoxTop") => vectorcraft_doc::LeadingModel::EmBoxTop, _ => return Err(bad(C, "`leadingModel` must be \"romanBaseline\" or \"emBoxTop\"")), }), }; let burasagari = match p.get("burasagari") { None => None, Some(v) => Some(match v.as_str() { Some("none") => vectorcraft_doc::Burasagari::None, Some("standard") => vectorcraft_doc::Burasagari::Standard, Some("forced") => vectorcraft_doc::Burasagari::Forced, _ => return Err(bad(C, "`burasagari` must be \"none\", \"standard\" or \"forced\"")), }), }; let spacing = |k: &str| spacing_param(p, k, C); let (word_spacing, letter_spacing, glyph_scaling) = (spacing("wordSpacing")?, spacing("letterSpacing")?, spacing("glyphScaling")?); let single_word = match p.get("singleWordJustification") { None => None, Some(v) => Some(match v.as_str() { Some("fullJustify") => SingleWordJustify::FullJustify, Some("alignLeft") => SingleWordJustify::AlignLeft, Some("alignCenter") => SingleWordJustify::AlignCenter, Some("alignRight") => SingleWordJustify::AlignRight, _ => return Err(bad(C, "`singleWordJustification` must be \"fullJustify\", \"alignLeft\", \"alignCenter\" or \"alignRight\"")), }), }; let auto_leading = match p.get("autoLeading") { None => None, Some(v) => Some(v.as_f64().filter(|v| v.is_finite()).ok_or_else(|| bad(C, "`autoLeading` must be a number"))?), }; // A paragraph whose Minimum, Desired and Maximum would no longer be in order. let mut out_of_order: Option<&str> = None; let range = super::typecmd::TextRange::parse(p, C)?; // The character attributes, given the baseline shift and horizontal scale in the type's own units. let chars = |st: &mut vectorcraft_doc::CharStyle, baseline_shift: Option, h_scale: Option| { if let Some(k) = kerning { st.kerning = k; } if let Some(v) = baseline_shift { st.baseline_shift = v; } if let Some(v) = h_scale { st.h_scale = v; } if let Some(v) = num("vScale") { st.v_scale = v.clamp(1.0, 10000.0); } if let Some(v) = num("rotation") { st.rotation = ((v + 180.0).rem_euclid(360.0)) - 180.0; } if let Some(v) = flag("underline") { st.underline = v; } if let Some(v) = flag("strikethrough") { st.strikethrough = v; } if let Some(v) = flag("allCaps") { st.all_caps = v; } if let Some(v) = position { st.position = v; } if let Some(v) = small_caps { st.small_caps = v; } if let Some(v) = char_align { st.char_align = v; } if let Some(v) = digits { st.digits = v; } if let Some(v) = flag("proportionalMetrics") { st.proportional_metrics = v; } if let Some(v) = &font_axes { super::textedit::change_font_axes(st, v); } }; s.edit("Character", |d, _| { for id in &ids { let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue }; let baseline_shift = num("baselineShift").map(|v| super::typecmd::local_type_value(t, v, (-1296.0, 1296.0), false, C)).transpose()?; let h_scale = num("hScale").map(|v| super::typecmd::local_type_value(t, v, (1.0, 10000.0), true, C)).transpose()?; super::typecmd::style_chars(t, range, |st| chars(st, baseline_shift, h_scale)); let span = super::typecmd::para_span(range, t); t.edit_paras(span, |para| { if let Some(v) = num("leftIndent") { para.left_indent = v.clamp(-MAX_PARA, MAX_PARA); } if let Some(v) = num("rightIndent") { para.right_indent = v.clamp(-MAX_PARA, MAX_PARA); } if let Some(v) = num("firstLineIndent") { para.first_line_indent = v.clamp(-MAX_PARA, MAX_PARA); } if let Some(v) = num("spaceBefore") { para.space_before = v.clamp(-MAX_PARA, MAX_PARA); } if let Some(v) = num("spaceAfter") { para.space_after = v.clamp(-MAX_PARA, MAX_PARA); } if let Some(v) = flag("hyphenate") { para.hyphenate = v; } let letters = |k: &str, lo: f64, hi: f64| num(k).map(|v| v.round().clamp(lo, hi) as u32); if let Some(v) = letters("wordsLongerThan", 1.0, 25.0) { para.hyphenation.words_longer_than = v; } if let Some(v) = letters("afterFirst", 1.0, 15.0) { para.hyphenation.after_first = v; } if let Some(v) = letters("beforeLast", 1.0, 15.0) { para.hyphenation.before_last = v; } if let Some(v) = letters("hyphenLimit", 0.0, 25.0) { para.hyphenation.hyphen_limit = v; } if let Some(v) = num("hyphenationZone") { para.hyphenation.zone = v.clamp(0.0, MAX_PARA); } if let Some(v) = flag("hyphenateCapitalized") { para.hyphenation.capitalized = v; } let j = &mut para.justification; for (name, given, value, limits) in [ ("wordSpacing", &word_spacing, &mut j.word_spacing, JustificationOptions::WORD_SPACING), ("letterSpacing", &letter_spacing, &mut j.letter_spacing, JustificationOptions::LETTER_SPACING), ("glyphScaling", &glyph_scaling, &mut j.glyph_scaling, JustificationOptions::GLYPH_SCALING), ] { let Some(given) = given else { continue }; for (v, g) in value.iter_mut().zip(given) { if let Some(g) = g { *v = g.clamp(limits.0, limits.1); } } if !(value[0] <= value[1] && value[1] <= value[2]) { out_of_order.get_or_insert(name); } } if let Some(v) = auto_leading { j.auto_leading = v.clamp(JustificationOptions::AUTO_LEADING.0, JustificationOptions::AUTO_LEADING.1); } if let Some(v) = single_word { j.single_word = v; } if let Some(v) = mojikumi { para.mojikumi = v; } if let Some(v) = kinsoku { para.kinsoku = v; } if let Some(v) = direction { para.direction = v; } if let Some(v) = leading_model { para.leading_model = v; } if let Some(v) = burasagari { para.burasagari = v; } if let Some(v) = flag("romanHangingPunctuation") { para.roman_hanging = v; } if let Some(v) = composer { para.composer = v; } }); if let Some(name) = out_of_order { return Err(bad(C, format!("`{name}`: Minimum must not exceed Desired, nor Desired Maximum"))); } super::typecmd::refresh_bounds(t); } // Graph labels keep a change made to all of their text through the graph being drawn again. if range.is_none() { let shift = num("baselineShift").map(|v| v.clamp(-1296.0, 1296.0)); let scale = num("hScale").map(|v| v.clamp(1.0, 10000.0)); super::graph::capture_label_styles(d, &ids, &|st| chars(st, shift, scale)); } Ok(()) })?; Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::>() })) }