mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 17:06:53 +00:00
An object opacity or blend mode set on a path directly in a graph series, and the blend mode of its topmost visible fill or stroke, were lost when the graph was drawn again. The graph now stores them per series next to the row opacity it already stores, and puts them on every path directly in the series when it draws the series again. - transparency.set and object.setProps store the object's opacity and blend mode. A value the mark already has still replaces the value another mark of the series stored. - appearance.clear, appearance.transfer, appearance.copyFrom with Transparency picked up, graphicStyle.apply without add and graphicStyle.redefine store the object's opacity and blend mode they leave on the mark. - The other Appearance and style commands store them when they change them on a mark. - Every command that stores the opacity of the topmost visible fill or stroke also stores its blend mode. A Filled Lines band's outline takes the stored stroke opacity and blend mode and keeps its black 0.25 pt stroke. - An opacity and blend mode are stored as a pair from the edited mark: setting only the opacity of one mark also stores that mark's blend mode. - A graph design's copy in a series keeps its own values. Docs: docs/mcp.md, docs/target-app-parity.md.
136 lines
6.0 KiB
Rust
136 lines
6.0 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, so a value a mark
|
|
// already has still replaces the one another mark left on the graph.
|
|
let transparency = opacity.is_some() || blend.is_some();
|
|
let stores = || transparency && crate::cmd::graph::series_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);
|
|
}
|
|
// Hidden or locked objects can't stay selected.
|
|
if hides {
|
|
sel.deselect_uneditable(d);
|
|
}
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|