mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #1908 from Messhias/feat/g6-image-variables
G6: image variables (VariableKind::Image)
This commit is contained in:
@@ -17,15 +17,18 @@ pub enum VariableKind {
|
||||
Text,
|
||||
/// Whether bound objects show.
|
||||
Visibility,
|
||||
/// A linked image's file path (the pixels swap; the object's bounds stay).
|
||||
Image,
|
||||
}
|
||||
|
||||
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).
|
||||
/// Parse the `kind` of `variable.define`: `text`, `visibility` (`visible` is accepted
|
||||
/// as the same thing, as the panel and the docs call it both), or `image`.
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"text" => Some(Self::Text),
|
||||
"visibility" | "visible" => Some(Self::Visibility),
|
||||
"image" => Some(Self::Image),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -35,6 +38,7 @@ impl VariableKind {
|
||||
match self {
|
||||
Self::Text => "text",
|
||||
Self::Visibility => "visibility",
|
||||
Self::Image => "image",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,13 +56,18 @@ pub struct Variable {
|
||||
pub enum DataValue {
|
||||
Text(String),
|
||||
Visible(bool),
|
||||
/// Absolute path of the image file an image variable should show.
|
||||
Image(String),
|
||||
}
|
||||
|
||||
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))
|
||||
matches!(
|
||||
(self, kind),
|
||||
(Self::Text(_), VariableKind::Text) | (Self::Visible(_), VariableKind::Visibility) | (Self::Image(_), VariableKind::Image)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,9 +183,10 @@ impl Document {
|
||||
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).
|
||||
/// What the object `id` holds for the variable of `kind`: its characters, whether it shows,
|
||||
/// or a linked image's path. `None` when the object is gone, is not of that kind, has no
|
||||
/// link path yet (an embedded image), 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;
|
||||
@@ -185,7 +195,8 @@ impl Document {
|
||||
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,
|
||||
(VariableKind::Image, NodeKind::Image(im)) => im.link.as_ref().map(|l| DataValue::Image(l.path.clone())),
|
||||
(VariableKind::Text | VariableKind::Image, _) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, Some(Preserve::Bounds))
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
//!
|
||||
//! One dialog covers all of them: what it shows is what the command takes. A variable is a
|
||||
//! name and a kind; a dataset is a name and one field per variable — a text field for a
|
||||
//! `text` variable, a checkbox for a `visibility` one. `dataset.set` opens with the row's
|
||||
//! current values, so editing a row is editing what it already says.
|
||||
//! `text` or `image` variable (path for image), a checkbox for a `visibility` one.
|
||||
//! `dataset.set` opens with the row's current values, so editing a row is editing what it
|
||||
//! already says.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{DataValue, VariableKind, Variables};
|
||||
@@ -19,11 +20,11 @@ use crate::widgets;
|
||||
pub const KIND: &str = "variables";
|
||||
|
||||
/// The kinds a variable can have, as `variable.define` names them.
|
||||
const KINDS: [&str; 2] = ["text", "visibility"];
|
||||
const KINDS: [&str; 3] = ["text", "visibility", "image"];
|
||||
|
||||
/// What the dialog shows for each of [`KINDS`].
|
||||
fn kind_labels() -> [&'static str; 2] {
|
||||
[tl!("Text"), tl!("Visibility")]
|
||||
fn kind_labels() -> [&'static str; 3] {
|
||||
[tl!("Text"), tl!("Visibility"), tl!("Image")]
|
||||
}
|
||||
|
||||
pub(super) const SPEC: DialogSpec =
|
||||
@@ -123,9 +124,10 @@ fn value_fields(vars: &Variables, current: Option<&std::collections::BTreeMap<St
|
||||
let value = match (current.and_then(|c| c.get(&v.name)), v.kind) {
|
||||
(Some(DataValue::Text(t)), _) => json!(t),
|
||||
(Some(DataValue::Visible(b)), _) => json!(b),
|
||||
// Without a value in the row a text field starts empty (no value in this row) and
|
||||
// a visibility one starts shown: its false is a value, so it is never "unset".
|
||||
(None, VariableKind::Text) => json!(""),
|
||||
(Some(DataValue::Image(p)), _) => json!(p),
|
||||
// Without a value in the row a text/image field starts empty (no value in this row)
|
||||
// and a visibility one starts shown: its false is a value, so it is never "unset".
|
||||
(None, VariableKind::Text | VariableKind::Image) => json!(""),
|
||||
(None, VariableKind::Visibility) => json!(true),
|
||||
};
|
||||
out.insert(value_key(&v.name), value);
|
||||
@@ -202,7 +204,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
label(ui, &v.name);
|
||||
let key = value_key(&v.name);
|
||||
match v.kind {
|
||||
VariableKind::Text => {
|
||||
VariableKind::Text | VariableKind::Image => {
|
||||
text(ui, d, &key, 220.0);
|
||||
}
|
||||
VariableKind::Visibility => checkbox(ui, d, &key),
|
||||
@@ -248,19 +250,22 @@ fn dropdown(ui: &mut egui::Ui, d: &mut Dialog, key: &str, options: &[&str], widt
|
||||
}
|
||||
}
|
||||
|
||||
/// The `values` object the dialog's fields mean: a text variable whose field is empty carries
|
||||
/// no value in this row, and a visibility checkbox is always a value — its false, too.
|
||||
/// The `values` object the dialog's fields mean: a text or image variable whose field is empty
|
||||
/// carries no value in this row, and a visibility checkbox is always a value — its false, too.
|
||||
fn values_of(d: &Dialog, vars: &Variables) -> Value {
|
||||
let mut out = serde_json::Map::new();
|
||||
for v in &vars.variables {
|
||||
let key = value_key(&v.name);
|
||||
if v.kind == VariableKind::Text {
|
||||
let text = d.str(&key);
|
||||
if !text.is_empty() {
|
||||
out.insert(v.name.clone(), json!(text));
|
||||
match v.kind {
|
||||
VariableKind::Text | VariableKind::Image => {
|
||||
let text = d.str(&key);
|
||||
if !text.is_empty() {
|
||||
out.insert(v.name.clone(), json!(text));
|
||||
}
|
||||
}
|
||||
VariableKind::Visibility => {
|
||||
out.insert(v.name.clone(), json!(d.bool(&key)));
|
||||
}
|
||||
} else {
|
||||
out.insert(v.name.clone(), json!(d.bool(&key)));
|
||||
}
|
||||
}
|
||||
Value::Object(out)
|
||||
|
||||
@@ -4911,3 +4911,4 @@ mark-style Japanese Japonské
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" umísťuje přechod: zadejte `gradient`
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" umísťuje lineární nebo radiální přechod, ne volný přechod
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" umísťuje samotný přechod: vynechte `start` a `end` (ty ho už umísťují v souřadnicích dokumentu)
|
||||
Make Image Dynamic Nastavit obraz jako dynamický
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4447,3 +4447,4 @@ theme Appearance Erscheinungsbild
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" platziert einen Verlauf: geben Sie `gradient` an
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" platziert einen linearen oder radialen Verlauf, keinen Freiform-Verlauf
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" platziert den Verlauf selbst: lassen Sie `start` und `end` weg (sie platzieren ihn bereits in Dokumentkoordinaten)
|
||||
Make Image Dynamic Bild dynamisch machen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4853,3 +4853,4 @@ mark-style Japanese Japonesas
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" coloca un degradado: indique `gradient`
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" coloca un degradado lineal o radial, no uno de forma libre
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" coloca el propio degradado: omita `start` y `end` (ya lo colocan en coordenadas del documento)
|
||||
Make Image Dynamic Hacer imagen dinámica
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4910,3 +4910,4 @@ theme Appearance Apparence
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" place un dégradé : indiquez `gradient`
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" place un dégradé linéaire ou radial, pas un dégradé de forme libre
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" place le dégradé lui-même : omettez `start` et `end` (ils le placent déjà en coordonnées du document)
|
||||
Make Image Dynamic Rendre l'image dynamique
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4878,3 +4878,4 @@ imageTrace Outline Körvonal
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" színátmenetet helyez el: adja meg a `gradient` értéket
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" lineáris vagy sugaras színátmenetet helyez el, nem szabad formájút
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" magát a színátmenetet helyezi el: hagyja el a `start` és `end` értéket (ezek már a dokumentum koordinátáiban helyezik el)
|
||||
Make Image Dynamic Kép dinamikussá tétele
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4487,3 +4487,4 @@ lighting Light Luce
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" posiziona una sfumatura: indica `gradient`
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" posiziona una sfumatura lineare o radiale, non una sfumatura a forma libera
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" posiziona la sfumatura stessa: ometti `start` e `end` (la posizionano già in coordinate del documento)
|
||||
Make Image Dynamic Rendi immagine dinamica
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4482,3 +4482,4 @@ lighting Light ライト
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" はグラデーションを配置します: `gradient` を指定してください
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" は線形または円形グラデーションを配置します。フリーグラデーションは配置できません
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" はグラデーション自体を配置します: `start` と `end` は省略してください (これらはすでにドキュメント座標で配置を指定します)
|
||||
Make Image Dynamic 画像を動的にする
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4451,3 +4451,4 @@ lighting Light 광원
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection"은 그레이디언트를 배치합니다: `gradient`를 지정하세요
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection"은 선형 또는 방사형 그레이디언트를 배치합니다. 자유형 그레이디언트는 배치할 수 없습니다
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection"은 그레이디언트 자체를 배치합니다: `start`와 `end`를 빼세요 (이미 문서 좌표로 배치합니다)
|
||||
Make Image Dynamic 이미지를 동적으로 만들기
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4755,3 +4755,4 @@ mark-style Japanese Japońskie
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" umieszcza gradient: podaj `gradient`
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" umieszcza gradient liniowy lub radialny, a nie dowolny
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" umieszcza sam gradient: pomiń `start` i `end` (one już umieszczają go we współrzędnych dokumentu)
|
||||
Make Image Dynamic Uczyń obraz dynamicznym
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3812,3 +3812,4 @@ axis Both Ambos
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" posiciona um degradê: informe `gradient`
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" posiciona um degradê linear ou radial, não um de forma livre
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" posiciona o próprio degradê: omita `start` e `end` (eles já o posicionam em coordenadas do documento)
|
||||
Make Image Dynamic Tornar imagem dinâmica
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4483,3 +4483,4 @@ lighting Light Свет
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" размещает градиент: укажите `gradient`
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" размещает линейный или радиальный градиент, а не произвольный
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" сам размещает градиент: уберите `start` и `end` (они уже задают его в координатах документа)
|
||||
Make Image Dynamic Сделать изображение динамическим
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4467,3 +4467,4 @@ lighting Light Світло
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" розміщує градієнт: вкажіть `gradient`
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" розміщує лінійний або радіальний градієнт, а не довільний
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" сам розміщує градієнт: приберіть `start` і `end` (вони вже задають його в координатах документа)
|
||||
Make Image Dynamic Зробити зображення динамічним
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 718 and column 40.
|
@@ -4467,3 +4467,4 @@ theme Appearance Giao diện
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" đặt một dải chuyển màu: hãy cung cấp `gradient`
|
||||
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" đặt dải chuyển màu tuyến tính hoặc xuyên tâm, không phải dạng tự do
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" tự đặt dải chuyển màu: hãy bỏ `start` và `end` (chúng đã đặt nó theo tọa độ tài liệu)
|
||||
Make Image Dynamic Tạo hình ảnh động
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3726 and column 10.
|
@@ -4028,3 +4028,4 @@ lighting Light 光源
|
||||
# Revolve cap description
|
||||
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 封闭开放轮廓的圆形端部。部分旋转的切割侧面保持开放。
|
||||
Distance 距离
|
||||
Make Image Dynamic 使图像动态化
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3815,3 +3815,4 @@ lighting Light 光源
|
||||
# Revolve cap description
|
||||
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 封閉開放輪廓的圓形端部。部分旋轉的切割側面保持開放。
|
||||
Distance 距離
|
||||
Make Image Dynamic 使影像動態化
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -72,13 +72,16 @@ struct Picked {
|
||||
selection_bound: bool,
|
||||
/// Make Text Dynamic needs exactly one type object: a text variable only drives type.
|
||||
single_type: bool,
|
||||
/// Make Image Dynamic needs exactly one image object.
|
||||
single_image: bool,
|
||||
}
|
||||
|
||||
fn picked(st: &vectorcraft_engine::DocState) -> Picked {
|
||||
let selected: Vec<u64> = st.selection.objects.iter().map(|i| i.0).collect();
|
||||
let selection_bound = selected.iter().any(|id| st.doc.variables.bindings.contains_key(&NodeId(*id)));
|
||||
let single_type = matches!(selected.as_slice(), [one] if st.doc.node(NodeId(*one)).is_some_and(|n| matches!(n.kind, NodeKind::Text(_))));
|
||||
Picked { has_selection: !selected.is_empty(), selected, selection_bound, single_type }
|
||||
let single_image = matches!(selected.as_slice(), [one] if st.doc.node(NodeId(*one)).is_some_and(|n| matches!(n.kind, NodeKind::Image(_))));
|
||||
Picked { has_selection: !selected.is_empty(), selected, selection_bound, single_type, single_image }
|
||||
}
|
||||
|
||||
/// Delete Variable: the highlighted variable only (the command's default); the rest stay.
|
||||
@@ -128,7 +131,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
})
|
||||
.collect();
|
||||
let active = sets.iter().find(|s| s.active);
|
||||
let Picked { selected, has_selection, selection_bound, single_type } = picked(st);
|
||||
let Picked { selected, has_selection, selection_bound, single_type, single_image } = picked(st);
|
||||
let highlighted = highlighted_row(st);
|
||||
|
||||
// The current data set across the top: its name, and the arrows that step through them.
|
||||
@@ -163,6 +166,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let icon = match row.kind {
|
||||
VariableKind::Text => "type",
|
||||
VariableKind::Visibility => "eye",
|
||||
VariableKind::Image => "image",
|
||||
};
|
||||
ui.add(egui::Image::new(crate::icons::source(icon)).fit_to_exact_size(egui::vec2(14.0, 14.0)));
|
||||
// The object a variable drives is named beside it, as the reference panel does.
|
||||
@@ -205,6 +209,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if widgets::icon_button_enabled(ui, "eye", tl!("Make Visibility Dynamic"), false, has_selection, 24.0).clicked() {
|
||||
app.run("variable.makeVisibilityDynamic", json!({})).ok();
|
||||
}
|
||||
if widgets::icon_button_enabled(ui, "image", tl!("Make Image Dynamic"), false, single_image, 24.0).clicked() {
|
||||
app.run("variable.makeImageDynamic", json!({})).ok();
|
||||
}
|
||||
if widgets::icon_button(ui, "plus", tl!("New Variable…"), false, 24.0).clicked() {
|
||||
crate::menus::invoke(app, "variable.define", json!({}));
|
||||
}
|
||||
@@ -239,7 +246,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let bound: Vec<u64> = st.doc.variables.bindings.keys().map(|i| i.0).collect();
|
||||
let highlighted = highlighted_row(st);
|
||||
let highlighted_bound: Vec<u64> = highlighted.as_ref().map(|h| bound_objects(st, h)).unwrap_or_default();
|
||||
let Picked { selected, has_selection, selection_bound, single_type } = picked(st);
|
||||
let Picked { selected, has_selection, selection_bound, single_type, single_image } = picked(st);
|
||||
|
||||
if widgets::menu_item(ui, tl!("New Variable…"), true, false) {
|
||||
crate::menus::invoke(app, "variable.define", json!({}));
|
||||
@@ -255,6 +262,9 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if widgets::menu_item(ui, tl!("Make Visibility Dynamic"), has_selection, false) {
|
||||
app.run("variable.makeVisibilityDynamic", json!({})).ok();
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Make Image Dynamic"), single_image, false) {
|
||||
app.run("variable.makeImageDynamic", json!({})).ok();
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Unbind Variable"), selection_bound, false) {
|
||||
app.run("variable.unbind", json!({"ids": selected})).ok();
|
||||
}
|
||||
|
||||
+6
-3
File diff suppressed because one or more lines are too long
+3
-3
@@ -986,9 +986,9 @@ already walks Image Trace. Short agent entry points:
|
||||
`symbol.replace`, `symbol.edit`, `symbol.duplicate`, `symbol.selectInstances {name}`.
|
||||
- Brushes: `brush.list`, `brush.get {name}`, `brush.apply {name, ids?}`, `brush.new {type, name?, params?, ids?}`,
|
||||
`brush.setCurrent {name}`, `brush.options {name, params?}`, `brush.freehand`, `object.expandBrush {ids?}`.
|
||||
- Variables: `variable.list`, `variable.define {name, kind: text|visibility}`, `variable.bind {variable, ids?}`,
|
||||
`variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.unbind`, `dataset.new` / `dataset.set` /
|
||||
`dataset.select`.
|
||||
- Variables: `variable.list`, `variable.define {name, kind: text|visibility|image}`, `variable.bind {variable, ids?}`,
|
||||
`variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeImageDynamic`, `variable.unbind`,
|
||||
`dataset.new` / `dataset.set` / `dataset.select` (image values are file path strings; apply keeps object bounds).
|
||||
- Image Trace: `imageTrace.presets`, `imageTrace.make {id?, preset?, params?, view?}` (or `imageTrace.makeAndExpand`),
|
||||
`imageTrace.setView`, `imageTrace.expand`, `imageTrace.release`.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user