mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
G5: Symbol Options Export Type (Graphic / Movie Clip)
Store Export Type on symbols for Animate handoff; New Symbol and Symbol Options set it, and symbol.list reports it.
This commit is contained in:
+2
-1
@@ -80,7 +80,7 @@ The dimension hours overlap (feature rows hold some UI, format and AI work), so
|
||||
| 3D and Materials | ~25% | 30–55 |
|
||||
| Type core — [type-parity.md](docs/type-parity.md) | 80% | 14–19 |
|
||||
| Type advanced (CJK, spelling, Touch Type) | 50% | 20–28 |
|
||||
| Symbols, blends, envelopes, Repeat, perspective | 85% | 6–10 |
|
||||
| Symbols, blends, envelopes, Repeat, perspective | 87% | 5–9 |
|
||||
| Image Trace, graphs, image tools | 75% | 5–8 |
|
||||
| Layers, artboards, document setup | 78% | 5–8 |
|
||||
| View & navigation | 70% | 9–14 |
|
||||
@@ -134,6 +134,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-16 | Symbol Options › Export Type (Graphic or Movie Clip, default Movie Clip) with Registration; `symbol.options` / `symbol.list` (G5).
|
||||
| 2026-10-16 | About 160 community PRs landed, each through the full gate. Crashes and hostile files: damaged PDF dictionaries and arrays no longer crash import (#2133, #2240, #2244, #2271), area type with huge row or column counts opens (#2109), hostile mesh edges are bounded (#2051). New: Dimension tool (#1788), legacy version 7 `.ai` save (#1794), Object › Intertwine (#2095), Apple Pencil and touch input on the web (#1740), Sample Color from Screen (#2257), Drop Shadow Darkness (#2252), Smart Blur edge modes (#2154), Scribble variations (#2180), Free Distort corners in its dialog (#2134), Rotate View presets (#2106), tritanopia proofing (#2234), Layers panel filters (#2087), Hide Template (#2117), File › Automate Batch through the Actions panel (#1938), dataset and graph CSV (#1929, #1941), Reset Tracking and Kerning (#2274), Alt+Enter copies in Transform fields (#2236), Shift+Tab panels (#2254), Control bar isolation, Recolor and image Replace (#2258, #2079, #2081). Revolve renders smooth with Cap on by default (#1193); Image Trace keeps tiny parts, thin outlines and shared corners, and Grayscale gradients are grey (#2012, #2025, #2263, #2264, #2265, #1879). Import: many `.ai`, EPS and PDF fidelity fixes (#2014–#2016, #2029, #2030, #2044, #2046, #2050–#2052, #2058, #2129, #2199, #2202, #2218), images up to 1 GB decoded (#2060), `.ai`/`.ait`/`.svgz` files paste from a file manager (#2148, #2201). Fixes across fields (arithmetic #2069, #2209, #2022; caret #1403), New Document (#2158, #2176), menus (#2232), shortcuts (#2235, #2146), Transform Each axes (#2227), Paste in Place (#1926), Color Picker tab order (#1937, #1939) and more. Languages: Bulgarian, Greek, Swedish and Finnish complete under the tests, pt-br completed. Details per PR in the PRs; headline numbers unchanged (estimated). |
|
||||
| 2026-10-14 | Brazilian Portuguese (`pt-br`) covers the whole interface and status/error messages under the completeness tests (#1914); Localization 71% → 74% (measured), hours 55–90 → 50–85. |
|
||||
| 2026-10-14 | Direct Selection: deleting an interior anchor of an open path leaves separate surviving runs. Delete/Backspace and history regressions cover the gap; explicit Remove Anchors remains connected. Headline numbers unchanged. |
|
||||
|
||||
@@ -20,9 +20,11 @@ pub mod inks;
|
||||
mod inline;
|
||||
pub mod intertwine;
|
||||
pub mod shared;
|
||||
mod symbol_export_type;
|
||||
mod symbol_registration;
|
||||
pub use inline::{SYMBOL_HALF, SYMBOL_SIZES};
|
||||
pub use shared::Shared;
|
||||
pub use symbol_export_type::{SYMBOL_EXPORT_TYPE, SymbolExportType};
|
||||
pub use symbol_registration::{SYMBOL_REGISTRATION, SymbolRegistration};
|
||||
pub mod links;
|
||||
pub mod live;
|
||||
@@ -1578,6 +1580,52 @@ impl Document {
|
||||
o.insert(name.into(), serde_json::json!(reg.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the stored registration when a symbol is renamed.
|
||||
pub fn rename_symbol_registration(&mut self, from: &str, to: &str) {
|
||||
if from == to {
|
||||
return;
|
||||
}
|
||||
let reg = self.symbol_registration(from);
|
||||
self.set_symbol_registration(from, SymbolRegistration::CENTER);
|
||||
self.set_symbol_registration(to, reg);
|
||||
}
|
||||
|
||||
/// Symbol Options › Export Type for `name` (default: Movie Clip).
|
||||
pub fn symbol_export_type(&self, name: &str) -> SymbolExportType {
|
||||
self.unknown.get(SYMBOL_EXPORT_TYPE).and_then(|m| m.get(name)).and_then(SymbolExportType::from_value).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Set or clear (when Movie Clip, the default) the export type stored for `name`.
|
||||
pub fn set_symbol_export_type(&mut self, name: &str, kind: SymbolExportType) {
|
||||
if kind.is_default() {
|
||||
self.clear_symbol_export_type(name);
|
||||
return;
|
||||
}
|
||||
let m = self.unknown.entry(SYMBOL_EXPORT_TYPE.to_string()).or_insert_with(|| serde_json::json!({}));
|
||||
let Some(o) = m.as_object_mut() else { return };
|
||||
o.insert(name.into(), serde_json::json!(kind.id()));
|
||||
}
|
||||
|
||||
/// Drop any stored export type for `name` (Movie Clip default).
|
||||
pub fn clear_symbol_export_type(&mut self, name: &str) {
|
||||
let Some(m) = self.unknown.get_mut(SYMBOL_EXPORT_TYPE) else { return };
|
||||
let Some(o) = m.as_object_mut() else { return };
|
||||
o.remove(name);
|
||||
if o.is_empty() {
|
||||
self.unknown.remove(SYMBOL_EXPORT_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the stored export type when a symbol is renamed.
|
||||
pub fn rename_symbol_export_type(&mut self, from: &str, to: &str) {
|
||||
if from == to {
|
||||
return;
|
||||
}
|
||||
let kind = self.symbol_export_type(from);
|
||||
self.clear_symbol_export_type(from);
|
||||
self.set_symbol_export_type(to, kind);
|
||||
}
|
||||
}
|
||||
|
||||
fn default_graphic_styles() -> Vec<GraphicStyle> {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Symbol Options › Export Type: Graphic or Movie Clip
|
||||
//! ([helpx Symbol Options](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html)).
|
||||
//!
|
||||
//! For Illustrator-only work the choice has no canvas effect; it is kept for Animate / Flash /
|
||||
//! HTML5 handoff. Illustrator's default is Movie Clip. Stored under [`SYMBOL_EXPORT_TYPE`] keyed
|
||||
//! by symbol name; the default (Movie Clip) is omitted from the map.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Document unknown key: `{ symbolName: "graphic"|"movieClip" }`.
|
||||
pub const SYMBOL_EXPORT_TYPE: &str = "symbolExportType";
|
||||
|
||||
/// Symbol Options › Export Type.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum SymbolExportType {
|
||||
/// Static / graphic symbol instance (Animate Graphic).
|
||||
Graphic,
|
||||
/// Default: Movie Clip for Animate / game handoff; instances still place the same on the canvas.
|
||||
#[default]
|
||||
MovieClip,
|
||||
}
|
||||
|
||||
impl SymbolExportType {
|
||||
pub const IDS: [&'static str; 2] = ["movieClip", "graphic"];
|
||||
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Graphic => "graphic",
|
||||
Self::MovieClip => "movieClip",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_id(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"graphic" => Some(Self::Graphic),
|
||||
"movieClip" => Some(Self::MovieClip),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// True when this is the document default (nothing stored for the symbol).
|
||||
pub fn is_default(self) -> bool {
|
||||
matches!(self, Self::MovieClip)
|
||||
}
|
||||
|
||||
pub fn from_value(v: &Value) -> Option<Self> {
|
||||
v.as_str().and_then(Self::from_id)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use std::sync::Arc;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_brush::{self as brush, Brush};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, Symbol, SymbolRegistration};
|
||||
use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, Symbol, SymbolExportType, SymbolRegistration};
|
||||
use vectorcraft_geom::{Affine, Point, Vec2};
|
||||
|
||||
use super::edit::{outermost, selected_roots};
|
||||
@@ -95,16 +95,25 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
brush_freehand
|
||||
),
|
||||
// ----- symbols -----
|
||||
cmd!(query "symbol.list", "Symbols", [], None, "{} → {symbols: [{name, instances, size: [w,h], registration: 0..8}], current}", has_doc, symbol_list),
|
||||
cmd!(query "symbol.list", "Symbols", [], None, "{} → {symbols: [{name, instances, size: [w,h], registration: 0..8, exportType: movieClip|graphic}], current}", has_doc, symbol_list),
|
||||
cmd!(
|
||||
"symbol.new",
|
||||
"New Symbol…",
|
||||
[],
|
||||
None,
|
||||
"{name?, ids?|id?, registration?: 0..8 (9-point grid, 4 = centre; Symbol Options › Registration)} make a symbol from the selection and replace it with an instance → {name, id, registration}",
|
||||
"{name?, ids?|id?, registration?: 0..8 (9-point grid, 4 = centre; Symbol Options › Registration), exportType?: movieClip (default)|graphic} make a symbol from the selection and replace it with an instance → {name, id, registration, exportType}",
|
||||
has_selection,
|
||||
symbol_new
|
||||
),
|
||||
cmd!(
|
||||
"symbol.options",
|
||||
"Symbol Options…",
|
||||
[],
|
||||
None,
|
||||
"{name?, newName?, registration?: 0..8, exportType?: movieClip|graphic} rename a symbol and/or set Symbol Options › Registration and Export Type (default name: current) → {name, registration, exportType}",
|
||||
has_doc,
|
||||
symbol_options
|
||||
),
|
||||
cmd!(
|
||||
"symbol.place",
|
||||
"Place Symbol Instance",
|
||||
@@ -517,10 +526,30 @@ fn place_xf(at: Point, (w, h): (f64, f64), reg: SymbolRegistration) -> Affine {
|
||||
Affine::translate(c.to_vec2()) * Affine::scale_non_uniform(w / (2.0 * HALF), h / (2.0 * HALF))
|
||||
}
|
||||
|
||||
fn registration_param(p: &Value, cmd: &str) -> Result<SymbolRegistration> {
|
||||
fn registration_param(p: &Value, cmd: &str) -> Result<Option<SymbolRegistration>> {
|
||||
match p.get("registration") {
|
||||
None => Ok(SymbolRegistration::CENTER),
|
||||
Some(v) => v.as_u64().and_then(SymbolRegistration::from_u64).ok_or_else(|| bad(cmd, "registration must be 0..8")),
|
||||
None => Ok(None),
|
||||
Some(v) => v.as_u64().and_then(SymbolRegistration::from_u64).map(Some).ok_or_else(|| bad(cmd, "registration must be 0..8")),
|
||||
}
|
||||
}
|
||||
|
||||
fn export_type_param(p: &Value, cmd: &str) -> Result<Option<SymbolExportType>> {
|
||||
match str_param(p, "exportType") {
|
||||
None => Ok(None),
|
||||
Some(s) => SymbolExportType::from_id(s).map(Some).ok_or_else(|| bad(cmd, format!("exportType must be movieClip or graphic, not `{s}`"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn rename_natural_size(d: &mut Document, from: &str, to: &str) {
|
||||
if from == to {
|
||||
return;
|
||||
}
|
||||
let size = d.unknown.get(SIZES_KEY).and_then(|m| m.get(from)).cloned();
|
||||
set_natural_size(d, from, None);
|
||||
if let Some(v) = size
|
||||
&& let (Some(w), Some(h)) = (v.get(0).and_then(Value::as_f64), v.get(1).and_then(Value::as_f64))
|
||||
{
|
||||
set_natural_size(d, to, Some((w, h)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,6 +595,7 @@ fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
"instances": instances_of(d, Some(&sym.name)).len(),
|
||||
"size": [w, h],
|
||||
"registration": d.symbol_registration(&sym.name).0,
|
||||
"exportType": d.symbol_export_type(&sym.name).id(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -589,11 +619,13 @@ fn replace_with_instance(d: &mut Document, ids: &[NodeId], name: &str, xf: Affin
|
||||
}
|
||||
|
||||
fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "symbol.new";
|
||||
let ids = roots(s, p)?;
|
||||
let art = selection_art(s, p)?.ok_or_else(|| EngineError::Other("nothing selected".into()))?;
|
||||
let (art, size, c) = normalize(art)?;
|
||||
let name = s.doc()?.doc.unique_symbol_name(str_param(p, "name").filter(|n| !n.trim().is_empty()).unwrap_or("New Symbol"));
|
||||
let reg = registration_param(p, "symbol.new")?;
|
||||
let reg = registration_param(p, C)?.unwrap_or(SymbolRegistration::CENTER);
|
||||
let export = export_type_param(p, C)?.unwrap_or_default();
|
||||
// Keep the art where it was: the geometric centre stays at `c`, whatever the registration is.
|
||||
let place = place_xf(c + reg.offset_from_center(size.0, size.1), size, reg);
|
||||
let id = s.edit("New Symbol", |d, sel| {
|
||||
@@ -601,12 +633,64 @@ fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
d.symbols.push(Symbol { name: name.clone(), art: Arc::new(art) });
|
||||
set_natural_size(d, &name, Some(size));
|
||||
d.set_symbol_registration(&name, reg);
|
||||
d.set_symbol_export_type(&name, export);
|
||||
d.unknown.insert(CURRENT_SYMBOL.into(), json!(name));
|
||||
let id = replace_with_instance(d, &ids, &name, place)?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})?;
|
||||
Ok(json!({ "name": name, "id": id.0, "registration": reg.0 }))
|
||||
Ok(json!({ "name": name, "id": id.0, "registration": reg.0, "exportType": export.id() }))
|
||||
}
|
||||
|
||||
fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "symbol.options";
|
||||
let name = resolve_symbol(&s.doc()?.doc, p, C)?;
|
||||
let export = export_type_param(p, C)?;
|
||||
let reg = registration_param(p, C)?;
|
||||
let new_name = str_param(p, "newName").map(str::trim).filter(|n| !n.is_empty()).map(str::to_string);
|
||||
if new_name.is_none() && export.is_none() && reg.is_none() {
|
||||
return Err(bad(C, "pass newName, registration and/or exportType"));
|
||||
}
|
||||
if let Some(ref n) = new_name
|
||||
&& n != &name
|
||||
&& s.doc()?.doc.symbols.iter().any(|x| x.name == *n)
|
||||
{
|
||||
return Err(bad(C, format!("a symbol named `{n}` already exists")));
|
||||
}
|
||||
let final_name = new_name.clone().unwrap_or_else(|| name.clone());
|
||||
s.edit("Symbol Options", |d, _| {
|
||||
if let Some(ref n) = new_name
|
||||
&& n != &name
|
||||
{
|
||||
if let Some(sym) = d.symbols.iter_mut().find(|x| x.name == name) {
|
||||
sym.name = n.clone();
|
||||
}
|
||||
for id in instances_of(d, Some(&name)) {
|
||||
if let Some(NodeKind::SymbolInstance { symbol, .. }) = d.node_mut(id).map(|node| &mut node.kind) {
|
||||
*symbol = n.clone();
|
||||
}
|
||||
}
|
||||
rename_natural_size(d, &name, n);
|
||||
d.rename_symbol_registration(&name, n);
|
||||
d.rename_symbol_export_type(&name, n);
|
||||
if d.unknown.get(CURRENT_SYMBOL).and_then(Value::as_str) == Some(name.as_str()) {
|
||||
d.unknown.insert(CURRENT_SYMBOL.into(), json!(n));
|
||||
}
|
||||
}
|
||||
if let Some(r) = reg {
|
||||
d.set_symbol_registration(&final_name, r);
|
||||
}
|
||||
if let Some(kind) = export {
|
||||
d.set_symbol_export_type(&final_name, kind);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let d = &s.doc()?.doc;
|
||||
Ok(json!({
|
||||
"name": final_name,
|
||||
"registration": d.symbol_registration(&final_name).0,
|
||||
"exportType": d.symbol_export_type(&final_name).id(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_symbol(d: &Document, p: &Value, cmd: &str) -> Result<String> {
|
||||
@@ -763,6 +847,7 @@ fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
d.symbols.retain(|x| x.name != name);
|
||||
set_natural_size(d, &name, None);
|
||||
d.set_symbol_registration(&name, SymbolRegistration::CENTER);
|
||||
d.clear_symbol_export_type(&name);
|
||||
if d.unknown.get(CURRENT_SYMBOL).and_then(Value::as_str) == Some(name.as_str()) {
|
||||
d.unknown.remove(CURRENT_SYMBOL);
|
||||
}
|
||||
@@ -779,14 +864,16 @@ fn symbol_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let new = d.unique_symbol_name(str_param(p, "newName").unwrap_or(&format!("{name} copy")));
|
||||
let size = natural_size(d, &name);
|
||||
let reg = d.symbol_registration(&name);
|
||||
let export = d.symbol_export_type(&name);
|
||||
s.edit("Duplicate Symbol", |d, _| {
|
||||
let art = d.reid(&sym.art);
|
||||
d.symbols.push(Symbol { name: new.clone(), art: Arc::new(art) });
|
||||
set_natural_size(d, &new, Some(size));
|
||||
d.set_symbol_registration(&new, reg);
|
||||
d.set_symbol_export_type(&new, export);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "name": new }))
|
||||
Ok(json!({ "name": new, "registration": reg.0, "exportType": export.id() }))
|
||||
}
|
||||
|
||||
fn symbol_replace(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -505,6 +505,33 @@ fn delete_duplicate_and_replace_symbols() {
|
||||
assert!(s.execute("symbol.delete", &json!({"name": "Small"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_export_type_defaults_to_movie_clip_and_round_trips() {
|
||||
let mut s = session();
|
||||
let (_, name, _) = make_symbol(&mut s);
|
||||
let list = s.execute("symbol.list", &json!({})).unwrap();
|
||||
assert_eq!(list["symbols"][0]["exportType"], "movieClip");
|
||||
assert!(s.doc().unwrap().doc.unknown.get(vectorcraft_doc::SYMBOL_EXPORT_TYPE).is_none());
|
||||
s.execute("symbol.options", &json!({"name": name, "exportType": "graphic"})).unwrap();
|
||||
assert_eq!(s.execute("symbol.list", &json!({})).unwrap()["symbols"][0]["exportType"], "graphic");
|
||||
assert_eq!(s.doc().unwrap().doc.unknown[vectorcraft_doc::SYMBOL_EXPORT_TYPE][&name], json!("graphic"));
|
||||
let copy = s.execute("symbol.duplicate", &json!({"name": name})).unwrap();
|
||||
assert_eq!(copy["exportType"], "graphic");
|
||||
s.execute("symbol.options", &json!({"name": name, "newName": "Renamed", "exportType": "movieClip"})).unwrap();
|
||||
let list = s.execute("symbol.list", &json!({})).unwrap();
|
||||
let renamed = list["symbols"].as_array().unwrap().iter().find(|x| x["name"] == "Renamed").unwrap();
|
||||
assert_eq!(renamed["exportType"], "movieClip");
|
||||
assert!(s.doc().unwrap().doc.unknown.get(vectorcraft_doc::SYMBOL_EXPORT_TYPE).and_then(|m| m.get("Renamed")).is_none());
|
||||
assert_eq!(s.doc().unwrap().doc.unknown[vectorcraft_doc::SYMBOL_EXPORT_TYPE]["Box copy"], json!("graphic"));
|
||||
s.execute("symbol.delete", &json!({"name": "Box copy"})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.unknown.get(vectorcraft_doc::SYMBOL_EXPORT_TYPE).is_none());
|
||||
rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let r = s.execute("symbol.new", &json!({"name": "Icon", "exportType": "graphic"})).unwrap();
|
||||
assert_eq!(r["exportType"], "graphic");
|
||||
assert!(s.execute("symbol.new", &json!({"exportType": "bogus"})).is_err());
|
||||
assert!(s.execute("symbol.options", &json!({"name": "Renamed"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprayer_command_creates_a_symbol_set_and_alt_removes() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//! Symbol Options: the name and registration point of the symbol New Symbol makes from the
|
||||
//! selection (`symbol.new`), as the Symbols panel's New Symbol button and menu item do.
|
||||
//! Symbol Options: name, Export Type (Graphic or Movie Clip) and Registration for a new symbol
|
||||
//! from the selection (`symbol.new`), or for the current symbol in the Symbols panel
|
||||
//! (`symbol.options`).
|
||||
//!
|
||||
//! Fields: `name`, `registration` (0..8, the 9-point grid; 4 = centre).
|
||||
//! Fields: `name`, `exportType` (`movieClip` | `graphic`), `registration` (0..8, the 9-point grid;
|
||||
//! 4 = centre), `__edit` (true when editing an existing symbol). Help:
|
||||
//! <https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html>.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -14,6 +17,9 @@ use crate::widgets;
|
||||
/// The dialog kind of Symbol Options.
|
||||
pub const KIND: &str = "symbolOptions";
|
||||
|
||||
/// Export Type choices: (value, label). Illustrator's default is Movie Clip.
|
||||
const EXPORT_TYPES: [(&str, &str); 2] = [("movieClip", "Movie Clip"), ("graphic", "Graphic")];
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Symbol Options").into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM };
|
||||
|
||||
/// Open Symbol Options to name a new symbol made from the selection.
|
||||
@@ -23,7 +29,38 @@ pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
return Err("nothing selected".into());
|
||||
}
|
||||
let name = st.doc.unique_symbol_name("New Symbol");
|
||||
app.ui.dialog = Some(Dialog::new(KIND, json!({ "name": name, "registration": 4 })));
|
||||
app.ui.dialog = Some(Dialog::new(KIND, json!({ "name": name, "exportType": "movieClip", "registration": 4, "__edit": false })));
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Open Symbol Options on an existing symbol (`name`, else the Symbols panel's current).
|
||||
pub fn open_edit(app: &mut VectorcraftApp, name: Option<&str>) -> Result<Value, String> {
|
||||
let st = app.session.active().ok_or("no document open")?;
|
||||
let name = match name.filter(|n| !n.is_empty()) {
|
||||
Some(n) if st.doc.symbols.iter().any(|s| s.name == n) => n.to_string(),
|
||||
Some(n) => return Err(format!("no symbol named `{n}`")),
|
||||
None => st
|
||||
.doc
|
||||
.unknown
|
||||
.get("currentSymbol")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|n| st.doc.symbols.iter().any(|s| s.name == *n))
|
||||
.map(str::to_string)
|
||||
.or_else(|| st.doc.symbols.first().map(|s| s.name.clone()))
|
||||
.ok_or_else(|| "the document has no symbols".to_string())?,
|
||||
};
|
||||
let export = st.doc.symbol_export_type(&name).id();
|
||||
let registration = st.doc.symbol_registration(&name).0;
|
||||
app.ui.dialog = Some(Dialog::new(
|
||||
KIND,
|
||||
json!({
|
||||
"name": name,
|
||||
"exportType": export,
|
||||
"registration": registration,
|
||||
"__edit": true,
|
||||
"__from": name,
|
||||
}),
|
||||
));
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
@@ -32,6 +69,14 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
label(ui, tl!("Name:"));
|
||||
form::text(ui, d, "name", 190.0);
|
||||
ui.end_row();
|
||||
label(ui, tl!("Export Type:"));
|
||||
let cur = d.str("exportType");
|
||||
let shown = EXPORT_TYPES.iter().find(|(v, _)| *v == cur).map_or(EXPORT_TYPES[0].1, |(_, l)| *l);
|
||||
let labels = EXPORT_TYPES.map(|(_, l)| l);
|
||||
if let Some((value, _)) = widgets::dropdown(ui, "symbol-export-type", shown, &labels, 190.0).and_then(|i| EXPORT_TYPES.get(i)) {
|
||||
d.fields.insert("exportType".into(), json!(value));
|
||||
}
|
||||
ui.end_row();
|
||||
label(ui, tl!("Registration:"));
|
||||
let cur = d.fields.get("registration").and_then(Value::as_u64).unwrap_or(4) as usize;
|
||||
if let Some(i) = widgets::reference_point(ui, cur) {
|
||||
@@ -42,15 +87,26 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// OK: make the symbol from the selection with the name and registration chosen.
|
||||
/// OK: make a new symbol, or apply name / Export Type / Registration on an existing one.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let r = app.run(
|
||||
"symbol.new",
|
||||
json!({
|
||||
"name": d.str("name"),
|
||||
"registration": d.fields.get("registration").and_then(Value::as_u64).unwrap_or(4),
|
||||
}),
|
||||
);
|
||||
let name = d.str("name");
|
||||
let name = name.trim();
|
||||
if name.is_empty() {
|
||||
return Err("name is empty".into());
|
||||
}
|
||||
let export = d.str("exportType");
|
||||
let registration = d.fields.get("registration").and_then(Value::as_u64).unwrap_or(4);
|
||||
let edit = d.fields.get("__edit").and_then(Value::as_bool).unwrap_or(false);
|
||||
let r = if edit {
|
||||
let from = d.fields.get("__from").and_then(Value::as_str).unwrap_or(name);
|
||||
let mut params = json!({ "name": from, "exportType": export, "registration": registration });
|
||||
if from != name {
|
||||
params["newName"] = json!(name);
|
||||
}
|
||||
app.run("symbol.options", params)
|
||||
} else {
|
||||
app.run("symbol.new", json!({ "name": name, "exportType": export, "registration": registration }))
|
||||
};
|
||||
if r.is_ok() {
|
||||
app.ui.dialog = None;
|
||||
}
|
||||
|
||||
@@ -4928,3 +4928,9 @@ theme Appearance Външен вид
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Цветова схема
|
||||
Modern Модерна
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Тип на експортиране:
|
||||
Movie Clip Филмов клип
|
||||
@msg name is empty името е празно
|
||||
@msg no symbol named `{n}` няма символ с име `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4946,3 +4946,9 @@ mark-style Japanese Japonské
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Barevné schéma
|
||||
Modern Moderní
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Typ exportu:
|
||||
Movie Clip Filmový klip
|
||||
@msg name is empty název je prázdný
|
||||
@msg no symbol named `{n}` žádný symbol s názvem `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4483,3 +4483,9 @@ theme Appearance Erscheinungsbild
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Farbschema
|
||||
Modern Modern
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Exporttyp:
|
||||
Movie Clip Movieclip
|
||||
@msg name is empty Name ist leer
|
||||
@msg no symbol named `{n}` kein Symbol namens `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4898,3 +4898,9 @@ effect Points Σημεία
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Χρωματικός συνδυασμός
|
||||
Modern Σύγχρονο
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Τύπος εξαγωγής:
|
||||
Movie Clip Κλιπ ταινίας
|
||||
@msg name is empty το όνομα είναι κενό
|
||||
@msg no symbol named `{n}` κανένα σύμβολο με όνομα `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1859 and column 19.
|
@@ -4888,3 +4888,9 @@ mark-style Japanese Japonesas
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Esquema de color
|
||||
Modern Moderno
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Tipo de exportación:
|
||||
Movie Clip Clip de película
|
||||
@msg name is empty el nombre está vacío
|
||||
@msg no symbol named `{n}` no hay ningún símbolo llamado `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4910,3 +4910,9 @@ mark-style Japanese Japanilainen
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Värimalli
|
||||
Modern Moderni
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Vientityyppi:
|
||||
Movie Clip Elokuvaleike
|
||||
@msg name is empty nimi on tyhjä
|
||||
@msg no symbol named `{n}` symbolia nimeltä `{n}` ei ole
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4945,3 +4945,9 @@ theme Appearance Apparence
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Jeu de couleurs
|
||||
Modern Moderne
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Type d'exportation :
|
||||
Movie Clip Clip
|
||||
@msg name is empty le nom est vide
|
||||
@msg no symbol named `{n}` aucun symbole nommé `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4913,3 +4913,9 @@ imageTrace Outline Körvonal
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Színséma
|
||||
Modern Modern
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Exportálás típusa:
|
||||
Movie Clip Mozgóklip
|
||||
@msg name is empty a név üres
|
||||
@msg no symbol named `{n}` nincs `{n}` nevű szimbólum
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4523,3 +4523,9 @@ lighting Light Luce
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Combinazione colori
|
||||
Modern Moderno
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Tipo di esportazione:
|
||||
Movie Clip Clip filmato
|
||||
@msg name is empty il nome è vuoto
|
||||
@msg no symbol named `{n}` nessun simbolo denominato `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4564,3 +4564,9 @@ lighting Light ライト
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme 配色
|
||||
Modern モダン
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: 書き出しタイプ:
|
||||
Movie Clip ムービークリップ
|
||||
@msg name is empty 名前が空です
|
||||
@msg no symbol named `{n}` `{n}` という名前のシンボルはありません
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4487,3 +4487,9 @@ lighting Light 광원
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme 색 구성표
|
||||
Modern 모던
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: 내보내기 유형:
|
||||
Movie Clip 무비 클립
|
||||
@msg name is empty 이름이 비어 있습니다
|
||||
@msg no symbol named `{n}` 이름이 `{n}`인 심볼이 없습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4791,3 +4791,9 @@ mark-style Japanese Japońskie
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Schemat kolorów
|
||||
Modern Nowoczesny
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Typ eksportu:
|
||||
Movie Clip Klip filmowy
|
||||
@msg name is empty nazwa jest pusta
|
||||
@msg no symbol named `{n}` brak symbolu o nazwie `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1046,6 +1046,9 @@
|
||||
Graphic Style Options… Opções de Estilo Gráfico…
|
||||
Symbol Options Opções de Símbolo
|
||||
Symbol Options… Opções de Símbolo…
|
||||
Export Type: Tipo de exportação:
|
||||
Movie Clip Clipe de filme
|
||||
Graphic Gráfico
|
||||
Graphic Styles Estilos Gráficos
|
||||
Graphic Styles Options Opções de Estilos Gráficos
|
||||
Graphics Processor Processador Gráfico
|
||||
@@ -4211,7 +4214,6 @@ axis Both Ambos
|
||||
Delete Group Excluir Grupo
|
||||
Import Library… Importar Biblioteca…
|
||||
Export Library… Exportar Biblioteca…
|
||||
Graphic Gráfico
|
||||
System Layout Layout do Sistema
|
||||
Tabular Lining Alinhado Tabular
|
||||
Tabular Oldstyle Antigo Tabular
|
||||
@@ -4863,3 +4865,7 @@ mark-style Japanese Japonesas
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Esquema de cores
|
||||
Modern Moderno
|
||||
|
||||
# Symbol Options › Export Type
|
||||
@msg name is empty o nome está vazio
|
||||
@msg no symbol named `{n}` nenhum símbolo chamado `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4519,3 +4519,9 @@ lighting Light Свет
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Цветовая схема
|
||||
Modern Современная
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Тип экспорта:
|
||||
Movie Clip Видеоклип
|
||||
@msg name is empty имя пустое
|
||||
@msg no symbol named `{n}` нет символа с именем `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4902,3 +4902,9 @@ theme Appearance Utseende
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Färgschema
|
||||
Modern Modern
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Exporttyp:
|
||||
Movie Clip Filmklipp
|
||||
@msg name is empty namnet är tomt
|
||||
@msg no symbol named `{n}` ingen symbol med namnet `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4503,3 +4503,9 @@ lighting Light Світло
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Колірна схема
|
||||
Modern Сучасна
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Тип експорту:
|
||||
Movie Clip Кліп
|
||||
@msg name is empty ім’я порожнє
|
||||
@msg no symbol named `{n}` немає символу з назвою `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 718 and column 40.
|
@@ -4503,3 +4503,9 @@ theme Appearance Giao diện
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme Bảng phối màu
|
||||
Modern Hiện đại
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: Kiểu xuất:
|
||||
Movie Clip Clip phim
|
||||
@msg name is empty tên trống
|
||||
@msg no symbol named `{n}` không có biểu tượng nào tên `{n}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3733 and column 10.
|
@@ -4054,3 +4054,9 @@ lighting Light 光源
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme 配色方案
|
||||
Modern 现代
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: 导出类型:
|
||||
Movie Clip 影片剪辑
|
||||
@msg name is empty 名称为空
|
||||
@msg no symbol named `{n}` 没有名为 `{n}` 的符号
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3842,3 +3842,9 @@ lighting Light 光源
|
||||
# User Interface › Color Scheme
|
||||
Color Scheme 配色方案
|
||||
Modern 現代
|
||||
|
||||
# Symbol Options › Export Type
|
||||
Export Type: 轉存類型:
|
||||
Movie Clip 影片片段
|
||||
@msg name is empty 名稱為空
|
||||
@msg no symbol named `{n}` 沒有名為 `{n}` 的符號
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -465,7 +465,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"ui.symbolOptions",
|
||||
"Symbol Options…",
|
||||
"",
|
||||
"{} open Symbol Options for a new symbol made from the selection, what the Symbols panel's New Symbol opens (dialog `symbolOptions`, fields `name` and `registration` 0..8 on the 9-point grid, default centre; default name the first free \"New Symbol\"/\"New Symbol N\"); OK runs symbol.new {name, registration}",
|
||||
"{name?} open Symbol Options (dialog `symbolOptions`, fields `name`, `exportType`: movieClip|graphic, `registration` 0..8). Without `name` and with a selection: New Symbol (default name the first free \"New Symbol\"/\"New Symbol N\", Export Type Movie Clip, Registration centre); OK runs symbol.new {name, exportType, registration}. With `edit: true` or `name`: edit that symbol; OK runs symbol.options {name, newName?, exportType, registration}",
|
||||
),
|
||||
(
|
||||
"tool.options",
|
||||
@@ -1460,7 +1460,15 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
"ui.colorPicker" => crate::dialogs::open_color_picker(app, p),
|
||||
"ui.sampleScreenColor" => crate::screen_color::start(app, p),
|
||||
"ui.graphicStyleOptions" => crate::dialogs::graphic_style_options::open(app, s("name").as_deref()),
|
||||
"ui.symbolOptions" => crate::dialogs::symbol_options::open(app),
|
||||
"ui.symbolOptions" => {
|
||||
let name = s("name");
|
||||
let edit = p.get("edit").and_then(Value::as_bool).unwrap_or(false);
|
||||
if edit || name.is_some() {
|
||||
crate::dialogs::symbol_options::open_edit(app, name.as_deref())
|
||||
} else {
|
||||
crate::dialogs::symbol_options::open(app)
|
||||
}
|
||||
}
|
||||
"ui.brushOptions" => crate::dialogs::brush_options::open(app, s("name").as_deref()),
|
||||
"tool.options" => crate::toolbar::open_options(app, &s("tool").unwrap_or_default()),
|
||||
"ui.colorGuideOptions" => {
|
||||
|
||||
@@ -172,11 +172,12 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty());
|
||||
let inst = has_instances_selected(app);
|
||||
let name = json!({"name": sel});
|
||||
let items: [(&str, bool, &str, Value); 9] = [
|
||||
let items: [(&str, bool, &str, Value); 10] = [
|
||||
(tl!("New Symbol…"), has_sel, "ui.symbolOptions", json!({})),
|
||||
(tl!("Redefine Symbol"), has_sel && sel.is_some(), "symbol.update", name.clone()),
|
||||
(tl!("Duplicate Symbol"), sel.is_some(), "symbol.duplicate", name.clone()),
|
||||
(tl!("Delete Symbol"), sel.is_some(), "symbol.delete", name.clone()),
|
||||
(tl!("Symbol Options…"), sel.is_some(), "ui.symbolOptions", json!({"edit": true, "name": sel})),
|
||||
(tl!("Edit Symbol"), inst, "symbol.edit", json!({})),
|
||||
(tl!("Place Symbol Instance"), sel.is_some(), "symbol.place", name.clone()),
|
||||
(tl!("Replace Symbol"), inst && sel.is_some(), "symbol.replace", name.clone()),
|
||||
@@ -270,15 +271,28 @@ mod tests {
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("symbolOptions"));
|
||||
assert!(symbols(&mut app).is_empty(), "no symbol before OK");
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "New Symbol");
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("exportType"), "movieClip");
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
// The next one is offered a name no symbol has, and OK keeps the name typed.
|
||||
let list = app.run("symbol.list", json!({})).unwrap();
|
||||
assert_eq!(list["symbols"][0]["exportType"], "movieClip");
|
||||
// The next one is offered a name no symbol has, and OK keeps the name typed and Export Type.
|
||||
app.run("shape.rectangle", json!({"x": 60, "y": 10, "width": 30, "height": 20})).unwrap();
|
||||
app.run("ui.symbolOptions", json!({})).unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "New Symbol 2");
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Badge"));
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("exportType".into(), json!("graphic"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(symbols(&mut app), ["New Symbol", "Badge"]);
|
||||
let list = app.run("symbol.list", json!({})).unwrap();
|
||||
assert_eq!(list["symbols"][1]["exportType"], "graphic");
|
||||
// Symbol Options on the current symbol renames and keeps Export Type.
|
||||
app.run("ui.symbolOptions", json!({"edit": true, "name": "Badge"})).unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("exportType"), "graphic");
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Mark"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(symbols(&mut app), ["New Symbol", "Mark"]);
|
||||
assert_eq!(app.run("symbol.list", json!({})).unwrap()["symbols"][1]["exportType"], "graphic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+6
-4
File diff suppressed because one or more lines are too long
+4
-2
@@ -987,8 +987,10 @@ Guides) can't be picked. An artboard guide is picked, drawn and snapped to acros
|
||||
These families are in the live catalog (`command_list` / `vectorcraft://commands`); the prompt `trace-and-style`
|
||||
already walks Image Trace. Short agent entry points:
|
||||
|
||||
- Symbols: `symbol.list`, `symbol.new {name?, ids?}`, `symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}`,
|
||||
`symbol.replace`, `symbol.edit`, `symbol.duplicate`, `symbol.selectInstances {name}`.
|
||||
- Symbols: `symbol.list` (each row includes `registration` 0..8 and `exportType`: `movieClip`|`graphic`),
|
||||
`symbol.new {name?, registration?, exportType?, ids?}`, `symbol.options {name?, newName?, registration?, exportType?}`,
|
||||
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}`, `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|image|graph}`, `variable.bind {variable, ids?}`,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user