Files
vectorcraft/crates/doc/src/variables.rs
T
dotCooCoo 00b02c1bdb Variables: Update Data Set writes only the variables the active data set holds
A data set can hold values for only some variables: `dataset.new` and
`dataset.set` store only the values they are given, and selecting the
data set leaves the other variables alone. Update Data Set replaced the
active data set's values with the values the art reports for every
variable. After one Update, a data set that set only a visibility
variable also held the current text of the bound type, and selecting it
again wrote that text back. Update also dropped a variable's value when
its objects were locked or inside a hidden or locked layer or group,
because such objects report no value.

`dataset.update` now writes the art's current value for each variable
the active data set holds, and keeps the data set's value when the art
reports none. A variable that the data set does not hold stays out of
it, including one defined after the data set was captured;
`dataset.update {all: true}` also adds the other variables that the art
reports a value for. The reply adds `left`, the variables it did not
write. The Variables panel's Update Data Set button and menu item run
it without `all`.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-16 02:26:08 +07:00

209 lines
8.7 KiB
Rust

//! Variables (data merge): named values bound to objects, with datasets that apply
//! them all at once. One template document produces many variants (badges, price
//! lists, localized copies) by selecting a dataset instead of editing each object.
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use super::{Document, NodeId, NodeKind};
/// What a variable drives.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VariableKind {
/// A type object's characters.
#[default]
Text,
/// Whether bound objects show.
Visibility,
}
impl VariableKind {
/// Parse the `kind` of `variable.define`: `text` or `visibility` (`visible` is accepted
/// as the same thing, as the panel and the docs call it both).
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"text" => Some(Self::Text),
"visibility" | "visible" => Some(Self::Visibility),
_ => None,
}
}
/// The kind's own name, as `variable.define` takes it and `variable.list` reports it.
pub fn label(self) -> &'static str {
match self {
Self::Text => "text",
Self::Visibility => "visibility",
}
}
}
/// One named variable.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Variable {
pub name: String,
pub kind: VariableKind,
}
/// One value a dataset gives a variable.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DataValue {
Text(String),
Visible(bool),
}
impl DataValue {
/// Whether this value fits the variable's kind (a dataset may carry values for
/// variables of either kind; each binding takes what fits it).
pub fn fits(&self, kind: VariableKind) -> bool {
matches!((self, kind), (Self::Text(_), VariableKind::Text) | (Self::Visible(_), VariableKind::Visibility))
}
}
/// One named row of values, applied with `dataset.select`.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct DataSet {
pub name: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub values: BTreeMap<String, DataValue>,
}
impl DataSet {
/// Whether the art, as [`Document::captured_values`] read it, still holds what this row
/// says. A value the art can't report (its object is locked or gone) isn't drift: the row
/// keeps it, and applying it again would skip it too.
pub fn matches(&self, current: &BTreeMap<String, DataValue>) -> bool {
self.values.iter().all(|(k, v)| current.get(k).is_none_or(|c| c == v))
}
}
/// The variable state of a document: definitions, datasets, bindings
/// (object id → the variables bound to it) and which dataset is active.
///
/// An object may be driven by several variables — the reference app binds the selected object
/// to the selected variable, one click at a time, and never says a second binding replaces the
/// first — so a binding is a set. `dataset.select` applies them in the order the variables are
/// defined, which is what makes two text variables on one object deterministic: the last one
/// wins, and `applied` counts both.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Variables {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub variables: Vec<Variable>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub datasets: Vec<DataSet>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub bindings: BTreeMap<NodeId, BTreeSet<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_dataset: Option<String>,
}
impl Variables {
/// The variable `name`, if the document defines it.
pub fn variable(&self, name: &str) -> Option<&Variable> {
self.variables.iter().find(|v| v.name == name)
}
/// The dataset `name`, if the document has it.
pub fn dataset(&self, name: &str) -> Option<&DataSet> {
self.datasets.iter().find(|d| d.name == name)
}
/// Bind `id` to the variable `name`. True when it was not bound to it already.
pub fn bind(&mut self, id: NodeId, name: &str) -> bool {
self.bindings.entry(id).or_default().insert(name.to_string())
}
/// Drop `id`'s binding to `name` (and the binding itself when that was its last variable).
/// True when there was one.
pub fn unbind(&mut self, id: NodeId, name: &str) -> bool {
let Some(bound) = self.bindings.get_mut(&id) else { return false };
let dropped = bound.remove(name);
if bound.is_empty() {
self.bindings.remove(&id);
}
dropped
}
/// The objects bound to the variable `name`, in document order.
pub fn objects_of(&self, name: &str) -> Vec<NodeId> {
let mut ids: Vec<NodeId> = self.bindings.iter().filter(|(_, v)| v.contains(name)).map(|(id, _)| *id).collect();
ids.sort_unstable();
ids
}
/// Drop the variables `names`, the dataset values that name them and the bindings to
/// them. Datasets named here are left alone: their names are their own namespace (the
/// active one is dropped by whoever deletes the dataset).
pub fn prune(&mut self, names: &[String]) -> usize {
// A set, so a long list of names costs a lookup per value rather than a scan.
let names: BTreeSet<&str> = names.iter().map(String::as_str).collect();
let before = self.variables.len();
self.variables.retain(|v| !names.contains(v.name.as_str()));
for dataset in &mut self.datasets {
dataset.values.retain(|k, _| !names.contains(k.as_str()));
}
for bound in self.bindings.values_mut() {
bound.retain(|v| !names.contains(v.as_str()));
}
self.bindings.retain(|_, v| !v.is_empty());
before - self.variables.len()
}
}
impl Document {
/// Forget the bindings of objects that are no longer in the document (after every edit),
/// like [`Document::prune_assets`] does for Asset Export's. A binding outliving its object
/// would count as a skip in every dataset application and grow the saved file forever.
pub fn prune_variable_bindings(&mut self) {
if self.variables.bindings.keys().all(|id| self.node(*id).is_some()) {
return;
}
let mut bindings = std::mem::take(&mut self.variables.bindings);
bindings.retain(|id, _| self.node(*id).is_some());
self.variables.bindings = bindings;
}
/// Whether a variable may read and rewrite the object `id`: it is in the document, neither
/// it nor anything above it is locked, and nothing above it is hidden. Its own visibility
/// doesn't count, as that is what a visibility variable drives: a row that hid it must be
/// able to show it again, and a capture must record it hidden.
pub fn variable_target(&self, id: NodeId) -> bool {
self.ancestry(id).is_some_and(|a| a.iter().all(|i| self.node(*i).is_some_and(|n| !n.locked && (n.visible || *i == id))))
}
/// What the object `id` holds for the variable of `kind`: its characters, or whether it
/// shows. `None` when the object is gone, is not of that kind, or is locked or on a hidden
/// or locked layer (the document's value is whatever it was left at).
pub fn variable_value(&self, id: NodeId, kind: VariableKind) -> Option<DataValue> {
if !self.variable_target(id) {
return None;
}
let node = self.node(id)?;
match (kind, &node.kind) {
(VariableKind::Text, NodeKind::Text(t)) => Some(DataValue::Text(t.plain_text())),
(VariableKind::Visibility, _) => Some(DataValue::Visible(node.visible)),
(VariableKind::Text, _) => None,
}
}
/// The values a dataset captured right now: what each bound object holds, in the order the
/// variables are defined. `dataset.capture` writes these into a new row and
/// `dataset.update` into the active one, for the variables that row holds (`all: true` adds
/// the rest); the Variables panel compares the active row with them to tell whether the art
/// still matches what it says.
pub fn captured_values(&self) -> BTreeMap<String, DataValue> {
let mut out = BTreeMap::new();
for v in &self.variables.variables {
// An object driven by several variables of the same kind reads from the first one
// bound, so a capture is one value per variable and applies back the same way.
let value = self.variables.objects_of(&v.name).into_iter().find_map(|id| self.variable_value(id, v.kind));
if let Some(value) = value {
out.insert(v.name.clone(), value);
}
}
out
}
}