mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 12:47:13 -06:00
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:
committed by
bflatastic
parent
c8310edea1
commit
41c8bd2a2d
@@ -479,6 +479,16 @@ fn clip_image(d: &mut Document, id: NodeId, (frame, room): Clip) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replace image `id` with the raster at `path`, keeping the object's bounds (what an image
|
||||
/// variable does on `dataset.select`). The file is linked; a missing or unreadable path is an
|
||||
/// error so the dataset application can skip that binding.
|
||||
pub(crate) fn set_image_from_path(d: &mut Document, id: NodeId, path: &str) -> Result<()> {
|
||||
let path = absolute_path(path);
|
||||
let bytes = read_file(&path)?;
|
||||
let read = (link_info(&path, &bytes), fileio::raster_image(&bytes)?);
|
||||
install(d, &[id], read, true)
|
||||
}
|
||||
|
||||
/// Image objects `ids` show the file read for `link` (`img`), each placed as its Placement
|
||||
/// Options say (`force`: as that Preserve choice says, whatever they say).
|
||||
fn install(d: &mut Document, ids: &[NodeId], (link, img): FileRead, force: Option<Preserve>) -> Result<()> {
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::NodeId;
|
||||
|
||||
use super::tests_links::{BLUE, Folder, RED, bounds, image, place, png, write};
|
||||
use super::*;
|
||||
|
||||
fn session() -> Session {
|
||||
@@ -827,3 +828,52 @@ fn ids_and_names_from_an_agent_are_checked() {
|
||||
assert!(s.execute("variable.delete", &json!({"names": []})).is_err());
|
||||
assert!(s.execute("variable.delete", &json!({"names": [5]})).is_err());
|
||||
}
|
||||
|
||||
/// Image variables swap the linked file and keep the object's bounds; capture records the path.
|
||||
#[test]
|
||||
fn image_datasets_swap_pixels_and_keep_bounds() {
|
||||
let dir = Folder::new("vars-image");
|
||||
let red_path = dir.file("red.png");
|
||||
let blue_path = dir.file("blue.png");
|
||||
write(&red_path, &png(40, 30, RED));
|
||||
write(&blue_path, &png(80, 60, BLUE));
|
||||
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &red_path);
|
||||
let before = bounds(&s, id);
|
||||
|
||||
s.execute("variable.define", &json!({"name": "Photo", "kind": "image"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Photo", "ids": [id.0]})).unwrap();
|
||||
// A text object can't take an image variable; a path must be a non-empty string.
|
||||
let t = text(&mut s, "x");
|
||||
assert!(s.execute("variable.bind", &json!({"variable": "Photo", "ids": [t.0]})).is_err());
|
||||
assert!(s.execute("dataset.new", &json!({"name": "B", "values": {"Photo": ""}})).is_err());
|
||||
assert!(s.execute("dataset.new", &json!({"name": "B", "values": {"Photo": true}})).is_err());
|
||||
|
||||
s.execute("dataset.new", &json!({"name": "Blue", "values": {"Photo": blue_path}})).unwrap();
|
||||
let v = s.execute("dataset.select", &json!({"name": "Blue"})).unwrap();
|
||||
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(0)));
|
||||
assert_eq!(image(&s, id).link.as_ref().map(|l| l.path.as_str()), Some(blue_path.as_str()));
|
||||
assert_eq!(bounds(&s, id), before, "bounds stay when the file swaps");
|
||||
|
||||
// Capture records the path the art holds now; a missing file is skipped, not an abort.
|
||||
s.execute("dataset.capture", &json!({"name": "Now"})).unwrap();
|
||||
let sets = s.execute("dataset.list", &json!({})).unwrap();
|
||||
let now = sets["datasets"].as_array().unwrap().iter().find(|d| d["name"] == "Now").unwrap();
|
||||
assert_eq!(now["values"]["Photo"], json!({"image": blue_path}));
|
||||
|
||||
let gone = dir.file("gone.png");
|
||||
s.execute("dataset.new", &json!({"name": "Missing", "values": {"Photo": gone}})).unwrap();
|
||||
let skip = s.execute("dataset.select", &json!({"name": "Missing"})).unwrap();
|
||||
assert_eq!((skip["applied"].as_u64(), skip["skipped"].as_u64()), (Some(0), Some(1)));
|
||||
assert_eq!(image(&s, id).link.as_ref().map(|l| l.path.as_str()), Some(blue_path.as_str()));
|
||||
|
||||
// Make Image Dynamic: one step from a selection to a bound image variable.
|
||||
let id2 = place(&mut s, &red_path);
|
||||
s.execute("select.set", &json!({"ids": [id2.0]})).unwrap();
|
||||
let made = s.execute("variable.makeImageDynamic", &json!({})).unwrap();
|
||||
assert_eq!(made["kind"], "image");
|
||||
assert_eq!(made["ids"], json!([id2.0]));
|
||||
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
|
||||
assert!(s.execute("variable.makeImageDynamic", &json!({})).is_err(), "type is not an image");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user