G6: image variables swap linked files and keep bounds

Add VariableKind::Image with path values on datasets, makeImageDynamic,
panel/dialog UI, and MCP docs so data merge can drive placed images.
This commit is contained in:
Fabio William Conceição
2026-10-16 04:45:06 +07:00
committed by bflatastic
parent c8310edea1
commit 41c8bd2a2d
9 changed files with 161 additions and 43 deletions
+34 -8
View File
@@ -1,9 +1,10 @@
//! Variables (data merge): named values bound to objects, applied per dataset.
//!
//! One template document produces many variants: bind a type object to a `text`
//! variable and anything to a `visibility` variable, put a row of values in each
//! dataset, and `dataset.select` swaps the whole document to that row in one undo
//! step. Agents reach every command through `run_command`, like all engine commands.
//! variable, anything to a `visibility` variable, and a linked image to an `image`
//! variable, put a row of values in each dataset, and `dataset.select` swaps the
//! whole document to that row in one undo step. Agents reach every command through
//! `run_command`, like all engine commands.
use std::collections::{BTreeMap, BTreeSet};
@@ -50,7 +51,8 @@ fn names_param(p: &Value, cmd: &str, fallback: Option<String>, nothing: &str) ->
}
/// Refuse what a variable of `kind` can't drive: art that is locked or on a hidden or locked
/// layer is not the document's to rewrite, and a text variable only drives type.
/// layer is not the document's to rewrite; a text variable only drives type; an image variable
/// only drives an image object.
fn check_bindable(doc: &vectorcraft_doc::Document, ids: &[NodeId], kind: VariableKind, cmd: &str) -> Result<()> {
for id in ids {
match doc.node(*id) {
@@ -59,6 +61,9 @@ fn check_bindable(doc: &vectorcraft_doc::Document, ids: &[NodeId], kind: Variabl
Some(n) if kind == VariableKind::Text && !matches!(n.kind, NodeKind::Text(_)) => {
return Err(bad(cmd, format!("node {} is not type", id.0)));
}
Some(n) if kind == VariableKind::Image && !matches!(n.kind, NodeKind::Image(_)) => {
return Err(bad(cmd, format!("node {} is not an image", id.0)));
}
Some(_) => {}
}
}
@@ -83,6 +88,8 @@ fn dataset_value(cmd: &str, var: &str, kind: VariableKind, v: &Value) -> Result<
match kind {
VariableKind::Text => v.as_str().map(|t| DataValue::Text(t.to_string())),
VariableKind::Visibility => v.as_bool().map(DataValue::Visible),
// An image value is a path string; empty is not a file to open.
VariableKind::Image => v.as_str().map(str::trim).filter(|t| !t.is_empty()).map(|t| DataValue::Image(t.to_string())),
}
.ok_or_else(|| bad(cmd, format!("value for `{var}` must match its variable kind")))
}
@@ -104,7 +111,7 @@ fn values_param(vars: &Variables, p: &Value, cmd: &str) -> Result<BTreeMap<Strin
fn define(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "variable.define";
let name = var_name(p, C)?;
let kind = str_param(p, "kind").and_then(VariableKind::parse).ok_or_else(|| bad(C, "`kind` must be `text` or `visibility`"))?;
let kind = str_param(p, "kind").and_then(VariableKind::parse).ok_or_else(|| bad(C, "`kind` must be `text`, `visibility` or `image`"))?;
if s.doc()?.doc.variables.variable(&name).is_some() {
return Err(bad(C, format!("variable `{name}` is already defined")));
}
@@ -147,7 +154,7 @@ fn rename(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"from": from, "name": to}))
}
/// Make Text Dynamic / Make Visibility Dynamic: define a variable of that kind and bind the
/// Make Text / Visibility / Image Dynamic: define a variable of that kind and bind the
/// selection to it in one step, naming it after what it drives — the panel's buttons.
fn make_dynamic(s: &mut Session, kind: VariableKind, stem: &str) -> Result<Value> {
// Errors name the command that ran: `variable.make{stem}Dynamic`.
@@ -463,6 +470,8 @@ fn apply_dataset(s: &mut Session, name: &str) -> Result<Value> {
n.visible = *show;
true
}),
// Swap the linked file; keep the object's bounds. A missing path skips.
(VariableKind::Image, DataValue::Image(path)) => links::set_image_from_path(d, id, path).is_ok(),
_ => false,
};
if done {
@@ -502,7 +511,15 @@ fn dataset_step(s: &mut Session, cmd: &str, forward: bool) -> Result<Value> {
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("variable.define", "New Variable…", ["Window", "Variables"], None, "{name, kind: text|visibility} define a variable", has_doc, define),
cmd!(
"variable.define",
"New Variable…",
["Window", "Variables"],
None,
"{name, kind: text|visibility|image} define a variable",
has_doc,
define
),
cmd!(
"variable.delete",
"Delete Variable",
@@ -540,12 +557,21 @@ pub fn specs() -> Vec<CommandSpec> {
has_selection,
|s, _| make_dynamic(s, VariableKind::Visibility, "Visibility")
),
cmd!(
"variable.makeImageDynamic",
"Make Image Dynamic",
["Window", "Variables"],
None,
"{} define an image variable over the selection, named after it and bound to it",
has_selection,
|s, _| make_dynamic(s, VariableKind::Image, "Image")
),
cmd!(
"variable.bind",
"Bind Variable",
["Window", "Variables"],
None,
"{variable, ids?} bind objects (default: the selection) to a variable; text needs type, locked art is refused. An object may hold several variables, and binding it to one it already has changes nothing → {bound, already}",
"{variable, ids?} bind objects (default: the selection) to a variable; text needs type, image needs an image object, locked art is refused. An object may hold several variables, and binding it to one it already has changes nothing → {bound, already}",
has_doc,
bind
),