mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
dataset.select applied a row's values in the order the variables were defined, and it skips an object inside a hidden group. When a row showed a group and set an object inside it, and the object's variable was defined first, the object was skipped while the group was still hidden: one selection applied the row in part, and selecting the row again applied the rest. A text variable on type inside the group was skipped the same way, and a row that hid a group skipped the objects inside it whose variables were defined after the group's. dataset.select, dataset.next and dataset.prev now apply the visibility values that show objects first, outermost object first, then the other values in definition order, then the visibility values that hide objects, innermost object first. Of two variables of the same kind on one object, the one defined last still wins. Docs: docs/mcp.md.
210 lines
8.8 KiB
Rust
210 lines
8.8 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 the text values 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. Of two visibility variables on one object, the
|
|
/// last one defined wins too.
|
|
#[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
|
|
}
|
|
}
|