mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 01:15:25 +00:00
A fill or stroke row that the Appearance commands gave a graph label, its object opacity, and the characters the Eyedropper gave it lasted only until the graph was drawn again: the graph stored nothing for a label's rows, and the Eyedropper's character attributes were not stored. - The graph keeps, for value axis numbers, category labels and legend labels, the paint, weight, opacity and blend mode of a label's topmost visible fill and stroke row, their order and place relative to the Characters row, and the label's object opacity and blend mode (labelStyles valuesRows, categoriesRows, legendRows). Every label of the kind gets those rows when the graph is drawn again, with a placed gradient fitted to each label. - Every appearance.* command stores them, and so do graphicStyle.apply and redefine and the paint, stroke, gradient and transparency edits aimed at a row, even when the label already showed them. transparency.set without a row and object.setProps store only the object opacity and blend mode. With several labels of a kind, the last one edited wins. - appearance.copyFrom onto all of a label's text stores the character attributes it applied and, from type, the paragraph attributes; a transparency pick stores the object opacity and blend mode, and append stores the rows. - paint.setFill and paint.setStroke store a character fill or stroke only when the characters take the paint (the whole label, or all of its text highlighted with the Type tool); aimed at a row, they store the row. - Document Color Mode and swatch edits reach the kept rows. Docs: docs/mcp.md, the commands' params text, docs/target-app-parity.md.
138 lines
6.1 KiB
Rust
138 lines
6.1 KiB
Rust
use super::{BlendMode, EngineError, Knockout, Node, Result, Session, Value, bad, check_numbers, check_switches, ok, percent, str_param, targets};
|
|
|
|
/// The most data entries `object.setProps` takes at once.
|
|
const MAX_DATA: usize = 256;
|
|
|
|
/// `object.setProps`'s `data`: each key (a data-* attribute's name: letters, digits, `-`, `_`,
|
|
/// `.`) with its value, or `None` to remove it.
|
|
fn data_param(m: &serde_json::Map<String, Value>) -> Result<Vec<(String, Option<String>)>> {
|
|
const C: &str = "object.setProps";
|
|
if m.len() > MAX_DATA {
|
|
return Err(bad(C, format!("data takes at most {MAX_DATA} keys at once")));
|
|
}
|
|
m.iter()
|
|
.map(|(k, v)| {
|
|
let k = k.strip_prefix("data-").unwrap_or(k);
|
|
if k.is_empty() || !k.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')) || k == "name" || k.starts_with("vc-") {
|
|
return Err(bad(C, format!("data key `{k}`: letters, digits, -, _ and . (not name or vc-…)")));
|
|
}
|
|
let v = match v {
|
|
Value::Null => None,
|
|
Value::String(s) => Some(s.clone()),
|
|
other => Some(other.to_string()),
|
|
};
|
|
Ok((k.to_string(), v))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(super) fn set_props(s: &mut Session, p: &Value) -> Result<Value> {
|
|
check_switches(p, &["visible", "locked", "isolate", "knockoutShape"], "object.setProps")?;
|
|
check_numbers(p, &["opacity"], "object.setProps")?;
|
|
let ids = targets(s, p)?;
|
|
let opacity = p.get("opacity").and_then(Value::as_f64).map(percent);
|
|
let blend = match str_param(p, "blend") {
|
|
Some(b) => Some(BlendMode::parse(b).ok_or_else(|| bad("object.setProps", format!("unknown blend mode `{b}`")))?),
|
|
None => None,
|
|
};
|
|
let data = match p.get("data") {
|
|
None | Some(Value::Null) => None,
|
|
Some(Value::Object(m)) => Some(data_param(m)?),
|
|
Some(v) => return Err(bad("object.setProps", format!("data must be an object of key: value, not {v}"))),
|
|
};
|
|
let knockout = match p.get("knockout") {
|
|
Some(v) => Some(
|
|
Knockout::from_value(v)
|
|
.ok_or_else(|| bad("object.setProps", format!("knockout must be \"on\", \"off\", \"neutral\" or a bool, not {v}")))?,
|
|
),
|
|
None => None,
|
|
};
|
|
let hides = p.get("visible").and_then(Value::as_bool) == Some(false) || p.get("locked").and_then(Value::as_bool) == Some(true);
|
|
// When every object already has every value given, the document and its undo history stay as
|
|
// they are.
|
|
let flag = |k: &str| p.get(k).and_then(Value::as_bool);
|
|
let has_values = |n: &Node| {
|
|
str_param(p, "name").is_none_or(|v| n.name.as_deref() == Some(v).filter(|v| !v.is_empty()))
|
|
&& flag("visible").is_none_or(|v| n.visible == v)
|
|
&& flag("locked").is_none_or(|v| n.locked == v)
|
|
&& opacity.is_none_or(|v| n.opacity == v)
|
|
&& blend.is_none_or(|b| n.blend == b)
|
|
&& flag("isolate").is_none_or(|v| n.isolate == v)
|
|
&& knockout.is_none_or(|k| n.knockout == k)
|
|
&& flag("knockoutShape").is_none_or(|v| n.knockout_shape == v)
|
|
&& data.iter().flatten().all(|(k, v)| {
|
|
n.extras().attrs.as_ref().and_then(|a| a.data.iter().find(|(key, _)| key == k)).map(|(_, now)| now.as_str()) == v.as_deref()
|
|
})
|
|
};
|
|
let d = &s.doc()?.doc;
|
|
// A graph keeps the object opacity and blend mode set on a mark of a series for the whole series, and set on a label
|
|
// for its kind of label, so a value a mark or label already has still replaces the one another left on the graph.
|
|
let transparency = opacity.is_some() || blend.is_some();
|
|
let stores =
|
|
|| transparency && (crate::cmd::graph::series_object_opacity_differs(d, &ids) || crate::cmd::graph::label_object_opacity_differs(d, &ids));
|
|
if ids.iter().all(|id| d.node(*id).is_some_and(has_values)) && !stores() {
|
|
// Hidden or locked objects can't stay selected.
|
|
if hides {
|
|
s.select(|d, sel| sel.deselect_uneditable(d))?;
|
|
}
|
|
return ok();
|
|
}
|
|
s.edit("Object Properties", |d, sel| {
|
|
for id in &ids {
|
|
let n = d.node_mut(*id).ok_or(EngineError::NoNode(*id))?;
|
|
if let Some(v) = str_param(p, "name") {
|
|
n.name = if v.is_empty() { None } else { Some(v.to_string()) };
|
|
}
|
|
if let Some(v) = p.get("visible").and_then(Value::as_bool) {
|
|
n.visible = v;
|
|
}
|
|
if let Some(v) = p.get("locked").and_then(Value::as_bool) {
|
|
n.locked = v;
|
|
}
|
|
if let Some(v) = opacity {
|
|
n.opacity = v;
|
|
}
|
|
if let Some(b) = blend {
|
|
n.blend = b;
|
|
}
|
|
if let Some(v) = p.get("isolate").and_then(Value::as_bool) {
|
|
n.isolate = v;
|
|
}
|
|
if let Some(k) = knockout {
|
|
n.knockout = k;
|
|
}
|
|
if let Some(v) = p.get("knockoutShape").and_then(Value::as_bool) {
|
|
n.knockout_shape = v;
|
|
}
|
|
if let Some(changes) = &data {
|
|
n.edit_attrs(|a| {
|
|
for (k, v) in changes {
|
|
match (a.data.iter().position(|(key, _)| key == k), v) {
|
|
(Some(i), Some(v)) => {
|
|
if let Some(e) = a.data.get_mut(i) {
|
|
e.1 = v.clone();
|
|
}
|
|
}
|
|
(Some(i), None) => {
|
|
a.data.remove(i);
|
|
}
|
|
(None, Some(v)) => a.data.push((k.clone(), v.clone())),
|
|
(None, None) => {}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if transparency {
|
|
crate::cmd::graph::capture_series_object_opacity(d, &ids);
|
|
crate::cmd::graph::capture_label_object_opacity(d, &ids);
|
|
}
|
|
// Hidden or locked objects can't stay selected.
|
|
if hides {
|
|
sel.deselect_uneditable(d);
|
|
}
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|