Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol

This commit is contained in:
bflatastic
2026-11-01 04:53:38 +07:00
parent 3f1928efc8
commit f22caa6108
105 changed files with 1218 additions and 212 deletions
+3 -2
View File
@@ -138,9 +138,10 @@ pub fn stain(inst: &Node) -> Option<(Color, f32)> {
Some((f.paint.color()?, f.opacity))
}
/// A symbol's art for instance `inst`, stained by the instance's fill ([`stain`]).
/// A symbol's art for instance `inst`: with the instance's own appearance changes to it (a dynamic
/// symbol's, [`vectorcraft_doc::symbol_overrides`]), stained by the instance's fill ([`stain`]).
pub fn instance_art(art: &Node, inst: &Node) -> Node {
let mut a = art.clone();
let mut a = vectorcraft_doc::symbol_overrides::dynamic_art(art, inst);
if let Some((c, amount)) = stain(inst) {
tint_node(&mut a, &c, amount);
}
+1 -1
View File
@@ -1134,7 +1134,7 @@ fn pieces_leave_out_what_draws_a_brushed_stroke_by_name() {
let mut brushed = rect_art(10.0, 4.0);
brushed.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(StrokeLayer { brush: Some("s".into()), ..stroke(1.0) }));
for (name, art) in [("plain", rect_art(10.0, 4.0)), ("brushed", brushed)] {
doc.symbols.push(Symbol { name: name.into(), art: std::sync::Arc::new(art.clone()) });
doc.symbols.push(Symbol { dynamic: false, name: name.into(), art: std::sync::Arc::new(art.clone()) });
doc.patterns.push(PatternDef::new(name, vec![std::sync::Arc::new(art)]));
}
let instance = |symbol: &str| Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: symbol.into(), xf: vectorcraft_geom::Affine::IDENTITY });
+1 -1
View File
@@ -622,7 +622,7 @@ impl<'d, 'a> Build<'d, 'a> {
let label = self.d.blocks.get(name).map_or(name, |b| b.name.as_str());
let symbol = unique(label, &mut self.next_suffix, &self.symbol_names);
self.symbol_names.insert(symbol.clone());
self.doc.symbols.push(Symbol { name: symbol.clone(), art: Arc::new(art) });
self.doc.symbols.push(Symbol { dynamic: false, name: symbol.clone(), art: Arc::new(art) });
symbol
});
self.symbols.insert(key, made.clone());
+1 -1
View File
@@ -545,7 +545,7 @@ impl<'a> Scene<'a> {
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
&& self.nest < MAX_NEST
{
let mut art = (*sym.art).clone();
let mut art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
art.transform(*xf, false);
self.nest += 1;
self.node(&art, true);
+1 -1
View File
@@ -986,7 +986,7 @@ fn crop_keeps_an_instance_whose_art_reaches_the_region() {
// while the 20 × 20 square around the origin, moved to (-60, -60) to (-40, -40), is off it.
let mut d = doc_with(|d| vec![Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((-50.0, -50.0)) })]);
let art = path(&mut d, shapes::rectangle(Rect::new(100.0, 50.0, 140.0, 90.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
d.symbols.push(vectorcraft_doc::Symbol { name: "Far".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Far".into(), art: Arc::new(art) });
let o = DxfOptions { crop: true, ..opts(&d) };
assert!(!kinds(&export(&d, &o).unwrap()).is_empty());
}
+3 -3
View File
@@ -485,7 +485,7 @@ mod tests {
#[test]
fn symbols_and_patterns_are_limited_too() {
let mut d = Document::new(10.0, 10.0);
d.symbols.push(crate::Symbol { name: "s".into(), art: nested(MAX_DEPTH + 3) });
d.symbols.push(crate::Symbol { dynamic: false, name: "s".into(), art: nested(MAX_DEPTH + 3) });
assert!(d.too_deep(None));
assert!(d.limit_depth());
assert!(!d.too_deep(None));
@@ -504,7 +504,7 @@ mod tests {
for _ in 0..levels {
art = Arc::new(Node::group(NodeId(3), vec![art]));
}
d.symbols.push(crate::Symbol { name: format!("s{i}"), art });
d.symbols.push(crate::Symbol { dynamic: false, name: format!("s{i}"), art });
}
d
}
@@ -573,7 +573,7 @@ mod tests {
};
let through_a_symbol = |d: &mut Document| {
d.patterns.push(crate::PatternDef::new("p", vec![instance(2, "s")]));
d.symbols.push(crate::Symbol { name: "s".into(), art: painted(3, "p") });
d.symbols.push(crate::Symbol { dynamic: false, name: "s".into(), art: painted(3, "p") });
};
for make in [&itself as &dyn Fn(&mut Document), &each_other, &through_a_symbol] {
let mut d = doc(painted(1, "p"));
+1 -1
View File
@@ -841,7 +841,7 @@ fn objects_at_matches_repeated_hits_on_random_documents() {
fn objects_at_finds_symbol_instances_by_their_art() {
let mut d = Document::new(200.0, 200.0);
let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(100.0, 60.0, 140.0, 80.0)), Appearance::default_art());
d.symbols.push(crate::Symbol { name: "Far".into(), art: Arc::new(art) });
d.symbols.push(crate::Symbol { dynamic: false, name: "Far".into(), art: Arc::new(art) });
let (layer, top, inner, group) = (d.layers[0].id, d.alloc_id(), d.alloc_id(), d.alloc_id());
let instance = |id, dy: f64| Node::new(id, NodeKind::SymbolInstance { symbol: "Far".into(), xf: vectorcraft_geom::Affine::translate((0.0, dy)) });
d.insert(Some(layer), 0, instance(top, 0.0)).unwrap();
+2
View File
@@ -24,11 +24,13 @@ pub mod intertwine;
pub mod shared;
mod symbol_export_type;
mod symbol_nine_slice;
pub mod symbol_overrides;
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_nine_slice::{SYMBOL_NINE_SLICE, SymbolNineSlice};
pub use symbol_overrides::SymbolOverride;
pub use symbol_registration::{SYMBOL_REGISTRATION, SymbolRegistration};
pub mod links;
pub mod live;
+1
View File
@@ -256,6 +256,7 @@ fn node_bytes(n: &Node, other: Option<&Node>) -> usize {
if e.intertwine_tiers.capacity() > 0 {
b += ALLOC + e.intertwine_tiers.capacity() * size_of::<PathData>() + e.intertwine_tiers.iter().map(path_bytes).sum::<usize>();
}
b += e.symbol_overrides.capacity() * size_of::<crate::SymbolOverride>();
b += e.objects_on_path.as_deref().map_or(0, |o| {
ALLOC + size_of::<crate::objects_on_path::ObjectsOnPath>() + o.members.capacity() * size_of::<crate::objects_on_path::Member>()
});
+4
View File
@@ -63,6 +63,9 @@ pub struct NodeExtras {
pub compound_shape: bool,
/// As a member of a compound shape, how this object combines with the members below it.
pub shape_mode: crate::ShapeMode,
/// As an instance of a dynamic symbol, its own appearance changes to objects of the symbol's
/// art (see [`crate::symbol_overrides`]); empty for none.
pub symbol_overrides: Vec<crate::SymbolOverride>,
}
/// No extras: what [`Node::extras`] gives for a node without any.
@@ -87,6 +90,7 @@ pub(super) static NO_EXTRAS: NodeExtras = NodeExtras {
colorize: None,
compound_shape: false,
shape_mode: crate::ShapeMode::Add,
symbol_overrides: Vec::new(),
};
/// A node's [`NodeExtras`], if it has any ([`Node::extras`]). Extras that are all unset equal
+18 -13
View File
@@ -52,6 +52,7 @@ impl Serialize for Node {
e.colorize.is_none(),
is_default(&e.compound_shape),
is_default(&e.shape_mode),
e.symbol_overrides.is_empty(),
];
let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;
let mut skip = skips.into_iter();
@@ -92,6 +93,7 @@ impl Serialize for Node {
field!("colorize", &e.colorize);
field!("compound_shape", &e.compound_shape);
field!("shape_mode", &e.shape_mode);
field!("symbol_overrides", &e.symbol_overrides);
st.end()
}
}
@@ -138,26 +140,26 @@ fn count_one<E: serde::de::Error>() -> Result<(), E> {
Ok(())
}
/// A member's Intertwine areas of tier 2 and up, each counting against [`with_node_budget`] as
/// an object does: a compressed file of repeated empty areas stays small while they take memory
/// once read.
fn counted_tiers<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<PathData>, D::Error> {
struct Tiers;
impl<'de> serde::de::Visitor<'de> for Tiers {
type Value = Vec<PathData>;
/// A list read from a file (a member's Intertwine areas of tier 2 and up, an instance's symbol
/// overrides), each item counting against [`with_node_budget`] as an object does: a compressed
/// file of repeated empty items stays small while they take memory once read.
fn counted_list<'de, D: serde::Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Vec<T>, D::Error> {
struct Items<T>(std::marker::PhantomData<T>);
impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for Items<T> {
type Value = Vec<T>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a list of areas")
f.write_str("a list")
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<PathData>, A::Error> {
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<T>, A::Error> {
let mut out = Vec::new();
while let Some(area) = seq.next_element::<PathData>()? {
while let Some(item) = seq.next_element::<T>()? {
count_one()?;
out.push(area);
out.push(item);
}
Ok(out)
}
}
d.deserialize_seq(Tiers)
d.deserialize_seq(Items(std::marker::PhantomData))
}
/// How a [`Node`] is read: the flat field list of the file format. Fields move into the node
@@ -216,7 +218,7 @@ pub(super) struct NodeWire {
dimension: Option<Box<crate::dimension::Dimension>>,
#[serde(default)]
intertwine: Option<Box<PathData>>,
#[serde(default, deserialize_with = "counted_tiers")]
#[serde(default, deserialize_with = "counted_list")]
intertwine_tiers: Vec<PathData>,
#[serde(default)]
live_paint_gaps: Option<Box<crate::live_paint::GapOptions>>,
@@ -228,6 +230,8 @@ pub(super) struct NodeWire {
compound_shape: bool,
#[serde(default)]
shape_mode: crate::ShapeMode,
#[serde(default, deserialize_with = "counted_list")]
symbol_overrides: Vec<crate::SymbolOverride>,
}
impl From<NodeWire> for Node {
@@ -253,6 +257,7 @@ impl From<NodeWire> for Node {
colorize: w.colorize,
compound_shape: w.compound_shape,
shape_mode: w.shape_mode,
symbol_overrides: w.symbol_overrides,
};
Node {
id: w.id,
+1 -1
View File
@@ -337,7 +337,7 @@ mod tests {
// The art is a 40 × 10 box to the right of and below the origin, not the 20 × 20 square
// around it that instances are measured by without the symbols.
let mut d = crate::Document::new(400.0, 400.0);
d.symbols.push(crate::Symbol { name: "Bar".into(), art: Arc::new(rect(9, Rect::new(0.0, 0.0, 40.0, 10.0))) });
d.symbols.push(crate::Symbol { dynamic: false, name: "Bar".into(), art: Arc::new(rect(9, Rect::new(0.0, 0.0, 40.0, 10.0))) });
let line = Node::path(NodeId(1), shapes::line(Point::new(100.0, 300.0), Point::new(300.0, 300.0)), Default::default());
let instance = Node::new(NodeId(2), NodeKind::SymbolInstance { symbol: "Bar".into(), xf: Affine::IDENTITY });
let other = rect(3, Rect::new(0.0, 0.0, 10.0, 10.0));
+1 -1
View File
@@ -200,7 +200,7 @@ mod tests {
fn oriented_bounds_hug_a_rotated_symbol_instances_art() {
let mut d = Document::new(500.0, 500.0);
let bar = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 10.0)), Appearance::default_art());
d.symbols.push(crate::Symbol { name: "Bar".into(), art: std::sync::Arc::new(bar) });
d.symbols.push(crate::Symbol { dynamic: false, name: "Bar".into(), art: std::sync::Arc::new(bar) });
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Bar".into(), xf: Affine::translate((100.0, 100.0)) })).unwrap();
let c = Point::new(120.0, 105.0);
+6 -2
View File
@@ -667,7 +667,7 @@ mod tests {
pixels: None,
})),
);
d.symbols.push(Symbol { name: "Star".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Star".into(), art: Arc::new(art) });
d.images.insert("pic".into(), ImageBlob::new("image/png", vec![1, 2, 3]));
let mut children = vec![Arc::new(Node::new(NodeId(51), NodeKind::SymbolInstance { symbol: "Star".into(), xf: Affine::IDENTITY }))];
if bytes.starts_with(b"nest") {
@@ -702,7 +702,11 @@ mod tests {
fn the_art_and_its_resources_join_only_a_prepared_copy_under_their_own_names() {
set_loader(stub_loader);
let mut d = Document::new(200.0, 200.0);
d.symbols.push(Symbol { name: "Star".into(), art: Arc::new(Node::new(NodeId(60), NodeKind::Group { children: vec![], clip: false })) });
d.symbols.push(Symbol {
dynamic: false,
name: "Star".into(),
art: Arc::new(Node::new(NodeId(60), NodeKind::Group { children: vec![], clip: false })),
});
d.images.insert("doc1-1".into(), ImageBlob::new("application/json", b"nest".to_vec()));
let p = placed("doc1-1");
let layer = d.layers[0].id;
+4
View File
@@ -108,6 +108,10 @@ impl SavedSelection {
pub struct Symbol {
pub name: String,
pub art: Arc<Node>,
/// Symbol Options › Symbol Type › Dynamic Symbol: each instance may change the appearance of
/// objects in the art and stay linked (see [`crate::symbol_overrides`]). Static by default.
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
pub dynamic: bool,
}
/// Encoded image bytes (PNG/JPEG/WebP…) shared by image objects.
+190
View File
@@ -0,0 +1,190 @@
//! Dynamic symbols (Symbol Options › Symbol Type › Dynamic Symbol): an instance of a dynamic
//! symbol keeps appearance changes of its own to objects in the symbol's art, while the art stays
//! the symbol's. Editing the symbol reshapes every instance and each keeps its changes; Reset drops
//! them. The changes are stored on the instance ([`crate::node::NodeExtras::symbol_overrides`]) and
//! applied wherever an instance is drawn or exported ([`dynamic_art`]).
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use vectorcraft_color::{BlendMode, Paint};
use crate::{Node, NodeKind};
/// Deepest object an override may name: groups nested this deep in the symbol's art.
pub const MAX_PATH_DEPTH: usize = 64;
/// One instance's appearance changes to one object of its dynamic symbol's art.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct SymbolOverride {
/// The object: child indices (paint order) from the art's root down through its groups; empty
/// for the root itself.
#[serde(deserialize_with = "short_path")]
pub path: Vec<u32>,
/// The object's (top) fill paint; type takes it as its characters' fill.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fill: Option<Paint>,
/// The object's (top) stroke paint; type takes it as its characters' stroke.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stroke: Option<Paint>,
/// The object's opacity, 0..1.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opacity: Option<f32>,
/// The object's blending mode.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blend: Option<BlendMode>,
}
impl SymbolOverride {
/// Does this override change nothing?
pub fn is_empty(&self) -> bool {
self.fill.is_none() && self.stroke.is_none() && self.opacity.is_none() && self.blend.is_none()
}
/// Put this override's changes on object `n`.
fn apply(&self, n: &mut Node) {
if let NodeKind::Text(t) = &mut n.kind {
for r in &mut t.runs {
if let Some(p) = &self.fill {
r.style.fill = p.clone();
}
if let Some(p) = &self.stroke {
r.style.stroke = p.clone();
}
}
} else {
if let Some(p) = &self.fill {
n.appearance.set_fill(p.clone());
}
if let Some(p) = &self.stroke {
n.appearance.set_stroke(p.clone());
}
}
if let Some(o) = self.opacity.filter(|o| o.is_finite()) {
n.opacity = o.clamp(0.0, 1.0);
}
if let Some(b) = self.blend {
n.blend = b;
}
}
}
/// An override's path read from a file: at most [`MAX_PATH_DEPTH`] indices, so a compressed file
/// of one endless path stays small once read.
fn short_path<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<u32>, D::Error> {
struct Path;
impl<'de> serde::de::Visitor<'de> for Path {
type Value = Vec<u32>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a list of child indices")
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<u32>, A::Error> {
let mut out = Vec::new();
while let Some(i) = seq.next_element::<u32>()? {
if out.len() == MAX_PATH_DEPTH {
return Err(serde::de::Error::custom(format!("a symbol override path is deeper than {MAX_PATH_DEPTH}")));
}
out.push(i);
}
Ok(out)
}
}
d.deserialize_seq(Path)
}
/// The children a path steps through: a group's (clip groups included) or a layer's.
fn groups_children(n: &Node) -> Option<&Vec<Arc<Node>>> {
matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }).then(|| n.children()).flatten()
}
/// The object `path` names in symbol art `art`, if there is one.
pub fn part<'a>(art: &'a Node, path: &[u32]) -> Option<&'a Node> {
if path.len() > MAX_PATH_DEPTH {
return None;
}
path.iter().try_fold(art, |n, &i| groups_children(n)?.get(i as usize).map(|c| &**c))
}
/// [`part`], to change (the nodes on the way are copied when shared).
fn part_mut<'a>(art: &'a mut Node, path: &[u32]) -> Option<&'a mut Node> {
let Some((&i, rest)) = path.split_first() else { return Some(art) };
if !matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) {
return None;
}
let c = art.children_mut()?.get_mut(i as usize)?;
part_mut(Arc::make_mut(c), rest)
}
/// Put `overrides` on symbol art `art`; an override naming no object of it is left out.
pub fn apply(art: &mut Node, overrides: &[SymbolOverride]) {
for o in overrides.iter().filter(|o| o.path.len() <= MAX_PATH_DEPTH) {
if let Some(n) = part_mut(art, &o.path) {
o.apply(n);
}
}
}
/// Symbol art `art` as instance `inst` shows it: a copy with the instance's overrides applied.
pub fn dynamic_art(art: &Node, inst: &Node) -> Node {
let mut a = art.clone();
apply(&mut a, &inst.extras().symbol_overrides);
a
}
/// Keep the overrides that name an object of `art` (a symbol's new art), dropping the rest.
pub fn retain_parts(overrides: &mut Vec<SymbolOverride>, art: &Node) {
overrides.retain(|o| part(art, &o.path).is_some());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Appearance, NodeId};
use vectorcraft_color::Color;
use vectorcraft_geom::{Rect, shapes};
fn square(id: u64) -> Arc<Node> {
Arc::new(Node::path(NodeId(id), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art()))
}
fn art() -> Node {
let inner = Node::group(NodeId(3), vec![square(4), square(5)]);
Node::group(NodeId(1), vec![square(2), Arc::new(inner)])
}
#[test]
fn paths_name_objects_through_groups_only() {
let a = art();
assert_eq!(part(&a, &[]).map(|n| n.id), Some(NodeId(1)));
assert_eq!(part(&a, &[1, 0]).map(|n| n.id), Some(NodeId(4)));
assert!(part(&a, &[2]).is_none());
assert!(part(&a, &[0, 0]).is_none(), "a path has no children");
assert!(part(&a, &[0; MAX_PATH_DEPTH + 1]).is_none());
}
#[test]
fn overrides_paint_their_object_and_skip_missing_ones() {
let red = Paint::solid(Color::rgb(1.0, 0.0, 0.0));
let o = vec![
SymbolOverride { path: vec![1, 1], fill: Some(red.clone()), opacity: Some(0.5), ..Default::default() },
SymbolOverride { path: vec![9], fill: Some(red.clone()), ..Default::default() },
];
let mut a = art();
apply(&mut a, &o);
let n = part(&a, &[1, 1]).unwrap();
assert_eq!(n.appearance.fill_paint(), red);
assert_eq!(n.opacity, 0.5);
assert_eq!(part(&a, &[1, 0]).unwrap().appearance.fill_paint(), Paint::solid(Color::WHITE), "its sibling keeps the symbol's look");
let mut kept = o;
retain_parts(&mut kept, &a);
assert_eq!(kept.len(), 1);
}
#[test]
fn a_path_deeper_than_the_cap_does_not_read() {
let deep = format!("{{\"path\":{:?}}}", vec![0; MAX_PATH_DEPTH + 1]);
assert!(serde_json::from_str::<SymbolOverride>(&deep).is_err());
let ok: SymbolOverride = serde_json::from_str("{\"path\":[1,0],\"opacity\":0.25}").unwrap();
assert_eq!(ok, SymbolOverride { path: vec![1, 0], opacity: Some(0.25), ..Default::default() });
}
}
+1 -1
View File
@@ -410,7 +410,7 @@ mod tests {
/// Symbol `name` holding `art` in a group.
fn symbol(d: &mut Document, name: &str, art: Vec<Node>) {
let group = Node::group(d.alloc_id(), art.into_iter().map(Arc::new).collect());
d.symbols.push(Symbol { name: name.into(), art: Arc::new(group) });
d.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(group) });
}
#[test]
+1 -1
View File
@@ -225,7 +225,7 @@ fn art_on_a_locked_artboard_cannot_be_selected_or_edited() {
fn art_on_artboard_measures_a_symbol_instance_by_its_art() {
let mut d = Document::new(612.0, 792.0);
let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
d.symbols.push(Symbol { name: "Box".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Box".into(), art: Arc::new(art) });
let (layer, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Box".into(), xf: Affine::translate((2.0, 2.0)) })).unwrap();
// The art, (2, 2) to (12, 12), lies inside; the 20 × 20 square, (-8, -8) to (12, 12), doesn't.
+2 -2
View File
@@ -473,8 +473,8 @@ pub fn adjust_in_document(d: &mut Document, n: &Node) -> Option<Node> {
let art;
let src = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } if has_adjustment(n) => {
let symbol = d.symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone());
art = crate::reshape::as_art(Some(d), n, symbol.as_deref())?;
let symbol = d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, n));
art = crate::reshape::as_art(Some(d), n, symbol.as_ref())?;
&art
}
_ => n,
+4 -2
View File
@@ -122,10 +122,12 @@ fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
return Some(bake_pieces(d, m));
}
let symbol = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => d.symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()),
NodeKind::SymbolInstance { symbol, .. } => {
d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, n))
}
_ => None,
};
let m = reshape(Some(d), n, symbol.as_deref())?;
let m = reshape(Some(d), n, symbol.as_ref())?;
return Some(bake_node(d, &m).unwrap_or(m));
}
// A path whose Transform effect copies it with a gradient, or scales its strokes or moves its
+1 -1
View File
@@ -24,7 +24,7 @@ fn outline(doc: Option<&Document>, n: &Node) -> Option<Node> {
match &n.kind {
NodeKind::SymbolInstance { symbol, xf } => {
let sym = doc?.symbols.iter().find(|s| s.name == *symbol)?;
let mut art = (*sym.art).clone();
let mut art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
art.transform(*xf, false);
Some(Node::group(n.id, vec![Arc::new(art)]))
}
+4 -4
View File
@@ -1191,10 +1191,10 @@ fn a_repeat_of_a_symbol_whose_art_is_a_repeat_shares_the_instance_budget() {
let instance = |name: &str| Arc::new(Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: name.into(), xf: Affine::IDENTITY }));
let mut d = Document::new(100.0, 100.0);
// A 500 × 500 grid as a symbol's art, directly and inside a group; a symbol holding itself.
d.symbols.push(Symbol { name: "Grid".into(), art: repeat(grid(unit(), 500)) });
d.symbols.push(Symbol { name: "Grouped".into(), art: Arc::new(Node::group(NodeId(0), vec![repeat(grid(unit(), 500))])) });
d.symbols.push(Symbol { name: "Loop".into(), art: Arc::new(Node::group(NodeId(0), vec![unit(), instance("Loop")])) });
d.symbols.push(Symbol { name: "Small".into(), art: repeat(grid(unit(), 3)) });
d.symbols.push(Symbol { dynamic: false, name: "Grid".into(), art: repeat(grid(unit(), 500)) });
d.symbols.push(Symbol { dynamic: false, name: "Grouped".into(), art: Arc::new(Node::group(NodeId(0), vec![repeat(grid(unit(), 500))])) });
d.symbols.push(Symbol { dynamic: false, name: "Loop".into(), art: Arc::new(Node::group(NodeId(0), vec![unit(), instance("Loop")])) });
d.symbols.push(Symbol { dynamic: false, name: "Small".into(), art: repeat(grid(unit(), 3)) });
// A 3 × 3 grid of an instance of each: each copy draws 250,000 instances, so one copy fits.
for name in ["Grid", "Grouped"] {
let outer = Node::new(NodeId(0), NodeKind::Repeat(grid(instance(name), 3)));
+1 -1
View File
@@ -335,7 +335,7 @@ fn scatter_shapes() -> BrushLibrary {
a.items.push(AppearanceItem::Fill(FillLayer::new(Paint::solid(gold))));
a
});
doc.symbols.push(Symbol { name: "Sparkle".into(), art: Arc::new(sparkle) });
doc.symbols.push(Symbol { dynamic: false, name: "Sparkle".into(), art: Arc::new(sparkle) });
let instance = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Sparkle".into(), xf: Affine::scale(0.3) });
let random = |a: f64, b: f64| (a, b);
let base = Scatter { colorization: Colorization::Tints, ..Default::default() };
+36 -7
View File
@@ -25,6 +25,7 @@ use super::{
use crate::EngineError;
mod brushes;
mod dynamic;
mod symbols;
pub(crate) use brushes::{apply_brush, brush_targets, stroke_paint};
@@ -32,6 +33,8 @@ use brushes::{
brush_apply, brush_delete, brush_duplicate, brush_freehand, brush_get, brush_list, brush_move, brush_new, brush_options, brush_remove,
brush_selection_art, brush_set_current, brush_unused, expand_brush,
};
use dynamic::{symbol_instance_reset, symbol_instance_set_appearance};
use symbols::selected_instances;
pub(crate) use symbols::{instance_in_place, natural_size, normalize, place_xf, set_natural_size, store_symbol};
use symbols::{
symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_edit, symbol_list, symbol_new, symbol_options, symbol_place,
@@ -157,13 +160,21 @@ pub fn specs() -> Vec<CommandSpec> {
brush_freehand
),
// ----- symbols -----
cmd!(query "symbol.list", "Symbols", [], None, "{} → {symbols: [{name, instances, size: [w,h], registration: 0..8, exportType: movieClip|graphic}], current}", has_doc, symbol_list),
cmd!(
query "symbol.list",
"Symbols",
[],
None,
"{} → {symbols: [{name, instances, size: [w,h], registration: 0..8, exportType: movieClip|graphic, nineSlice, type: static|dynamic}], 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), exportType?: movieClip (default)|graphic, nineSlice?: bool, nineSliceGuides?: array} make a symbol from the selection and replace it with an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds; nineSlice is Symbol Options › Enable Guides for 9-Slice Scaling, nineSliceGuides [left, top, right, bottom] as fractions 0..1 of the natural size (default a third each) → {name, id, registration, exportType, nineSlice}",
"{name?, ids?|id?, registration?: 0..8 (9-point grid, 4 = centre; Symbol Options › Registration), exportType?: movieClip (default)|graphic, nineSlice?: bool, nineSliceGuides?: array, type?: static (default)|dynamic (Symbol Options › Symbol Type)} make a symbol from the selection and replace it with an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds; nineSlice is Symbol Options › Enable Guides for 9-Slice Scaling, nineSliceGuides [left, top, right, bottom] as fractions 0..1 of the natural size (default a third each) → {name, id, registration, exportType, nineSlice, type}",
has_selection,
symbol_new
),
@@ -172,7 +183,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Symbol Options…",
[],
None,
"{name?, newName?, registration?: 0..8, exportType?: movieClip|graphic, nineSlice?: bool, nineSliceGuides?: array} rename a symbol and/or set Symbol Options › Registration and Export Type and 9-slice guides (nineSliceGuides [left, top, right, bottom] fractions 0..1; default name: current) → {name, registration, exportType, nineSlice}",
"{name?, newName?, registration?: 0..8, exportType?: movieClip|graphic, nineSlice?: bool, nineSliceGuides?: array, type?: static|dynamic} rename a symbol and/or set Symbol Options › Registration, Export Type, 9-slice guides (nineSliceGuides [left, top, right, bottom] fractions 0..1) and Symbol Type (a dynamic symbol's instances may change the appearance of objects in its art and stay linked; making it static drops those changes); default name: current → {name, registration, exportType, nineSlice, type}",
has_doc,
symbol_options
),
@@ -190,7 +201,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Break Link to Symbol",
[],
None,
"{ids?|id?} expand selected instances into plain art → {ids}",
"{ids?|id?} expand selected instances into plain art (with their appearance changes to a dynamic symbol) → {ids}",
has_selection,
symbol_break_link
),
@@ -199,7 +210,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Edit Symbol",
[],
None,
"{id?, ids?: [id…] (instead of id)} break the link of the selected instance so its art can be edited; finish with symbol.update → {name, ids}",
"{id?, ids?: [id…] (instead of id)} break the link of the selected instance so the symbol's art can be edited (without the instance's own appearance changes to a dynamic symbol); finish with symbol.update → {name, ids}",
has_selection,
symbol_edit
),
@@ -208,7 +219,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Redefine Symbol",
[],
None,
"{name, ids?|id?} replace the symbol's art with the selection (instances update; the selection becomes an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds) → {name, id}",
"{name, ids?|id?} replace the symbol's art with the selection (instances update, keeping their appearance changes to the objects at paths the new art still has; the selection becomes an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds) → {name, id}",
has_selection,
symbol_update
),
@@ -227,10 +238,28 @@ pub fn specs() -> Vec<CommandSpec> {
"Replace Symbol",
[],
None,
"{name, ids?|id?} swap the symbol of the selected instances → {ids}",
"{name, ids?|id?} swap the symbol of the selected instances (they keep their appearance changes to the objects at paths a dynamic new symbol's art has) → {ids}",
has_selection,
symbol_replace
),
cmd!(
"symbol.instance.setAppearance",
"Symbol Instance Appearance",
[],
None,
"{id?|ids?: [one id] (default: the one selected object), path: [i…] (the object in the symbol's art: child indices in paint order from the art's root down through its groups; [] the root), fill?: \"#rrggbb\"|{…paint params of paint.setFill: color, none, swatch, tint, gradient, keepModel}|null (null: the symbol's own again), stroke?: as fill, opacity?: 0..100|null, blend?: name|null} change the appearance of one object of a dynamic symbol instance's art, for that instance only: it stays linked, so editing the symbol reshapes it and keeps the change; refused for a static symbol or a path naming no object → {id, overrides: how many objects the instance changes}",
has_selection,
symbol_instance_set_appearance
),
cmd!(
"symbol.instance.reset",
"Reset Symbol Instance",
[],
None,
"{ids?|id?} drop the selected dynamic symbol instances' own appearance changes, so they show the symbol's appearance again → {ids: the instances that had changes}",
has_selection,
symbol_instance_reset
),
cmd!(
"symbol.setCurrent",
"Current Symbol",
+151
View File
@@ -0,0 +1,151 @@
//! Dynamic symbols (Symbol Options › Symbol Type): an instance of a dynamic symbol changes the
//! appearance of objects in the symbol's art and stays linked to the symbol
//! (`symbol.instance.setAppearance`, `symbol.instance.reset`). The changes live on the instance
//! ([`vectorcraft_doc::symbol_overrides`]); editing the symbol keeps those whose object is still
//! there ([`fit_overrides`]).
use std::sync::Arc;
use vectorcraft_color::BlendMode;
use vectorcraft_doc::symbol_overrides::{self, SymbolOverride};
use super::{Document, EngineError, Node, NodeId, NodeKind, Result, Session, Value, bad, check_numbers, ids_json, json, roots, selected_instances};
/// The `type` param, Symbol Options › Symbol Type: `Some(true)` for dynamic.
pub(super) fn type_param(p: &Value, cmd: &str) -> Result<Option<bool>> {
let Some(v) = p.get("type").filter(|v| !v.is_null()) else { return Ok(None) };
match v.as_str() {
Some("static") => Ok(Some(false)),
Some("dynamic") => Ok(Some(true)),
_ => Err(bad(cmd, format!("type must be static or dynamic, not {v}"))),
}
}
/// The `type` a symbol reports.
pub(super) fn type_id(dynamic: bool) -> &'static str {
if dynamic { "dynamic" } else { "static" }
}
/// Fit the appearance changes of instances `ids` to their symbols: an instance of a static (or
/// missing) symbol keeps none, an instance of a dynamic one those naming an object of its art.
pub(super) fn fit_overrides(d: &mut Document, ids: &[NodeId]) {
for id in ids {
let art = match d.node(*id) {
Some(n) if !n.extras().symbol_overrides.is_empty() => match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => d.symbols.iter().find(|s| s.name == *symbol).filter(|s| s.dynamic).map(|s| s.art.clone()),
_ => continue,
},
_ => continue,
};
let Some(n) = d.node_mut(*id) else { continue };
let mut e = n.extras_mut();
match art {
Some(art) => symbol_overrides::retain_parts(&mut e.symbol_overrides, &art),
None => e.symbol_overrides.clear(),
}
}
}
/// The instance `id` names (else the one selected object) and its symbol's art; the symbol must
/// be dynamic.
fn dynamic_instance(s: &Session, p: &Value, cmd: &str) -> Result<(NodeId, Arc<Node>)> {
let d = &s.doc()?.doc;
let not_one = || EngineError::Other("select a symbol instance".into());
let [id] = roots(s, p)?[..] else { return Err(not_one()) };
let Some(NodeKind::SymbolInstance { symbol, .. }) = d.node(id).map(|n| &n.kind) else { return Err(not_one()) };
let sym = d.symbols.iter().find(|x| x.name == *symbol).ok_or_else(|| bad(cmd, format!("no symbol named `{n}`", n = symbol)))?;
if !sym.dynamic {
let name = &sym.name;
return Err(bad(cmd, format!("`{name}` is a static symbol: set its Symbol Type to Dynamic first")));
}
Ok((id, sym.art.clone()))
}
/// The `path` param: child indices from the art's root, naming an object of `art`.
fn path_param(p: &Value, art: &Node, cmd: &str) -> Result<Vec<u32>> {
let path: Option<Vec<u32>> =
p.get("path").and_then(Value::as_array).and_then(|a| a.iter().map(|i| i.as_u64().and_then(|i| u32::try_from(i).ok())).collect());
let path = path.ok_or_else(|| bad(cmd, "path must be a list of child indices"))?;
if symbol_overrides::part(art, &path).is_none() {
let shown = json!(path);
return Err(bad(cmd, format!("no object at path {shown} in the symbol's art")));
}
Ok(path)
}
/// `fill` or `stroke` of `p` for override `o`: absent leaves it, null clears it, a colour string or
/// an object of `paint.setFill`'s paint params sets it.
fn paint_change(s: &Session, p: &Value, key: &str, cmd: &str) -> Result<Option<Option<vectorcraft_color::Paint>>> {
let Some(v) = p.get(key) else { return Ok(None) };
if v.is_null() {
return Ok(Some(None));
}
let v = if v.is_string() { json!({ "color": v }) } else { v.clone() };
let paint = crate::cmd::paint::paint_from(s, &v)?.ok_or_else(|| bad(cmd, "give color, none, swatch or gradient"))?;
Ok(Some(Some(paint)))
}
/// The change `p` asks of the override `old` (for the object at `path`).
fn changed(s: &Session, p: &Value, old: SymbolOverride, cmd: &str) -> Result<SymbolOverride> {
let mut o = old;
if let Some(fill) = paint_change(s, p, "fill", cmd)? {
o.fill = fill;
}
if let Some(stroke) = paint_change(s, p, "stroke", cmd)? {
o.stroke = stroke;
}
check_numbers(p, &["opacity"], cmd)?;
if let Some(v) = p.get("opacity") {
o.opacity = v.as_f64().map(crate::cmd::opacitymask::percent);
}
if let Some(v) = p.get("blend") {
o.blend = match v {
Value::Null => None,
b => Some(b.as_str().and_then(BlendMode::parse).ok_or_else(|| bad(cmd, format!("unknown blend mode {b}")))?),
};
}
Ok(o)
}
pub(super) fn symbol_instance_set_appearance(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "symbol.instance.setAppearance";
if !["fill", "stroke", "opacity", "blend"].iter().any(|k| p.get(k).is_some()) {
return Err(bad(C, "give fill, stroke, opacity and/or blend"));
}
let (id, art) = dynamic_instance(s, p, C)?;
let path = path_param(p, &art, C)?;
let d = &s.doc()?.doc;
let old = d.node(id).and_then(|n| n.extras().symbol_overrides.iter().find(|o| o.path == path).cloned());
let new = changed(s, p, old.unwrap_or_else(|| SymbolOverride { path: path.clone(), ..Default::default() }), C)?;
let count = s.edit("Symbol Instance Appearance", |d, _| {
let n = d.node_mut(id).ok_or(EngineError::NoNode(id))?;
let mut e = n.extras_mut();
let list = &mut e.symbol_overrides;
list.retain(|o| o.path != path);
if !new.is_empty() {
list.push(new);
}
Ok(list.len())
})?;
Ok(json!({ "id": id.0, "overrides": count }))
}
pub(super) fn symbol_instance_reset(s: &mut Session, p: &Value) -> Result<Value> {
let ids = selected_instances(s, p)?;
if ids.is_empty() {
return Err(EngineError::Other("no symbol instances selected".into()));
}
let d = &s.doc()?.doc;
let changed: Vec<NodeId> = ids.into_iter().filter(|id| d.node(*id).is_some_and(|n| !n.extras().symbol_overrides.is_empty())).collect();
if !changed.is_empty() {
s.edit("Reset Symbol Instance", |d, _| {
for id in &changed {
if let Some(n) = d.node_mut(*id) {
n.extras_mut().symbol_overrides.clear();
}
}
Ok(())
})?;
}
Ok(json!({ "ids": ids_json(&changed) }))
}
+46 -18
View File
@@ -1,3 +1,4 @@
use super::dynamic::{fit_overrides, type_id, type_param};
use super::{
Affine, AppearanceItem, Arc, CURRENT_SYMBOL, Color, Document, EngineError, FillLayer, HALF, Node, NodeId, NodeKind, Paint, Point, Result,
SIZES_KEY, Session, Symbol, SymbolExportType, SymbolNineSlice, SymbolRegistration, Value, Vec2, bad, brush, color_value, ids_json, json,
@@ -128,6 +129,7 @@ pub(super) fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
"registration": d.symbol_registration(&sym.name).0,
"exportType": d.symbol_export_type(&sym.name).id(),
"nineSlice": nine_slice_json(d, &sym.name),
"type": type_id(sym.dynamic),
})
})
.collect();
@@ -138,7 +140,7 @@ pub(super) fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
/// current symbol.
pub(crate) fn store_symbol(d: &mut Document, art: &Node, name: &str, size: (f64, f64)) {
let art = d.reid(art);
d.symbols.push(Symbol { name: name.into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(art) });
set_natural_size(d, name, Some(size));
d.unknown.insert(CURRENT_SYMBOL.into(), json!(name));
}
@@ -179,10 +181,14 @@ pub(super) fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
let reg = registration_param(p, C)?.unwrap_or(SymbolRegistration::CENTER);
let export = export_type_param(p, C)?.unwrap_or_default();
let nine = nine_slice_param(p, C, None)?.flatten();
let dynamic = type_param(p, C)?.unwrap_or(false);
// 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| {
store_symbol(d, &art, &name, size);
if let Some(sym) = d.symbols.iter_mut().find(|x| x.name == name) {
sym.dynamic = dynamic;
}
d.set_symbol_registration(&name, reg);
d.set_symbol_export_type(&name, export);
d.set_symbol_nine_slice(&name, nine);
@@ -190,9 +196,14 @@ pub(super) fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
sel.set([id]);
Ok(id)
})?;
Ok(
json!({ "name": name, "id": id.0, "registration": reg.0, "exportType": export.id(), "nineSlice": nine.map_or(Value::Null, SymbolNineSlice::to_value) }),
)
Ok(json!({
"name": name,
"id": id.0,
"registration": reg.0,
"exportType": export.id(),
"nineSlice": nine.map_or(Value::Null, SymbolNineSlice::to_value),
"type": type_id(dynamic),
}))
}
pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
@@ -201,9 +212,10 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
let export = export_type_param(p, C)?;
let reg = registration_param(p, C)?;
let nine = nine_slice_param(p, C, s.doc()?.doc.symbol_nine_slice(&name))?;
let dynamic = type_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() && nine.is_none() {
return Err(bad(C, "pass newName, registration, exportType and/or nineSlice"));
if new_name.is_none() && export.is_none() && reg.is_none() && nine.is_none() && dynamic.is_none() {
return Err(bad(C, "pass newName, registration, exportType, nineSlice and/or type"));
}
if let Some(ref n) = new_name
&& n != &name
@@ -241,6 +253,13 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(g) = nine {
d.set_symbol_nine_slice(&final_name, g);
}
if let Some(dynamic) = dynamic
&& let Some(sym) = d.symbols.iter_mut().find(|x| x.name == final_name)
{
// A static symbol's instances drop their own appearance changes.
sym.dynamic = dynamic;
fit_overrides(d, &instances_of(d, Some(&final_name)));
}
Ok(())
})?;
let d = &s.doc()?.doc;
@@ -249,6 +268,7 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
"registration": d.symbol_registration(&final_name).0,
"exportType": d.symbol_export_type(&final_name).id(),
"nineSlice": nine_slice_json(d, &final_name),
"type": type_id(d.symbols.iter().any(|x| x.name == final_name && x.dynamic)),
}))
}
@@ -279,11 +299,12 @@ pub(super) fn symbol_place(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "id": id.0 }))
}
/// The plain art of instance `inst` in document space (fresh ids).
fn expanded_instance(d: &mut Document, inst: &Node) -> Option<Node> {
/// The plain art of instance `inst` in document space (fresh ids), with the instance's own
/// appearance changes to a dynamic symbol's art when `own`.
fn expanded_instance(d: &mut Document, inst: &Node, own: bool) -> Option<Node> {
let NodeKind::SymbolInstance { symbol, xf } = &inst.kind else { return None };
let sym = d.symbols.iter().find(|s| &s.name == symbol)?;
let mut art = (*sym.art).clone();
let mut art = if own { vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, inst) } else { (*sym.art).clone() };
if let Some(f) = inst.appearance.fill()
&& let Some(c) = f.paint.color()
{
@@ -302,11 +323,13 @@ fn expanded_instance(d: &mut Document, inst: &Node) -> Option<Node> {
Some(art)
}
fn break_links(d: &mut Document, ids: &[NodeId]) -> Result<Vec<NodeId>> {
/// Replace instances `ids` with their art ([`expanded_instance`], `own` as there). Returns the
/// art's ids.
fn break_links(d: &mut Document, ids: &[NodeId], own: bool) -> Result<Vec<NodeId>> {
let mut out = vec![];
for id in ids {
let Some(inst) = d.node(*id).cloned() else { continue };
let Some(art) = expanded_instance(d, &inst) else { continue };
let Some(art) = expanded_instance(d, &inst, own) else { continue };
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
let aid = art.id;
d.remove(*id)?;
@@ -316,7 +339,7 @@ fn break_links(d: &mut Document, ids: &[NodeId]) -> Result<Vec<NodeId>> {
Ok(out)
}
fn selected_instances(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
pub(super) fn selected_instances(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
let d = &s.doc()?.doc;
let mut out = vec![];
for id in roots(s, p)? {
@@ -337,7 +360,7 @@ pub(super) fn symbol_break_link(s: &mut Session, p: &Value) -> Result<Value> {
return Err(EngineError::Other("no symbol instances selected".into()));
}
let out = s.edit("Break Link to Symbol", |d, sel| {
let out = break_links(d, &ids)?;
let out = break_links(d, &ids, true)?;
sel.set(out.iter().copied());
Ok(out)
})?;
@@ -351,8 +374,9 @@ pub(super) fn symbol_edit(s: &mut Session, p: &Value) -> Result<Value> {
Some(NodeKind::SymbolInstance { symbol, .. }) => symbol.clone(),
_ => return Err(EngineError::Other("select a symbol instance".into())),
};
// The symbol's own art: one instance's appearance changes would become every instance's.
let out = s.edit("Edit Symbol", |d, sel| {
let out = break_links(d, &[first])?;
let out = break_links(d, &[first], false)?;
sel.set(out.iter().copied());
Ok(out)
})?;
@@ -377,11 +401,14 @@ pub(super) fn symbol_update(s: &mut Session, p: &Value) -> Result<Value> {
}
set_natural_size(d, &name, Some(size));
let fix = Affine::scale_non_uniform(size.0 / old.0, size.1 / old.1);
for i in instances_of(d, Some(&name)) {
if let Some(NodeKind::SymbolInstance { xf, .. }) = d.node_mut(i).map(|n| &mut n.kind) {
let instances = instances_of(d, Some(&name));
for i in &instances {
if let Some(NodeKind::SymbolInstance { xf, .. }) = d.node_mut(*i).map(|n| &mut n.kind) {
*xf *= fix;
}
}
// Each instance keeps its appearance changes to the objects the new art still has.
fit_overrides(d, &instances);
let reg = d.symbol_registration(&name);
let id = instance_in_place(d, &ids, &name, place_xf(c + reg.offset_from_center(size.0, size.1), size, reg))?;
sel.set([id]);
@@ -400,7 +427,7 @@ pub(super) fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
s.edit("Delete Symbol", |d, _| {
let inst = instances_of(d, Some(&name));
if expand {
break_links(d, &inst)?;
break_links(d, &inst, true)?;
} else {
remove_each(d, &inst)?;
}
@@ -430,7 +457,7 @@ pub(super) fn symbol_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
let nine = d.symbol_nine_slice(&name);
s.edit("Duplicate Symbol", |d, _| {
let art = d.reid(&sym.art);
d.symbols.push(Symbol { name: new.clone(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: sym.dynamic, 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);
@@ -462,6 +489,7 @@ pub(super) fn symbol_replace(s: &mut Session, p: &Value) -> Result<Value> {
*xf *= Affine::scale_non_uniform(new.0 / old.0, new.1 / old.1);
}
}
fit_overrides(d, &ids);
Ok(())
})?;
Ok(json!({ "ids": ids_json(&ids) }))
+4 -2
View File
@@ -682,10 +682,12 @@ fn expanded_node(d: &mut Document, mut n: Node, brushes: &[vectorcraft_brush::Br
// Type, images, symbol instances, live objects (and paths whose Transform effect copies
// them with a gradient or changes their fills and strokes): through their outlines.
let symbol = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => d.symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()),
NodeKind::SymbolInstance { symbol, .. } => {
d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, &n))
}
_ => None,
};
effects::expand_outlined(Some(&*d), &v, symbol.as_deref()).map(|mut m| {
effects::expand_outlined(Some(&*d), &v, symbol.as_ref()).map(|mut m| {
expand_pieces(d, &mut m, true, brushes, &mut stroke_art);
m
})
+1 -1
View File
@@ -181,7 +181,7 @@ fn basic_shapes() -> SymbolLibrary {
let id = doc.alloc_id();
let art = Node::path(id, path, Appearance::basic(Paint::solid(fill), Paint::solid(edge), 1.0));
let Ok((art, size, _)) = normalize(art) else { continue };
doc.symbols.push(Symbol { name: name.into(), art: Arc::new(art) });
doc.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(art) });
set_natural_size(&mut doc, name, Some(size));
}
doc.title = "Basic Shapes".into();
+1
View File
@@ -41,6 +41,7 @@ mod dashalign;
mod distort;
mod docsetup;
mod draw2;
mod dynamic_symbols;
mod editcolors;
mod effectedit;
mod emptytype;
+1 -1
View File
@@ -256,7 +256,7 @@ fn a_file_with_script_proportions_out_of_range_opens_with_them_in_range() {
}
let mut art = d.node(id).unwrap().clone();
art.id = NodeId(9000);
d.symbols.push(vectorcraft_doc::Symbol { name: "Formula".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Formula".into(), art: Arc::new(art) });
if let Some(g) = d.node_mut(graph).and_then(|n| n.extras_if_any()).and_then(|e| e.graph.as_deref_mut()) {
let position = CharPosition::Subscript(ScriptMetrics { size: 0.0, position: 1000.0 });
g.label_styles.legend = Some(vectorcraft_doc::CharStyle { position, small_caps: Some(0.0), ..vectorcraft_doc::CharStyle::default() });
+165
View File
@@ -0,0 +1,165 @@
//! Dynamic symbols (#4938): an instance changes the appearance of objects in its symbol's art and
//! stays linked; editing the symbol keeps those changes, Reset and Break Link handle them.
use serde_json::{Value, json};
use vectorcraft_doc::{Node, NodeKind};
use vectorcraft_geom::Rect;
use crate::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 100})).unwrap();
s
}
fn id(v: &Value) -> NodeId {
NodeId(v["id"].as_u64().unwrap())
}
/// A blue square at `x` (40 pt, no stroke).
fn square(s: &mut Session, x: f64) -> NodeId {
let r = id(&s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 40, "height": 40})).unwrap());
s.execute("paint.setFill", &json!({"ids": [r.0], "color": "#0000ff"})).unwrap();
s.execute("paint.setStroke", &json!({"ids": [r.0], "none": true})).unwrap();
r
}
/// Symbol "Pair" of two blue squares (x 10..50 and 60..100) of `kind`, made in place: its instance.
fn pair(s: &mut Session, kind: &str) -> NodeId {
let (a, b) = (square(s, 10.0), square(s, 60.0));
id(&s.execute("symbol.new", &json!({"name": "Pair", "ids": [a.0, b.0], "type": kind})).unwrap())
}
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().doc.node(id).unwrap().clone()
}
fn overrides(s: &Session, id: NodeId) -> usize {
node(s, id).extras().symbol_overrides.len()
}
fn pixel(s: &Session, x: u32, y: u32) -> [u8; 4] {
vectorcraft_render::Renderer::new().render_region(&s.doc().unwrap().doc, Rect::new(0.0, 0.0, 200.0, 100.0), 1.0, true).pixel(x, y)
}
const RED: [u8; 4] = [255, 0, 0, 255];
const BLUE: [u8; 4] = [0, 0, 255, 255];
#[test]
fn a_dynamic_instance_paints_one_object_its_own_way_and_stays_linked() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
assert_eq!(s.execute("symbol.list", &json!({})).unwrap()["symbols"][0]["type"], "dynamic");
let other = id(&s.execute("symbol.place", &json!({"name": "Pair", "x": 55, "y": 80})).unwrap());
let r = s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "fill": "#ff0000"})).unwrap();
assert_eq!(r["overrides"], 1);
assert!(matches!(node(&s, inst).kind, NodeKind::SymbolInstance { .. }), "still an instance");
assert_eq!(pixel(&s, 80, 30), RED, "the changed object");
assert_eq!(pixel(&s, 30, 30), BLUE, "its sibling keeps the symbol's look");
assert_eq!(overrides(&s, other), 0, "other instances keep the symbol's look");
// A later change to the same object merges with the first; null takes one back.
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "opacity": 50})).unwrap();
assert_eq!(node(&s, inst).extras().symbol_overrides[0].opacity, Some(0.5));
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "opacity": null})).unwrap();
let o = node(&s, inst).extras().symbol_overrides[0].clone();
assert!(o.opacity.is_none() && o.fill.is_some());
// SVG export draws it too.
let svg = s.execute("document.export", &json!({"format": "svg"})).unwrap();
let svg = String::from_utf8(vectorcraft_format::base64_decode(svg["dataBase64"].as_str().unwrap()).unwrap()).unwrap();
assert!(svg.to_ascii_lowercase().contains("#ff0000") || svg.contains("rgb(255,0,0)"), "{svg}");
}
#[test]
fn static_symbols_and_bad_targets_are_refused() {
let mut s = session();
let inst = pair(&mut s, "static");
let set = |s: &mut Session, p: Value| s.execute("symbol.instance.setAppearance", &p);
assert!(set(&mut s, json!({"id": inst.0, "path": [0], "fill": "#ff0000"})).is_err(), "a static symbol");
s.execute("symbol.options", &json!({"name": "Pair", "type": "dynamic"})).unwrap();
for bad in [
json!({"id": inst.0, "path": [2], "fill": "#ff0000"}),
json!({"id": inst.0, "path": [0, 0], "fill": "#ff0000"}),
json!({"id": inst.0, "path": [-1], "fill": "#ff0000"}),
json!({"id": inst.0, "path": "0", "fill": "#ff0000"}),
json!({"id": inst.0, "fill": "#ff0000"}),
json!({"id": inst.0, "path": [0]}),
json!({"id": inst.0, "path": [0], "blend": "sparkle"}),
json!({"id": inst.0, "path": [0], "opacity": "half"}),
json!({"id": inst.0, "path": [0], "fill": {}}),
] {
assert!(set(&mut s, bad.clone()).is_err(), "{bad}");
}
let plain = square(&mut s, 120.0);
assert!(set(&mut s, json!({"id": plain.0, "path": [], "fill": "#ff0000"})).is_err(), "not an instance");
assert!(s.execute("symbol.options", &json!({"name": "Pair", "type": "sometimes"})).is_err());
assert_eq!(overrides(&s, inst), 0);
}
#[test]
fn redefining_the_symbol_keeps_the_changes_whose_objects_remain() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "fill": "#ff0000"})).unwrap();
// New geometry with the same structure: two squares, now 10..40 and 50..80 tall.
let (a, b) = (square(&mut s, 10.0), square(&mut s, 120.0));
s.execute("symbol.update", &json!({"name": "Pair", "ids": [a.0, b.0]})).unwrap();
assert_eq!(overrides(&s, inst), 1, "the change follows the edited symbol");
// Art of one object (no children) leaves path [1] behind.
let c = square(&mut s, 10.0);
s.execute("symbol.update", &json!({"name": "Pair", "ids": [c.0]})).unwrap();
assert_eq!(overrides(&s, inst), 0);
}
#[test]
fn reset_and_static_drop_the_changes_and_undo_brings_them_back() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [0], "fill": "#ff0000", "blend": "multiply"})).unwrap();
let r = s.execute("symbol.instance.reset", &json!({"ids": [inst.0]})).unwrap();
assert_eq!(r["ids"], json!([inst.0]));
assert_eq!(overrides(&s, inst), 0);
assert_eq!(pixel(&s, 30, 30), BLUE);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(overrides(&s, inst), 1);
// Nothing to reset: no undo step.
let other = id(&s.execute("symbol.place", &json!({"name": "Pair"})).unwrap());
let undo = s.doc().unwrap().history.undo.len();
assert_eq!(s.execute("symbol.instance.reset", &json!({"ids": [other.0]})).unwrap()["ids"], json!([]));
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
// Static again: the instances show the symbol's appearance.
s.execute("symbol.options", &json!({"name": "Pair", "type": "static"})).unwrap();
assert_eq!(overrides(&s, inst), 0);
}
#[test]
fn break_link_bakes_the_changes_and_edit_symbol_shows_the_symbols_own_art() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "fill": "#ff0000"})).unwrap();
let other = id(&s.execute("symbol.place", &json!({"name": "Pair", "x": 55, "y": 80})).unwrap());
s.execute("symbol.instance.setAppearance", &json!({"id": other.0, "path": [1], "fill": "#ff0000"})).unwrap();
let fills = |s: &Session, art: NodeId| -> Vec<String> {
node(s, art).children().unwrap().iter().map(|c| c.appearance.fill_paint().color().unwrap().to_hex()).collect()
};
let art = NodeId(s.execute("symbol.breakLink", &json!({"ids": [inst.0]})).unwrap()["ids"][0].as_u64().unwrap());
assert_eq!(fills(&s, art), ["#0000ff", "#ff0000"]);
let art = NodeId(s.execute("symbol.edit", &json!({"ids": [other.0]})).unwrap()["ids"][0].as_u64().unwrap());
assert_eq!(fills(&s, art), ["#0000ff", "#0000ff"], "the symbol's art, without this instance's changes");
}
#[test]
fn dynamic_symbols_and_their_changes_round_trip_and_old_documents_save_as_before() {
let mut s = session();
pair(&mut s, "static");
let json = serde_json::to_string(&s.doc().unwrap().doc).unwrap();
assert!(!json.contains("\"dynamic\"") && !json.contains("symbol_overrides"), "nothing new is written for static symbols");
let mut s = session();
let inst = pair(&mut s, "dynamic");
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "fill": "#ff0000", "opacity": 25})).unwrap();
let doc = &s.doc().unwrap().doc;
let back = vectorcraft_format::load(&vectorcraft_format::save(doc, false)).unwrap();
assert!(back.symbols[0].dynamic);
assert_eq!(back.node(inst).unwrap().extras().symbol_overrides, doc.node(inst).unwrap().extras().symbol_overrides);
assert_eq!(back.symbols, doc.symbols);
}
+1 -1
View File
@@ -274,7 +274,7 @@ fn symbols_bend_with_the_envelope() {
shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)),
vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
);
d.symbols.push(Symbol { name: "Box".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Box".into(), art: Arc::new(art) });
let id = d.alloc_id();
let xf = Affine::translate((200.0, 150.0)) * Affine::scale_non_uniform(10.0, 5.0);
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Box".into(), xf }))?;
+2 -2
View File
@@ -119,9 +119,9 @@ fn groups_layers_type_images_and_symbols_keep_their_raster_effects() {
// A symbol instance with a shadow, and a symbol whose art blurs.
let mut d = Document::new(200.0, 200.0);
let art = rect(&mut d, Rect::new(0.0, 0.0, 50.0, 50.0), Color::rgb(0.9, 0.6, 0.0));
d.symbols.push(Symbol { name: "Tile".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Tile".into(), art: Arc::new(art) });
let blurred = with(rect(&mut d, Rect::new(0.0, 0.0, 40.0, 40.0), Color::rgb(0.2, 0.2, 0.8)), blur());
d.symbols.push(Symbol { name: "Soft".into(), art: Arc::new(blurred) });
d.symbols.push(Symbol { dynamic: false, name: "Soft".into(), art: Arc::new(blurred) });
let inst = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Tile".into(), xf: Affine::translate((30.0, 30.0)) });
add(&mut d, with(inst, shadow()));
let soft = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Soft".into(), xf: Affine::translate((120.0, 120.0)) });
+6 -4
View File
@@ -32,7 +32,7 @@ fn star_source(path: &str, title: &str) {
d.title = title.into();
let layer = d.layers[0].id;
let art = Node::path(vectorcraft_doc::NodeId(900), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 50.0)), look(BLUE));
d.symbols.push(vectorcraft_doc::Symbol { name: "Star".into(), art: std::sync::Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Star".into(), art: std::sync::Arc::new(art) });
let id = d.alloc_id();
d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Star".into(), xf: vectorcraft_geom::Affine::IDENTITY })).unwrap();
write(path, &vectorcraft_format::save_file(&d));
@@ -45,9 +45,11 @@ fn look(c: [u8; 3]) -> Appearance {
/// The active document gets a red symbol named `Star` of its own.
fn parent_star(s: &mut Session) {
let star = Node::path(vectorcraft_doc::NodeId(901), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), look(RED));
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc)
.symbols
.push(vectorcraft_doc::Symbol { name: "Star".into(), art: std::sync::Arc::new(star) });
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).symbols.push(vectorcraft_doc::Symbol {
dynamic: false,
name: "Star".into(),
art: std::sync::Arc::new(star),
});
}
/// Place `path` (linked: the default), centred on the artboard → its id.
+2 -2
View File
@@ -380,13 +380,13 @@ fn an_object_has_the_art_of_at_most_4096_symbol_instances_drawn() {
let mut filled = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
filled.effects.push(Effect { id: "path.offsetPath".into(), params: json!({"offset": 1}), visible: true });
let square = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), filled);
d.symbols.push(Symbol { name: "S0".into(), art: Arc::new(square) });
d.symbols.push(Symbol { dynamic: false, name: "S0".into(), art: Arc::new(square) });
for k in 1..=12 {
let mut instance =
|dx: f64| Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: format!("S{}", k - 1), xf: Affine::translate((dx, 0.0)) }));
let pair = vec![instance(0.0), instance(20.0 * f64::from(1 << (k - 1)))];
let art = Node::group(d.alloc_id(), pair);
d.symbols.push(Symbol { name: format!("S{k}"), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: format!("S{k}"), art: Arc::new(art) });
}
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S12".into(), xf: Affine::IDENTITY });
let (layer, id) = (d.layers[0].id, instance.id);
+9 -9
View File
@@ -115,7 +115,7 @@ fn set_bounds_with_use_preview_bounds_keeps_the_strokes_in_the_symbol_at_their_w
let mut d = Document::new(200.0, 120.0);
let stroked = Appearance::basic(Paint::solid(Color::BLACK), Paint::solid(Color::BLACK), 2.0);
let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 8.0, 8.0)), stroked);
d.symbols.push(Symbol { name: "Box".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Box".into(), art: Arc::new(art) });
let mut instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Box".into(), xf: Affine::translate((50.0, 50.0)) });
*instance.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0);
let (layer, id) = (d.layers[0].id, instance.id);
@@ -136,7 +136,7 @@ fn set_bounds_with_use_preview_bounds_keeps_the_strokes_in_the_symbol_at_their_w
let mut round = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0);
round.stroke_mut().unwrap().join = vectorcraft_doc::LineJoin::Round;
let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 8.0, 8.0)), round);
d.symbols.push(Symbol { name: "Ring".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Ring".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Ring".into(), xf: Affine::translate((50.0, 50.0)) });
let (layer, id) = (d.layers[0].id, instance.id);
d.insert(Some(layer), 0, instance).unwrap();
@@ -164,7 +164,7 @@ fn set_bounds_sizes_an_instance_and_a_path_whose_strokes_differ() {
Node::path(d.alloc_id(), shapes::rectangle(Rect::new(at, at, at + 10.0, at + 10.0)), stroked)
};
let art = square(&mut d, 0.0, 10.0);
d.symbols.push(Symbol { name: "Thick".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Thick".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Thick".into(), xf: Affine::translate((50.0, 50.0)) });
let path = square(&mut d, 50.0, 2.0);
let (layer, ids) = (d.layers[0].id, [instance.id, path.id]);
@@ -202,7 +202,7 @@ fn set_bounds_measures_the_objects_in_an_instances_art_one_by_one() {
*art.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
*instance.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
}
d.symbols.push(Symbol { name: "Mixed".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Mixed".into(), art: Arc::new(art) });
let mut roots = vec![if grouped { Node::group(d.alloc_id(), vec![Arc::new(instance)]) } else { instance }];
if plain {
let filled = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
@@ -271,7 +271,7 @@ fn set_bounds_measures_an_instance_as_one_object_past_4096_objects_in_its_art()
art.push(Arc::new(Node::path(d.alloc_id(), shapes::rectangle(Rect::new(x, y, x + 1.0, y + 1.0)), square)));
}
let art = Node::group(d.alloc_id(), art);
d.symbols.push(Symbol { name: "Many".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Many".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Many".into(), xf: Affine::translate((0.0, 100.0)) });
let (layer, id) = (d.layers[0].id, instance.id);
d.insert(Some(layer), 0, instance).unwrap();
@@ -293,7 +293,7 @@ fn set_bounds_measures_a_turned_instance_by_the_box_around_its_turned_art_box()
let mut d = Document::new(400.0, 400.0);
let filled = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
let circle = Node::path(d.alloc_id(), shapes::ellipse(Rect::new(-10.0, -10.0, 10.0, 10.0)), filled);
d.symbols.push(Symbol { name: "Disc".into(), art: Arc::new(circle) });
d.symbols.push(Symbol { dynamic: false, name: "Disc".into(), art: Arc::new(circle) });
let xf = Affine::translate((100.0, 100.0)) * Affine::rotate(30f64.to_radians());
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Disc".into(), xf });
let (layer, id) = (d.layers[0].id, instance.id);
@@ -327,7 +327,7 @@ fn far_on_artboard_2() -> (Session, NodeId) {
let mut d = Document::new(800.0, 600.0);
let rect = shapes::rectangle(Rect::new(1100.0, 50.0, 1140.0, 70.0));
let art = Node::path(d.alloc_id(), rect, Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
d.symbols.push(Symbol { name: "Far".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Far".into(), art: Arc::new(art) });
let (layer, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((100.0, 100.0)) })).unwrap();
let mut s = Session::new();
@@ -437,7 +437,7 @@ fn an_artboards_svg_keeps_an_instance_whose_brush_art_is_on_it() {
let mut stroke = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
stroke.stroke_mut().unwrap().brush = Some("3 pt. Round".into());
let line = Node::path(d.alloc_id(), shapes::line((0.0, 40.0).into(), (0.0, 60.0).into()), stroke);
d.symbols.push(Symbol { name: "Nib".into(), art: Arc::new(line) });
d.symbols.push(Symbol { dynamic: false, name: "Nib".into(), art: Arc::new(line) });
let (layer, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Nib".into(), xf: Affine::translate((-5.0, 0.0)) })).unwrap();
let mut s = Session::new();
@@ -462,7 +462,7 @@ fn an_artboards_svg_keeps_an_instance_whose_shadow_is_on_it() {
} else {
instance.appearance.effects.push(shadow);
}
d.symbols.push(Symbol { name: "Dot".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Dot".into(), art: Arc::new(art) });
let layer = d.layers[0].id;
d.insert(Some(layer), 0, instance).unwrap();
let mut s = Session::new();
+1 -1
View File
@@ -247,7 +247,7 @@ fn a_file_with_type_sizes_out_of_range_opens_with_them_in_range() {
(h.size, h.h_scale, h.v_scale, h.baseline_shift, h.leading) = (f64::MAX, f64::MAX, f64::MAX, f64::MAX, Some(f64::MAX));
(k.size, k.h_scale, k.v_scale, k.baseline_shift, k.leading) = (0.001, 0.5, 0.5, 5000.0, Some(0.0));
let art = d.node(id).unwrap().clone();
d.symbols.push(vectorcraft_doc::Symbol { name: "Lines".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Lines".into(), art: Arc::new(art) });
let attrs = json!({"size": 0, "h_scale": 1e9, "v_scale": -1, "baseline_shift": -1e9, "leading": -1});
d.char_styles.push(vectorcraft_doc::TextStyleDef { name: "Broken".into(), attrs: attrs.as_object().unwrap().clone() });
let attrs = json!({"size": 40, "leading": null});
+2 -2
View File
@@ -4312,7 +4312,7 @@ fn brushed_document(kind: usize, art: usize) -> Document {
Node::path(doc.alloc_id(), square(), Appearance::basic(fill, Paint::None, 0.0))
};
let (in_symbol, in_pattern) = (brushed(&mut doc), brushed(&mut doc));
doc.symbols.push(Symbol { name: "s0".into(), art: std::sync::Arc::new(in_symbol) });
doc.symbols.push(Symbol { dynamic: false, name: "s0".into(), art: std::sync::Arc::new(in_symbol) });
doc.patterns.push(PatternDef::new("p0", vec![std::sync::Arc::new(in_pattern)]));
let art = match art {
0 => instance(&mut doc),
@@ -4502,7 +4502,7 @@ fn symbols_document(count: usize, levels: usize, last: &str) -> Document {
for _ in 0..levels {
art = Node::group(doc.alloc_id(), vec![std::sync::Arc::new(art)]);
}
doc.symbols.push(Symbol { name: format!("s{i}"), art: std::sync::Arc::new(art) });
doc.symbols.push(Symbol { dynamic: false, name: format!("s{i}"), art: std::sync::Arc::new(art) });
}
let first = instance(&mut doc, "s0".into());
let layer = doc.layers[0].id;
+1 -1
View File
@@ -278,7 +278,7 @@ impl<'a> Writer<'a> {
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
&& self.nest < MAX_NEST
{
let mut art = (*sym.art).clone();
let mut art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
art.transform(*xf, false);
art.visible = true;
self.nest += 1;
+1 -1
View File
@@ -313,7 +313,7 @@ impl<'a> Scene<'a> {
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
&& self.nest < MAX_NEST
{
let mut art = (*sym.art).clone();
let mut art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
art.transform(*xf, false);
self.nest += 1;
self.node(&art, true);
+2 -2
View File
@@ -633,7 +633,7 @@ fn an_instance_whose_art_reaches_the_region_is_written() {
// while the 20 × 20 square around the origin, moved to (-60, -60) to (-40, -40), is off it.
let mut d = doc_with(|d| vec![Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((-50.0, -50.0)) })]);
let art = path(&mut d, shapes::rectangle(Rect::new(100.0, 100.0, 140.0, 140.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
d.symbols.push(vectorcraft_doc::Symbol { name: "Far".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Far".into(), art: Arc::new(art) });
let page = page_of(&d, &opts(&d));
assert!(page.contains("1 0 0 rg"), "{page}");
}
@@ -652,7 +652,7 @@ fn an_instance_whose_brush_art_reaches_the_region_is_written() {
vectorcraft_brush::store(&mut d, &lib);
let mut art = path(&mut d, shapes::line((0.0, 40.0).into(), (0.0, 60.0).into()), Paint::None, Paint::solid(Color::rgb(1.0, 0.0, 0.0)), 1.0);
art.appearance.stroke_mut().unwrap().brush = Some("3 pt. Round".into());
d.symbols.push(vectorcraft_doc::Symbol { name: "Nib".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Nib".into(), art: Arc::new(art) });
let page = page_of(&d, &opts(&d));
assert!(page.contains("1 0 0 rg"), "{page}");
}
+1 -1
View File
@@ -129,7 +129,7 @@ fn unused_images_are_pruned() {
n.extras_mut().mask = Some(Box::new(OpacityMask::new(image_node(&mut d, "in mask"), false)));
d.insert(Some(layer), 0, n).unwrap();
let art = image_node(&mut d, "in symbol");
d.symbols.push(Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
// Image objects inside foreign data (a brush library kept as JSON) keep theirs too.
d.unknown.insert("brushes".into(), json!([{"art": {"type": "image", "key": "in brush"}}]));
let back = load(&save(&d, true)).unwrap();
+1 -1
View File
@@ -200,7 +200,7 @@ fn art_outside_the_layers_counts_too() {
let mut art = paths_doc(1, anchors_for(1, 2 * PRETTY_MAX_BYTES));
let path = Arc::clone(&art.layers[0]);
art.layers.clear();
d.symbols.push(Symbol { name: "Leaves".into(), art: path });
d.symbols.push(Symbol { dynamic: false, name: "Leaves".into(), art: path });
assert!(!SaveOptions::for_doc(&d).pretty);
let bytes = save_file(&d);
assert!(!is_pretty(&bytes));
+1 -1
View File
@@ -217,7 +217,7 @@ impl<'a> Scene<'a> {
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
&& self.nest < MAX_NEST
{
let mut art = (*sym.art).clone();
let mut art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
art.transform(*xf, false);
self.nest += 1;
self.node(&art, true);
+1 -1
View File
@@ -393,7 +393,7 @@ fn an_instance_whose_art_reaches_the_artboard_is_written() {
let inst = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((-50.0, -50.0)) });
let mut d = doc_with(100.0, 100.0, vec![inst]);
let art = path(shapes::rectangle(Rect::new(100.0, 100.0, 140.0, 140.0)), red(), Paint::None, 0.0);
d.symbols.push(vectorcraft_doc::Symbol { name: "Far".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Far".into(), art: Arc::new(art) });
let k = kinds(&emf_records(&emf(&d).bytes));
assert!(k.contains(&39), "a brush fills the art: {k:?}");
}
+1 -1
View File
@@ -217,7 +217,7 @@ impl Exporter<'_> {
let total = sym.map_or(0, |sym| self.symbol_nodes.saturating_add(sym.art.count() as u64));
if let Some(sym) = sym.filter(|_| total <= vectorcraft_doc::MAX_SYMBOL_NODES) {
self.symbol_nodes = total;
let mut art = (*sym.art).clone();
let mut art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
art.transform(*xf, false);
self.node(s, &art, page, true);
}
+2 -2
View File
@@ -194,7 +194,7 @@ fn an_instance_whose_art_reaches_the_page_is_written() {
// the 20 × 20 square around the origin, moved to (-60, -60) to (-40, -40), is off it.
let mut d = doc(200.0, 100.0);
let red = rect_node(Rect::new(100.0, 50.0, 140.0, 90.0), Color::rgb(1.0, 0.0, 0.0));
let far = vectorcraft_doc::Symbol { name: "Far".into(), art: Arc::new(red) };
let far = vectorcraft_doc::Symbol { dynamic: false, name: "Far".into(), art: Arc::new(red) };
d.symbols.push(far.clone());
let instance = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((-50.0, -50.0)) });
add(&mut d, instance.clone());
@@ -228,7 +228,7 @@ fn an_instance_whose_brush_art_reaches_the_page_is_written() {
let mut stroke = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
stroke.stroke_mut().unwrap().brush = Some("3 pt. Round".into());
let line = Node::path(NodeId(0), shapes::line((0.0, 40.0).into(), (0.0, 60.0).into()), stroke);
d.symbols.push(vectorcraft_doc::Symbol { name: "Nib".into(), art: Arc::new(line) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Nib".into(), art: Arc::new(line) });
add(&mut d, Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Nib".into(), xf: Affine::translate((-5.0, 0.0)) }));
let out = roundtrip(&d);
let l = leaves(&out);
+2 -2
View File
@@ -265,7 +265,7 @@ fn a_symbol_instance_prints_by_its_art() {
let mut d = doc(100.0, 100.0, &[]);
let red = Appearance::basic(rgb(1.0, 0.0, 0.0), Paint::None, 0.0);
let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(100.0, 100.0, 140.0, 140.0)), red);
d.symbols.push(vectorcraft_doc::Symbol { name: "Far".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Far".into(), art: Arc::new(art) });
let (layer, id) = (d.default_layer().unwrap(), d.alloc_id());
let xf = vectorcraft_geom::Affine::translate((-50.0, -50.0));
d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Far".into(), xf })).unwrap();
@@ -293,7 +293,7 @@ fn a_symbol_instances_brush_art_keeps_its_artboard_printed() {
let mut stroke = Appearance::basic(Paint::None, rgb(1.0, 0.0, 0.0), 1.0);
stroke.stroke_mut().unwrap().brush = Some("3 pt. Round".into());
let art = Node::path(d.alloc_id(), shapes::line((0.0, 40.0).into(), (0.0, 60.0).into()), stroke);
d.symbols.push(vectorcraft_doc::Symbol { name: "Nib".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Nib".into(), art: Arc::new(art) });
let (layer, id) = (d.default_layer().unwrap(), d.alloc_id());
let xf = vectorcraft_geom::Affine::translate((-5.0, 0.0));
d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Nib".into(), xf })).unwrap();
+3 -3
View File
@@ -370,7 +370,7 @@ mod tests {
shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)),
Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: std::sync::Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: std::sync::Arc::new(art) });
let id = d.alloc_id();
let mut inst = Node::new(id, vectorcraft_doc::NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((50.0, 50.0)) });
inst.appearance.set_fill(Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
@@ -394,7 +394,7 @@ mod tests {
let mut ap = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
ap.stroke_mut().unwrap().brush = Some("3 pt. Round".into());
let art = Node::path(vectorcraft_doc::NodeId(0), shapes::line((0.0, 40.0).into(), (0.0, 60.0).into()), ap);
d.symbols.push(vectorcraft_doc::Symbol { name: "Nib".into(), art: std::sync::Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Nib".into(), art: std::sync::Arc::new(art) });
let id = d.alloc_id();
let inst = Node::new(id, vectorcraft_doc::NodeKind::SymbolInstance { symbol: "Nib".into(), xf: Affine::translate((-6.0, 0.0)) });
let l = d.layers[0].id;
@@ -424,7 +424,7 @@ mod tests {
let moved = Affine::translate((-8.0, 0.0));
let source = if instance {
let art = line(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "Nib".into(), art: std::sync::Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Nib".into(), art: std::sync::Arc::new(art) });
Node::new(d.alloc_id(), vectorcraft_doc::NodeKind::SymbolInstance { symbol: "Nib".into(), xf: moved })
} else {
let mut n = line(&mut d);
+1 -1
View File
@@ -236,7 +236,7 @@ fn art_bounds_take_in_the_effects_in_a_symbols_art() {
let mut art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), black);
let shadow = vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: serde_json::json!({"x": 8, "y": 0, "blur": 0}), visible: true };
art.appearance.effects.push(shadow);
d.symbols.push(vectorcraft_doc::Symbol { name: "Box".into(), art: std::sync::Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Box".into(), art: std::sync::Arc::new(art) });
let (id, l) = (d.alloc_id(), d.layers[0].id);
let xf = vectorcraft_geom::Affine::translate((20.0, 20.0));
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Box".into(), xf })).unwrap();
+2 -2
View File
@@ -479,7 +479,7 @@ fn thirty_stacked_glows_draw_quickly_cached_or_not() {
shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 40.0)),
Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
);
instanced.symbols.push(vectorcraft_doc::Symbol { name: "Square".into(), art: std::sync::Arc::new(art) });
instanced.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Square".into(), art: std::sync::Arc::new(art) });
let mut instance =
Node::new(instanced.alloc_id(), vectorcraft_doc::NodeKind::SymbolInstance { symbol: "Square".into(), xf: Affine::translate((30.0, 30.0)) });
instance.appearance.effects = glows.into_iter().map(|(id, p)| Effect { id: id.into(), params: p, visible: true }).collect();
@@ -754,7 +754,7 @@ fn a_moved_group_draws_the_shadow_of_the_instance_art_in_it_while_interacting()
let mut base = Document::new(100.0, 100.0);
let orange = Appearance::basic(Paint::solid(Color::rgb(0.9, 0.4, 0.1)), Paint::None, 0.0);
let art = Node::path(base.alloc_id(), shapes::rectangle(Rect::new(100.0, 100.0, 130.0, 126.0)), orange);
base.symbols.push(vectorcraft_doc::Symbol { name: "Far".into(), art: std::sync::Arc::new(art) });
base.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Far".into(), art: std::sync::Arc::new(art) });
// The instance draws the art from (20, 20) to (50, 46), and the 20 × 20 square around the
// origin from (-90, -90) to (-70, -70).
let xf = Affine::translate((-80.0, -80.0));
+2 -2
View File
@@ -771,7 +771,7 @@ mod tests {
true => Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: format!("S{}", k + 1), xf: Affine::IDENTITY }),
false => square(&mut d),
};
d.symbols.push(Symbol { name: format!("S{k}"), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: format!("S{k}"), art: Arc::new(art) });
}
let first = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S0".into(), xf: Affine::IDENTITY });
let kind = EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true };
@@ -805,7 +805,7 @@ mod tests {
};
let (x, y) = (square(&mut d, 0.0), square(&mut d, 20.0));
for (name, art) in [("A", &x), ("B", &y), ("C", &x)] {
d.symbols.push(Symbol { name: name.into(), art: art.clone() });
d.symbols.push(Symbol { dynamic: false, name: name.into(), art: art.clone() });
}
let content = ["A", "B", "C"].map(|s| Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: s.into(), xf: Affine::IDENTITY })));
let kind = EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true };
+3 -1
View File
@@ -56,7 +56,9 @@ impl Renderer {
/// placed again when the fonts change.
pub(super) fn placed_art(&mut self, art: &Arc<Node>, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> (Arc<Node>, bool) {
let kept_kind = matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || vectorcraft_doc::live::is_live(art);
if !kept_kind || fx::has_object_fx(art) {
// An instance with appearance changes of its own isn't kept: the key doesn't tell them apart.
let overridden = inst.is_some_and(|i| !i.extras().symbol_overrides.is_empty());
if !kept_kind || overridden || fx::has_object_fx(art) {
return (place(art, xf, scale_strokes, inst), false);
}
let stain = stain_bits(inst.and_then(vectorcraft_brush::stain));
+10 -10
View File
@@ -220,7 +220,7 @@ fn hidden_and_culled() {
fn far_instance(art: Rect) -> Document {
let mut d = Document::new(100.0, 100.0);
let art = rect_node(&mut d, art, Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
d.symbols.push(vectorcraft_doc::Symbol { name: "Far".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Far".into(), art: Arc::new(art) });
let (id, l) = (d.alloc_id(), d.layers[0].id);
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((-50.0, -50.0)) })).unwrap();
d
@@ -250,7 +250,7 @@ fn an_instance_is_culled_by_the_effects_in_its_art() {
let mut art = rect_node(&mut d, Rect::new(-1.0, -1.0, 1.0, 1.0), Paint::solid(Color::BLACK), Paint::None, 0.0);
let shadow = vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: serde_json::json!({"x": 8, "y": 0, "blur": 0}), visible: true };
art.appearance.effects.push(shadow);
d.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Dot".into(), art: Arc::new(art) });
let (id, l) = (d.alloc_id(), d.layers[0].id);
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Dot".into(), xf: Affine::translate((-5.0, 20.0)) })).unwrap();
let img = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions::default());
@@ -264,7 +264,7 @@ fn a_repeat_of_an_instance_is_culled_by_the_effects_in_its_art() {
let mut art = rect_node(&mut d, Rect::new(-1.0, -1.0, 1.0, 1.0), Paint::solid(Color::BLACK), Paint::None, 0.0);
let shadow = vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: serde_json::json!({"x": 8, "y": 0, "blur": 0}), visible: true };
art.appearance.effects.push(shadow);
d.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Dot".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Dot".into(), xf: Affine::translate((-5.0, 20.0)) });
let grid = vectorcraft_doc::TileType::Grid;
let kind =
@@ -287,11 +287,11 @@ fn past_the_measured_placements_an_instance_is_culled_by_an_estimate_of_its_art_
let mut art = rect_node(&mut d, Rect::new(-1.0, -1.0, 1.0, 1.0), Paint::solid(Color::BLACK), Paint::None, 0.0);
let shadow = vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: serde_json::json!({"x": 8, "y": 0, "blur": 0}), visible: true };
art.appearance.effects.push(shadow);
d.symbols.push(vectorcraft_doc::Symbol { name: "Spot".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Spot".into(), art: Arc::new(art) });
for name in ["Dot", "Late"] {
let spot = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Spot".into(), xf: Affine::IDENTITY });
let group = Node::group(d.alloc_id(), vec![Arc::new(spot)]);
d.symbols.push(vectorcraft_doc::Symbol { name: name.into(), art: Arc::new(group) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: name.into(), art: Arc::new(group) });
}
let l = d.layers[0].id;
for i in 0..4100 {
@@ -317,7 +317,7 @@ fn nested_instances_are_culled_in_bounded_time(effect: Option<vectorcraft_doc::E
let mut art = rect_node(&mut d, Rect::new(0.0, 0.0, 1.0, 1.0), Paint::solid(Color::BLACK), Paint::None, 0.0);
let shadow = vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: serde_json::json!({"x": 2, "y": 0, "blur": 0}), visible: true };
art.appearance.effects.push(shadow);
d.symbols.push(vectorcraft_doc::Symbol { name: "S30".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S30".into(), art: Arc::new(art) });
let instance = |d: &mut Document, symbol: String, xf: Affine| {
let mut n = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol, xf });
n.appearance.effects.extend(effect.clone());
@@ -330,7 +330,7 @@ fn nested_instances_are_culled_in_bounded_time(effect: Option<vectorcraft_doc::E
pair.push(Arc::new(instance(&mut d, next.clone(), xf)));
}
let group = Node::group(d.alloc_id(), pair);
d.symbols.push(vectorcraft_doc::Symbol { name: format!("S{i}"), art: Arc::new(group) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: format!("S{i}"), art: Arc::new(group) });
}
let first = instance(&mut d, "S0".into(), Affine::translate((-100_000.0, 0.0)));
let l = d.layers[0].id;
@@ -369,7 +369,7 @@ fn a_scaled_instance_is_culled_by_the_effects_in_its_art_as_placed() {
let mut art = rect_node(&mut d, Rect::new(-1.0, -1.0, 1.0, 1.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
let double = vectorcraft_doc::Effect { id: "distort.transform".into(), params: serde_json::json!({"scaleH": 200, "scaleV": 200}), visible: true };
art.appearance.effects.push(double);
d.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Dot".into(), art: Arc::new(art) });
let (id, l) = (d.alloc_id(), d.layers[0].id);
let xf = Affine::translate((-50.0, 50.0)) * Affine::scale(30.0);
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Dot".into(), xf })).unwrap();
@@ -401,7 +401,7 @@ fn an_instance_with_a_geometry_effect_is_culled_by_the_effects_in_its_reshaped_a
let effect = || vectorcraft_doc::Effect { id: "distort.transform".into(), params: scale.clone(), visible: true };
let mut art = rect_node(&mut d, Rect::new(-1.0, -1.0, 1.0, 1.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
art.appearance.effects.push(effect());
d.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Dot".into(), art: Arc::new(art) });
let (id, l) = (d.alloc_id(), d.layers[0].id);
let mut instance = Node::new(id, NodeKind::SymbolInstance { symbol: "Dot".into(), xf: Affine::translate((-11.0, 20.0)) });
instance.appearance.effects.push(effect());
@@ -845,7 +845,7 @@ fn placed_symbol_art_root_applies_its_own_mask() {
let mut art = if grouped { Node::group(d.alloc_id(), vec![Arc::new(red)]) } else { red };
let inner = rect_node(&mut d, Rect::new(10.0, 10.0, 50.0, 90.0), Paint::solid(Color::WHITE), Paint::None, 0.0);
art.extras_mut().mask = Some(Box::new(OpacityMask::new(inner, true)));
d.symbols.push(vectorcraft_doc::Symbol { name: "Masked".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Masked".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Masked".into(), xf: Affine::IDENTITY });
d.insert(Some(d.layers[0].id), 0, instance).unwrap();
+1 -1
View File
@@ -112,7 +112,7 @@ fn an_embedded_image_is_recoloured_and_the_original_pixels_stay() {
fn a_symbol_instance_recolours_its_art() {
let mut d = Document::new(100.0, 100.0);
let art = red_square(-15.0, -15.0);
d.symbols.push(Symbol { name: "Red".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Red".into(), art: Arc::new(art) });
let mut n = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Red".into(), xf: Affine::translate((50.0, 50.0)) });
n.appearance.effects.push(hue());
let l = d.layers[0].id;
+1 -1
View File
@@ -220,7 +220,7 @@ fn an_unchanged_document_changes_nothing_and_a_new_symbol_everything() {
assert_eq!(redraw(&old, &old.clone()), Changed::Nothing);
let mut new = old.clone();
let art = Arc::new(rect(&mut new, Rect::new(0.0, 0.0, 5.0, 5.0), Color::BLACK));
new.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art });
new.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Dot".into(), art });
assert_eq!(redraw(&old, &new), Changed::Everything);
}
+1 -1
View File
@@ -118,7 +118,7 @@ fn the_transparency_test_is_kept_while_the_art_stays_the_same() {
// A symbol whose art shows transparency, placed nowhere, makes the page draw by ink planes again.
let mut art = rect(&mut e, Rect::new(0.0, 0.0, 5.0, 5.0), CYAN);
art.opacity = 0.5;
e.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
e.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
assert!(frame(&mut r, &e) == frame(&mut Renderer::new(), &e));
assert_eq!(kept(&r).map(|(_, shows)| shows), Some(true));
}
+3 -3
View File
@@ -162,7 +162,7 @@ fn symbol_type_redraws_when_its_font_becomes_available() {
let mut t = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(-140.0, 25.0), "Hamburg", style))));
t.appearance.effects.push(vectorcraft_doc::Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 0.0}), visible: true });
let art = Node::group(d.alloc_id(), vec![Arc::new(t)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Word".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Word".into(), art: Arc::new(art) });
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Word".into(), xf: Affine::translate((150.0, 75.0)) })).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
@@ -207,7 +207,7 @@ fn type_with_geometry_effects_redraws_when_its_font_becomes_available() {
}
3 => {
let art = Node::group(d.alloc_id(), vec![Arc::new(object)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Word".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Word".into(), art: Arc::new(art) });
object = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Word".into(), xf: Affine::translate((150.0, 75.0)) });
object.appearance.effects.push(moved());
}
@@ -243,7 +243,7 @@ fn enveloped_type_redraws_when_its_font_becomes_available() {
let typed = word(&mut d, Point::new(10.0, 60.0));
let art = word(&mut d, Point::new(-140.0, 20.0));
let art = Node::group(d.alloc_id(), vec![Arc::new(art)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Word".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Word".into(), art: Arc::new(art) });
let shown = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Word".into(), xf: Affine::translate((150.0, 120.0)) });
let l = d.layers[0].id;
for (i, content) in [typed, shown].into_iter().enumerate() {
+33 -33
View File
@@ -389,7 +389,7 @@ fn type_with_an_inline_graphic_draws_its_symbols_new_size() {
for i in 0..4 {
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let style = CharStyle { size: 24.0, ..Default::default() };
let mut t = TextObject::point(vectorcraft_geom::Point::new(10.0, 50.0), "A", style.clone());
t.runs.push(TextRun::inline(InlineArt::new("S"), style.clone()));
@@ -428,7 +428,7 @@ fn symbol_art(d: &mut Document) -> Node {
fn symbol_doc() -> Document {
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let l = d.layers[0].id;
let xfs = [
Affine::translate((30.0, 30.0)),
@@ -579,9 +579,9 @@ fn art_placed_again_in_every_frame_keeps_one_outline_per_stroke() {
if let Some(c) = art.children_mut() {
c.push(Arc::new(image));
}
d.symbols.push(vectorcraft_doc::Symbol { name: "I".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "I".into(), art: Arc::new(art) });
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let l = d.layers[0].id;
let instance = |d: &mut Document, symbol: &str, x: f64, y: f64| {
let id = d.alloc_id();
@@ -707,7 +707,7 @@ fn strokes_of_brush_art_draw_as_a_new_renderer_does() {
// Three instances show symbol `B`, a group of a wave. A fourth wave is a path of the document,
// and a Mirror repeat draws a fifth wave and its reflection.
let art = Node::group(d.alloc_id(), vec![Arc::new(wave(&mut d, (0.0, 0.0)))]);
d.symbols.push(vectorcraft_doc::Symbol { name: "B".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "B".into(), art: Arc::new(art) });
let l = d.layers[0].id;
for at in [(50.0, 30.0), (150.0, 30.0), (50.0, 70.0)] {
let id = d.alloc_id();
@@ -755,10 +755,10 @@ fn brushed_copies_doc() -> Document {
use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun};
let mut d = Document::new(200.0, 100.0);
let line = brushed_line(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "Line".into(), art: Arc::new(line) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Line".into(), art: Arc::new(line) });
let line = brushed_line(&mut d);
let held = Node::group(d.alloc_id(), vec![Arc::new(line)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Held".into(), art: Arc::new(held) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Held".into(), art: Arc::new(held) });
let l = d.layers[0].id;
for (symbol, at) in [("Line", (20.0, 15.0)), ("Line", (60.0, 15.0)), ("Line", (100.0, 15.0)), ("Held", (20.0, 45.0)), ("Held", (60.0, 45.0))] {
let id = d.alloc_id();
@@ -856,7 +856,7 @@ fn brush_art_draws_edits_of_the_symbols_and_patterns_it_shows() {
let doc = |dot_brushed: bool, fill_brushed: bool| {
let mut d = Document::new(200.0, 100.0);
let dot = art(&mut d, Paint::solid(Color::rgb(0.0, 0.0, 1.0)), dot_brushed);
d.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art: Arc::new(dot) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Dot".into(), art: Arc::new(dot) });
let fill = art(&mut d, Paint::solid(Color::rgb(0.0, 0.6, 0.0)), fill_brushed);
let mut def = PatternDef::new("Fill", vec![Arc::new(fill)]);
def.tile = Rect::new(-6.0, -6.0, 6.0, 6.0);
@@ -917,7 +917,7 @@ fn brush_art_draws_renamed_symbols_and_patterns_it_shows() {
[("Dot", Paint::solid(Color::rgb(0.0, 0.0, 1.0)), false), ("Other", Paint::solid(Color::rgb(1.0, 0.0, 0.0)), true)]
{
let a = art(&mut d, paint, brushed);
d.symbols.push(vectorcraft_doc::Symbol { name: name.into(), art: Arc::new(a) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: name.into(), art: Arc::new(a) });
}
for (name, paint, brushed) in
[("Fill", Paint::solid(Color::rgb(0.0, 0.6, 0.0)), false), ("Spare", Paint::solid(Color::rgb(1.0, 0.0, 0.0)), true)]
@@ -978,7 +978,7 @@ fn live_objects_in_brush_art_keep_their_evaluations_between_frames() {
};
let held = bent(&mut d, (-60.0, 0.0), (60.0, 0.0));
let held = Node::group(d.alloc_id(), vec![Arc::new(held)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Wave".into(), art: Arc::new(held) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Wave".into(), art: Arc::new(held) });
let path = bent(&mut d, (20.0, 25.0), (180.0, 25.0));
let l = d.layers[0].id;
d.insert(Some(l), usize::MAX, path).unwrap();
@@ -1006,7 +1006,7 @@ fn editing_an_instance_or_a_symbol_places_only_its_art_again() {
let green = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(0.0, 1.0, 0.0)), Paint::None, 0.0);
let square = Node::path(d.alloc_id(), vectorcraft_geom::shapes::rectangle(Rect::new(-5.0, -5.0, 5.0, 5.0)), green);
let t = Node::group(d.alloc_id(), vec![Arc::new(square)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "T".into(), art: Arc::new(t) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "T".into(), art: Arc::new(t) });
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "T".into(), xf: Affine::translate((30.0, 80.0)) })).unwrap();
let opts = RenderOptions::default();
@@ -1056,7 +1056,7 @@ fn instances_of_art_outside_the_square_follow_their_symbols_art() {
// the one from x 200 lies from (40, 40) to (60, 60), in it. The 20 × 20 square around the
// origin lies from (-170, 30) to (-150, 50), left of the view.
let (off, on) = (square_at(&mut d, 100.0), square_at(&mut d, 200.0));
d.symbols.push(vectorcraft_doc::Symbol { name: "Far".into(), art: off.clone() });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Far".into(), art: off.clone() });
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((-160.0, 40.0)) })).unwrap();
let opts = RenderOptions::default();
@@ -1098,7 +1098,7 @@ fn draws_as_new(d: &mut Document, before: &RenderOptions, after: &RenderOptions,
/// `d` with symbol `Holder` (a group of `members`) shown by one instance at (100, 50).
fn holder_doc(mut d: Document, members: Vec<Node>) -> Document {
let holder = Node::group(d.alloc_id(), members.into_iter().map(Arc::new).collect());
d.symbols.push(vectorcraft_doc::Symbol { name: "Holder".into(), art: Arc::new(holder) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Holder".into(), art: Arc::new(holder) });
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Holder".into(), xf: Affine::translate((100.0, 50.0)) })).unwrap();
d
@@ -1113,7 +1113,7 @@ fn art_showing_another_symbol_draws_its_edits() {
use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun};
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let style = CharStyle { size: 24.0, ..Default::default() };
let mut t = TextObject::point(vectorcraft_geom::Point::new(-60.0, 10.0), "A", style.clone());
t.runs.push(TextRun::inline(InlineArt::new("S"), style.clone()));
@@ -1234,7 +1234,7 @@ fn live_objects_in_symbol_art_keep_their_evaluations_between_frames() {
for (name, art, at) in
[("Bent", bent, (40.0, 25.0)), ("Envelope", envelope, (100.0, 25.0)), ("Blend", blend, (160.0, 25.0)), ("Grid", grid, (20.0, 60.0))]
{
d.symbols.push(vectorcraft_doc::Symbol { name: name.into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: name.into(), art: Arc::new(art) });
let id = d.alloc_id();
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: name.into(), xf: Affine::translate(at) })).unwrap();
}
@@ -1298,7 +1298,7 @@ fn live_objects_in_symbol_art_draw_symbol_and_pattern_edits() {
for bricks in [false, true] {
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let xf = Affine::translate((10.0, 10.0)) * Affine::scale(0.5);
let dot = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf });
let mut dots = PatternDef::new("Dots", vec![Arc::new(dot)]);
@@ -1307,7 +1307,7 @@ fn live_objects_in_symbol_art_draw_symbol_and_pattern_edits() {
let band = filled(&mut d, rectangle(Rect::new(-80.0, -30.0, 80.0, 30.0)), pattern_paint("Dots"), vec![]);
let envelope = arc_envelope(&mut d, vec![band], true);
let art = if grouped { Node::group(d.alloc_id(), vec![Arc::new(envelope)]) } else { envelope };
d.symbols.push(vectorcraft_doc::Symbol { name: "Bent".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Bent".into(), art: Arc::new(art) });
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Bent".into(), xf: Affine::translate((100.0, 50.0)) })).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
@@ -1401,7 +1401,7 @@ fn live_objects_draw_symbol_and_pattern_art_edits() {
let with_s = || {
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
d
};
let red_bar = |d: &mut Document| {
@@ -1415,7 +1415,7 @@ fn live_objects_draw_symbol_and_pattern_art_edits() {
let mut d = with_s();
let shown = instance(&mut d, "S", Affine::IDENTITY);
let a = Node::group(d.alloc_id(), vec![Arc::new(shown)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "A".into(), art: Arc::new(a) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "A".into(), art: Arc::new(a) });
let a = instance(&mut d, "A", Affine::translate((100.0, 50.0)) * Affine::scale(2.0));
let envelope = arc_envelope(&mut d, vec![a], false);
edit_draws_as_new(with_art(d, vec![envelope]), red_bar);
@@ -1430,7 +1430,7 @@ fn live_objects_draw_symbol_and_pattern_art_edits() {
let mut d = Document::new(200.0, 100.0);
d.patterns.push(checks(Color::rgb(1.0, 0.0, 0.0)));
let band = filled(&mut d, rectangle(Rect::new(-80.0, -30.0, 80.0, 30.0)), pattern_paint("Checks"), vec![]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Banded".into(), art: Arc::new(band) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Banded".into(), art: Arc::new(band) });
let banded = instance(&mut d, "Banded", Affine::translate((100.0, 50.0)));
let envelope = arc_envelope(&mut d, vec![banded], true);
edit_draws_as_new(with_art(d, vec![envelope]), |d| {
@@ -1457,7 +1457,7 @@ fn symbol_in_live_and_effect_art() -> Document {
use vectorcraft_geom::shapes::rectangle;
let mut d = Document::new(500.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let bent = |d: &mut Document, x: f64| {
let xf = Affine::translate((x, 50.0)) * Affine::scale(2.0);
let s = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf });
@@ -1470,7 +1470,7 @@ fn symbol_in_live_and_effect_art() -> Document {
group.appearance.effects.push(drop_shadow());
let copy = symbol_art(&mut d);
let art = arc_envelope(&mut d, vec![copy], false);
d.symbols.push(vectorcraft_doc::Symbol { name: "Bent".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Bent".into(), art: Arc::new(art) });
let xf = Affine::translate((250.0, 50.0)) * Affine::scale(2.0);
let symbol = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Bent".into(), xf });
let mask = bent(&mut d, 350.0);
@@ -1603,7 +1603,7 @@ fn objects_with_plugin_effects_follow_the_plugins_installed() {
let mut d = Document::new(200.0, 100.0);
let art = square(&mut d, -20.0, -20.0, vec![]);
let art = Node::group(d.alloc_id(), vec![Arc::new(art)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Shaped".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Shaped".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shaped".into(), xf: Affine::translate((100.0, 50.0)) });
plugin_change_draws_as_new(with_art(d, vec![instance]), ID, small.clone(), big);
// Left of the view, the square is culled until the plug-in draws it in the view.
@@ -1616,7 +1616,7 @@ fn objects_with_plugin_effects_follow_the_plugins_installed() {
// effects in the art paint past the art, which the plug-in's first shape does not.
let mut d = Document::new(200.0, 100.0);
let art = square(&mut d, -20.0, -20.0, vec![]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Shaped".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Shaped".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shaped".into(), xf: Affine::translate((-30.0, 50.0)) });
plugin_change_draws_as_new(with_art(d, vec![instance]), ID, Some(triangle(-15.0, -15.0, 10.0)), Some(triangle(0.0, 0.0, 150.0)));
}
@@ -1676,7 +1676,7 @@ fn effect_art_follows_the_plugins_installed() {
plugin_change_draws_as_new(group(vec![feather]), ID, small.clone(), big.clone());
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let mut instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((100.0, 50.0)) });
instance.appearance.effects.push(plugin());
plugin_change_draws_as_new(with_art(d, vec![instance]), ID, small.clone(), big.clone());
@@ -1711,7 +1711,7 @@ fn live_objects_follow_the_plugins_installed() {
// Symbol `Shaped` is a square with the plug-in effect.
let mut d = Document::new(200.0, 100.0);
let art = black(&mut d, Rect::new(-20.0, -20.0, 20.0, 20.0));
d.symbols.push(vectorcraft_doc::Symbol { name: "Shaped".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Shaped".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shaped".into(), xf: Affine::translate((100.0, 50.0)) });
let envelope = arc_envelope(&mut d, vec![instance], true);
plugin_change_draws_as_new(with_art(d, vec![envelope]), ID, small.clone(), big.clone());
@@ -1753,7 +1753,7 @@ fn pattern_and_symbol_art_with_plugin_effects_follow_the_plugins_installed() {
// the instance's box, from (80, 30) to (120, 70).
let mut d = Document::new(200.0, 100.0);
let art = filled(&mut d, rectangle(Rect::new(-20.0, -20.0, 20.0, 20.0)), Paint::solid(Color::BLACK), vec![plugin()]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Shaped".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Shaped".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shaped".into(), xf: Affine::translate((100.0, 50.0)) });
let mut group = Node::group(d.alloc_id(), vec![Arc::new(instance)]);
group.appearance.effects.push(drop_shadow());
@@ -1839,7 +1839,7 @@ fn pattern_tiles_draw_edits_to_what_their_art_shows() {
let doc = |pattern: &str, effects: Vec<vectorcraft_doc::Effect>| {
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let shown = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((10.0, 10.0)) });
d.patterns.push(tile("Stamps", shown));
d.patterns.push(checks(Color::rgb(1.0, 0.0, 0.0)));
@@ -1890,10 +1890,10 @@ fn rasters_draw_symbol_brush_and_image_edits() {
// A group of an instance of symbol `A`, whose art shows symbol `S`, and a green square.
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let shown = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::IDENTITY });
let a = Node::group(d.alloc_id(), vec![Arc::new(shown)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "A".into(), art: Arc::new(a) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "A".into(), art: Arc::new(a) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "A".into(), xf: Affine::translate((50.0, 50.0)) });
let green = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(0.0, 0.8, 0.0)), Paint::None, 0.0);
let square = Node::path(d.alloc_id(), rectangle(Rect::new(120.0, 30.0, 160.0, 70.0)), green);
@@ -2067,7 +2067,7 @@ fn proof_doc() -> Document {
let spot = filled(&mut d, rectangle(Rect::new(110.0, 10.0, 150.0, 50.0)), spot, vec![blur]);
let yellow = filled(&mut d, rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)), Paint::solid(Color::cmyk(0.0, 0.0, 1.0, 0.0)), vec![]);
let art = Node::group(d.alloc_id(), vec![Arc::new(yellow)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((175.0, 75.0)) });
with_art(d, vec![cyan, circle, white, spot, instance])
}
@@ -2228,7 +2228,7 @@ fn symbol_instances_draw_as_groups_of_their_placed_art() {
let circle = vectorcraft_geom::shapes::ellipse(Rect::new(-6.0, -6.0, 6.0, 6.0));
let ap = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.8, 0.0)), Paint::solid(Color::BLACK), 1.0);
let circle = Node::path(d.alloc_id(), circle, ap);
d.symbols.push(vectorcraft_doc::Symbol { name: "U".into(), art: Arc::new(circle) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "U".into(), art: Arc::new(circle) });
let id = d.alloc_id();
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: "U".into(), xf: Affine::translate((175.0, 25.0)) })).unwrap();
// `groups` holds the same art in place of the instances: groups, and the circle as a path.
@@ -2272,7 +2272,7 @@ fn inline_graphics_keep_their_placed_art_between_frames() {
use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun};
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
let style = CharStyle { size: 24.0, ..Default::default() };
let mut t = TextObject::point(vectorcraft_geom::Point::new(10.0, 50.0), "A", style.clone());
t.runs.push(TextRun::inline(InlineArt::new("S"), style.clone()));
+2 -2
View File
@@ -300,7 +300,7 @@ fn symbol_doc(effects: Vec<Effect>) -> Document {
shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)),
Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
);
d.symbols.push(Symbol { name: "Square".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Square".into(), art: Arc::new(art) });
let mut n = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Square".into(), xf: Affine::translate((50.0, 50.0)) });
n.appearance.effects = effects;
doc_with(d, n)
@@ -469,7 +469,7 @@ fn a_transform_effect_on_a_group_symbol_or_envelope_moves_and_scales_the_gradien
t.id = NodeId(500);
// The type in a group, as the art of a symbol, and in an envelope that leaves it as it is.
let mut symbols = Document::new(100.0, 100.0);
symbols.symbols.push(Symbol { name: "Type".into(), art: Arc::new(t.clone()) });
symbols.symbols.push(Symbol { dynamic: false, name: "Type".into(), art: Arc::new(t.clone()) });
let holders = [
doc_with(Document::new(100.0, 100.0), Node::group(NodeId(0), vec![Arc::new(t.clone())])),
doc_with(symbols, Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Type".into(), xf: Affine::IDENTITY })),
+5 -1
View File
@@ -281,7 +281,11 @@ fn a_ready_bitmap_draws_again_where_its_file_shows() {
let brush = vectorcraft_brush::Brush { name: "Cards".into(), kind: vectorcraft_brush::BrushKind::Scatter(scatter) };
vectorcraft_brush::store(&mut brushed, &[brush]);
assert_eq!(placed_area(&brushed, &only_a), Changed::Everything);
d.symbols.push(vectorcraft_doc::Symbol { name: "Card".into(), art: Arc::new(placed_node(65, "area-a", 40.0, 20.0, Affine::IDENTITY)) });
d.symbols.push(vectorcraft_doc::Symbol {
dynamic: false,
name: "Card".into(),
art: Arc::new(placed_node(65, "area-a", 40.0, 20.0, Affine::IDENTITY)),
});
assert_eq!(placed_area(&d, &only_a), Changed::Everything);
}
+1 -1
View File
@@ -310,7 +310,7 @@ fn rasterize_and_shadows_paint_in_stack_order() {
Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0),
);
art.appearance.effects.push(Effect { id: shadow.0.into(), params: shadow.1.clone(), visible: true });
d.symbols.push(vectorcraft_doc::Symbol { name: "Shadowed".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Shadowed".into(), art: Arc::new(art) });
let mut instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shadowed".into(), xf: Affine::translate((20.0, 20.0)) });
instance.appearance.effects.push(Effect { id: "rasterize".into(), params: json!({}), visible: true });
let l = d.layers[0].id;
+1 -1
View File
@@ -653,7 +653,7 @@ impl Writer<'_> {
/// by a transform under which the def would not look as the canvas paints it ([`Reuse`]).
fn symbol_use(&mut self, sym: &vectorcraft_doc::Symbol, inst: &Node, xf: Affine) -> Option<String> {
// SVG Tiny has no `<symbol>`.
if self.tiny() || vectorcraft_brush::stain(inst).is_some() {
if self.tiny() || vectorcraft_brush::stain(inst).is_some() || !inst.extras().symbol_overrides.is_empty() {
return None;
}
if !self.symbols.contains_key(&sym.name) {
+1 -1
View File
@@ -186,7 +186,7 @@ impl Importer {
let name = unique_name(self.name_of(&sym), |n| self.doc.symbols.iter().any(|s| s.name == n));
let reuse = Reuse::of(&self.doc, &art);
let made = SymbolMade { name: name.clone(), art: without_ids(&art), reuse };
self.doc.symbols.push(vectorcraft_doc::Symbol { name: name.clone(), art: Arc::new(art) });
self.doc.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: name.clone(), art: Arc::new(art) });
self.symbols.insert(sym, made);
if !whole || !reuse.allows(xf) {
return None;
+1 -1
View File
@@ -230,7 +230,7 @@ fn symbol_document(name: &str) -> Document {
let mut doc = import(&svg(r#"<rect id="instance" width="20" height="20"/>"#)).unwrap();
let id = named(&doc, "instance").id;
let art = doc.node(id).unwrap().clone();
doc.symbols.push(vectorcraft_doc::Symbol { name: name.into(), art: std::sync::Arc::new(art) });
doc.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: name.into(), art: std::sync::Arc::new(art) });
let instance = doc.node_mut(id).unwrap();
instance.appearance = vectorcraft_doc::Appearance::default().into();
instance.kind = vectorcraft_doc::NodeKind::SymbolInstance { symbol: name.into(), xf: vectorcraft_geom::Affine::IDENTITY };
@@ -140,7 +140,7 @@ fn tiny_has_no_filters_masks_symbols_or_style_sheets() {
Some(Box::new(OpacityMask::new(Node::path(d.alloc_id(), shapes::ellipse(Rect::new(80.0, 10.0, 130.0, 60.0)), fill(255, 255, 255)), true)));
add(&mut d, b);
let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(-5.0, -5.0, 5.0, 5.0)), fill(0, 128, 0));
d.symbols.push(Symbol { name: "Dot".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Dot".into(), art: Arc::new(art) });
add(&mut d, Node::new(vectorcraft_doc::NodeId(0), NodeKind::SymbolInstance { symbol: "Dot".into(), xf: Affine::translate((200.0, 30.0)) }));
let mut c = Node::path(
vectorcraft_doc::NodeId(0),
+1 -1
View File
@@ -376,7 +376,7 @@ fn symbols_and_effects_export_import_export_is_stable() {
shapes::ellipse(Rect::new(0.0, 0.0, 20.0, 20.0)),
Appearance::basic(Paint::solid(Color::rgb8(200, 30, 30)), Paint::None, 0.0),
);
d.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art: Arc::new(dot) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Dot".into(), art: Arc::new(dot) });
let l = d.layers[0].id;
for x in [10.0, 60.0] {
let id = d.alloc_id();
+4 -4
View File
@@ -28,7 +28,7 @@ fn symbol_doc(stroke: f64, xfs: &[Affine]) -> Document {
let mut d = Document::new(200.0, 120.0);
let ap = Appearance::basic(Paint::solid(Color::rgb8(30, 90, 220)), Paint::solid(Color::BLACK), stroke);
let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)), ap);
d.symbols.push(Symbol { name: "My Mark".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "My Mark".into(), art: Arc::new(art) });
for xf in xfs {
instance(&mut d, "My Mark", *xf);
}
@@ -108,7 +108,7 @@ fn pattern_paints_in_symbols_stay_on_the_page() {
d.patterns.push(def);
let art =
Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), Appearance::basic(pattern_paint("Checks"), Paint::None, 0.0));
d.symbols.push(Symbol { name: "Tiles".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Tiles".into(), art: Arc::new(art) });
instance(&mut d, "Tiles", Affine::translate((10.0, 10.0)));
instance(&mut d, "Tiles", Affine::translate((105.0, 37.0)));
let svg = export(&d, &ExportOptions::default());
@@ -143,7 +143,7 @@ fn masks_in_symbols_move_with_the_art_when_linked() {
let mut mask = vectorcraft_doc::OpacityMask::new(shape, true);
mask.linked = linked;
art.extras_mut().mask = Some(Box::new(mask));
d.symbols.push(Symbol { name: "Half".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Half".into(), art: Arc::new(art) });
instance(&mut d, "Half", Affine::translate((10.0, 10.0)));
instance(&mut d, "Half", Affine::translate((15.0, 50.0)));
let svg = export(&d, &ExportOptions::default());
@@ -160,7 +160,7 @@ fn symbols_inside_themselves_end() {
let inner = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Loop".into(), xf: Affine::translate((5.0, 0.0)) });
let mut g = Node::new(d.alloc_id(), NodeKind::Group { children: vec![Arc::new(inner)], clip: false });
g.name = Some("g".into());
d.symbols.push(Symbol { name: "Loop".into(), art: Arc::new(g) });
d.symbols.push(Symbol { dynamic: false, name: "Loop".into(), art: Arc::new(g) });
instance(&mut d, "Loop", Affine::translate((20.0, 20.0)));
let svg = export(&d, &ExportOptions::default());
assert!(import(&svg).is_ok(), "{svg}");
+13 -7
View File
@@ -675,6 +675,7 @@ mod tests {
assert_eq!(sampled_hex(&a), "#0000ff");
// A symbol of a green square, placed at (300, 300) and doubled.
d.symbols.push(vectorcraft_doc::Symbol {
dynamic: false,
name: "Green".into(),
art: filled_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Paint::solid(Color::rgb(0.0, 1.0, 0.0))),
});
@@ -693,8 +694,9 @@ mod tests {
use vectorcraft_geom::{Affine, Rect};
let (mut d, _) = doc_with_halves();
let red = Paint::solid(Color::rgb(1.0, 0.0, 0.0));
d.symbols.push(vectorcraft_doc::Symbol { name: "Red".into(), art: filled_rect(Rect::new(0.0, 0.0, 20.0, 10.0), red) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Red".into(), art: filled_rect(Rect::new(0.0, 0.0, 20.0, 10.0), red) });
d.symbols.push(vectorcraft_doc::Symbol {
dynamic: false,
name: "Tiled".into(),
art: filled_rect(Rect::new(0.0, 0.0, 20.0, 10.0), vectorcraft_doc::pattern::pattern_paint("Halves")),
});
@@ -730,7 +732,7 @@ mod tests {
let red = || Paint::solid(Color::rgb(1.0, 0.0, 0.0));
let pair =
Node::group(NodeId(0), vec![filled_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red()), filled_rect(Rect::new(20.0, 0.0, 30.0, 10.0), red())]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Pair".into(), art: Arc::new(pair) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Pair".into(), art: Arc::new(pair) });
let (l, inst) = (d.layers[0].id, d.alloc_id());
let mut n = Node::new(inst, NodeKind::SymbolInstance { symbol: "Pair".into(), xf: Affine::translate((100.0, 140.0)) });
n.appearance.set_fill(Paint::solid(Color::rgb(0.0, 0.0, 1.0)));
@@ -744,7 +746,7 @@ mod tests {
// A 200 pt tile holding an instance of a 180 pt green bar, which reaches far past the
// square an instance is measured by without its symbol.
let green = Paint::solid(Color::rgb(0.0, 1.0, 0.0));
d.symbols.push(vectorcraft_doc::Symbol { name: "Bar".into(), art: filled_rect(Rect::new(0.0, 0.0, 180.0, 10.0), green) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Bar".into(), art: filled_rect(Rect::new(0.0, 0.0, 180.0, 10.0), green) });
let bar = Arc::new(Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Bar".into(), xf: Affine::IDENTITY }));
let mut bars = vectorcraft_doc::PatternDef::new("Bars", vec![bar]);
bars.tile = Rect::new(0.0, 0.0, 200.0, 10.0);
@@ -779,15 +781,19 @@ mod tests {
let instance = |name: String| Arc::new(Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: name, xf: Affine::IDENTITY }));
for i in 0..12 {
let art = Node::group(NodeId(0), (0..4).map(|_| instance(format!("Fan{}", i + 1))).collect());
d.symbols.push(Symbol { name: format!("Fan{i}"), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: format!("Fan{i}"), art: Arc::new(art) });
}
d.symbols.push(Symbol { name: "Fan12".into(), art: filled_rect(Rect::new(300.0, 300.0, 400.0, 400.0), pattern_paint("Nest8")) });
d.symbols.push(Symbol {
dynamic: false,
name: "Fan12".into(),
art: filled_rect(Rect::new(300.0, 300.0, 400.0, 400.0), pattern_paint("Nest8")),
});
// "Deep0" to "Deep99": one instance of the next; "Deep100" a green square.
for i in 0..100 {
d.symbols.push(Symbol { name: format!("Deep{i}"), art: instance(format!("Deep{}", i + 1)) });
d.symbols.push(Symbol { dynamic: false, name: format!("Deep{i}"), art: instance(format!("Deep{}", i + 1)) });
}
let green = Paint::solid(Color::rgb(0.0, 1.0, 0.0));
d.symbols.push(Symbol { name: "Deep100".into(), art: filled_rect(Rect::new(300.0, 0.0, 400.0, 100.0), green) });
d.symbols.push(Symbol { dynamic: false, name: "Deep100".into(), art: filled_rect(Rect::new(300.0, 0.0, 400.0, 100.0), green) });
let l = d.layers[0].id;
for name in ["Fan0", "Deep0"] {
let inst = d.alloc_id();
+22 -6
View File
@@ -1,9 +1,11 @@
//! Symbol Options: name, Export Type (Graphic or Movie Clip) and Registration for a new symbol
//! Symbol Options: name, Export Type (Graphic or Movie Clip), Symbol Type (Dynamic or Static) and
//! Registration for a new symbol
//! from the selection (`symbol.new`), or for the current symbol in the Symbols panel
//! (`symbol.options`).
//!
//! Fields: `name`, `nineSlice` (bool) with `sliceLeft` / `sliceTop` / `sliceRight` / `sliceBottom`
//! (percent of the symbol's size), `exportType` (`movieClip` | `graphic`), `registration` (0..8, the 9-point grid;
//! (percent of the symbol's size), `exportType` (`movieClip` | `graphic`), `type` (`dynamic` |
//! `static`; a new symbol is dynamic), `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>.
@@ -38,7 +40,7 @@ fn slice_params(d: &Dialog) -> Value {
if d.bool("nineSlice") { json!({ "nineSlice": true, "nineSliceGuides": [l, t, r, b] }) } else { json!({ "nineSlice": false }) }
}
/// Export Type choices: (value, label). Illustrator's default is Movie Clip.
/// Export Type choices: (value, label). The reference app'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 };
@@ -50,7 +52,7 @@ pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
return Err("nothing selected".into());
}
let name = st.doc.unique_symbol_name("New Symbol");
let mut fields = json!({ "name": name, "exportType": "movieClip", "registration": 4, "__edit": false });
let mut fields = json!({ "name": name, "exportType": "movieClip", "type": "dynamic", "registration": 4, "__edit": false });
if let Some(o) = fields.as_object_mut() {
o.extend(slice_fields(None));
}
@@ -76,9 +78,11 @@ pub fn open_edit(app: &mut VectorcraftApp, name: Option<&str>) -> Result<Value,
};
let export = st.doc.symbol_export_type(&name).id();
let registration = st.doc.symbol_registration(&name).0;
let kind = if st.doc.symbols.iter().any(|s| s.name == name && s.dynamic) { "dynamic" } else { "static" };
let mut fields = json!({
"name": name,
"exportType": export,
"type": kind,
"registration": registration,
"__edit": true,
"__from": name,
@@ -103,6 +107,17 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
d.fields.insert("exportType".into(), json!(value));
}
ui.end_row();
label(ui, tl!("Symbol Type:"));
// A dynamic symbol's instances may change the appearance of parts of its art and stay linked.
ui.horizontal(|ui| {
let cur = d.str("type");
for (value, text) in [("dynamic", tl!("Dynamic Symbol")), ("static", tl!("Static Symbol"))] {
if widgets::radio(ui, text, cur == value, true) {
d.fields.insert("type".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) {
@@ -134,18 +149,19 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
return Err("name is empty".into());
}
let export = d.str("exportType");
let kind = d.str("type");
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 });
let mut params = json!({ "name": from, "exportType": export, "type": kind, "registration": registration });
merge(&mut params, slice_params(d));
if from != name {
params["newName"] = json!(name);
}
app.run("symbol.options", params)
} else {
let mut params = json!({ "name": name, "exportType": export, "registration": registration });
let mut params = json!({ "name": name, "exportType": export, "type": kind, "registration": registration });
merge(&mut params, slice_params(d));
app.run("symbol.new", params)
};
+12
View File
@@ -5971,3 +5971,15 @@ breakText Glyphs Глифи
Odia Ория
Urdu Урду
VectorCraft {release} and later (version {n}) VectorCraft {release} и по-нови (версия {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Тип на символа:
Dynamic Symbol Динамичен символ
Static Symbol Статичен символ
Symbol Instance Appearance Външен вид на екземпляра на символ
Reset Symbol Instance Нулиране на екземпляра на символ
@msg type must be static or dynamic, not {v} type трябва да е static или dynamic, а не {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` е статичен символ: първо задайте типа на символа на „Динамичен“
@msg path must be a list of child indices path трябва да е списък с индекси на дъщерни обекти
@msg no object at path {shown} in the symbol's art в графиката на символа няма обект на пътя {shown}
@msg give fill, stroke, opacity and/or blend задайте fill, stroke, opacity и/или blend
@msg pass newName, registration, exportType, nineSlice and/or type задайте newName, registration, exportType, nineSlice и/или type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6004,3 +6004,15 @@ shape Open Otvoren
Odia Odija
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} i novije (verzija {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Vrsta simbola:
Dynamic Symbol Dinamički simbol
Static Symbol Statički simbol
Symbol Instance Appearance Izgled instance simbola
Reset Symbol Instance Vrati instancu simbola na početno
@msg type must be static or dynamic, not {v} type mora biti static ili dynamic, a ne {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` je statički simbol: prvo postavite vrstu simbola na Dinamički
@msg path must be a list of child indices path mora biti lista indeksa podređenih objekata
@msg no object at path {shown} in the symbol's art nema objekta na putanji {shown} u grafici simbola
@msg give fill, stroke, opacity and/or blend navedite fill, stroke, opacity i/ili blend
@msg pass newName, registration, exportType, nineSlice and/or type navedite newName, registration, exportType, nineSlice i/ili type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6077,3 +6077,15 @@ breakText Glyphs Glifs
Odia Oriya
Urdu Urdú
VectorCraft {release} and later (version {n}) VectorCraft {release} i posteriors (versió {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Tipus de símbol:
Dynamic Symbol Símbol dinàmic
Static Symbol Símbol estàtic
Symbol Instance Appearance Aparença de la instància de símbol
Reset Symbol Instance Restableix la instància de símbol
@msg type must be static or dynamic, not {v} type ha de ser static o dynamic, no {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` és un símbol estàtic: primer establiu el tipus de símbol a Dinàmic
@msg path must be a list of child indices path ha de ser una llista d’índexs de fills
@msg no object at path {shown} in the symbol's art no hi ha cap objecte al camí {shown} de l’art del símbol
@msg give fill, stroke, opacity and/or blend indiqueu fill, stroke, opacity i/o blend
@msg pass newName, registration, exportType, nineSlice and/or type indiqueu newName, registration, exportType, nineSlice i/o type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6087,3 +6087,15 @@ breakText Glyphs Glyfy
Odia Urijština
Urdu Urdština
VectorCraft {release} and later (version {n}) VectorCraft {release} a novější (verze {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Typ symbolu:
Dynamic Symbol Dynamický symbol
Static Symbol Statický symbol
Symbol Instance Appearance Vzhled instance symbolu
Reset Symbol Instance Obnovit instanci symbolu
@msg type must be static or dynamic, not {v} type musí být static nebo dynamic, ne {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` je statický symbol: nejprve nastavte jeho typ symbolu na Dynamický
@msg path must be a list of child indices path musí být seznam indexů potomků
@msg no object at path {shown} in the symbol's art v kresbě symbolu není na cestě {shown} žádný objekt
@msg give fill, stroke, opacity and/or blend zadejte fill, stroke, opacity a/nebo blend
@msg pass newName, registration, exportType, nineSlice and/or type zadejte newName, registration, exportType, nineSlice a/nebo type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5985,3 +5985,15 @@ breakText Glyphs Glyffer
Odia Oriya
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} og nyere (version {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Symboltype:
Dynamic Symbol Dynamisk symbol
Static Symbol Statisk symbol
Symbol Instance Appearance Symbolforekomstens udseende
Reset Symbol Instance Nulstil symbolforekomst
@msg type must be static or dynamic, not {v} type skal være static eller dynamic, ikke {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` er et statisk symbol: indstil først symboltypen til Dynamisk
@msg path must be a list of child indices path skal være en liste over underordnede indekser
@msg no object at path {shown} in the symbol's art intet objekt ved stien {shown} i symbolets grafik
@msg give fill, stroke, opacity and/or blend angiv fill, stroke, opacity og/eller blend
@msg pass newName, registration, exportType, nineSlice and/or type angiv newName, registration, exportType, nineSlice og/eller type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5672,3 +5672,15 @@ breakText Glyphs Glyphen
Odia Oriya
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} und neuer (Version {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Symboltyp:
Dynamic Symbol Dynamisches Symbol
Static Symbol Statisches Symbol
Symbol Instance Appearance Aussehen der Symbolinstanz
Reset Symbol Instance Symbolinstanz zurücksetzen
@msg type must be static or dynamic, not {v} type muss static oder dynamic sein, nicht {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` ist ein statisches Symbol: Stellen Sie zuerst den Symboltyp auf „Dynamisch“ ein
@msg path must be a list of child indices path muss eine Liste von Kindindizes sein
@msg no object at path {shown} in the symbol's art kein Objekt am Pfad {shown} in der Grafik des Symbols
@msg give fill, stroke, opacity and/or blend geben Sie fill, stroke, opacity und/oder blend an
@msg pass newName, registration, exportType, nineSlice and/or type geben Sie newName, registration, exportType, nineSlice und/oder type an
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6039,3 +6039,15 @@ breakText Glyphs Γλύφοι
Odia Όντια
Urdu Ούρντου
VectorCraft {release} and later (version {n}) VectorCraft {release} και νεότερες (έκδοση {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Τύπος συμβόλου:
Dynamic Symbol Δυναμικό σύμβολο
Static Symbol Στατικό σύμβολο
Symbol Instance Appearance Εμφάνιση στιγμιότυπου συμβόλου
Reset Symbol Instance Επαναφορά στιγμιότυπου συμβόλου
@msg type must be static or dynamic, not {v} το type πρέπει να είναι static ή dynamic, όχι {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first το `{name}` είναι στατικό σύμβολο: ορίστε πρώτα τον τύπο συμβόλου σε Δυναμικό
@msg path must be a list of child indices το path πρέπει να είναι λίστα δεικτών θυγατρικών αντικειμένων
@msg no object at path {shown} in the symbol's art δεν υπάρχει αντικείμενο στη διαδρομή {shown} στο γραφικό του συμβόλου
@msg give fill, stroke, opacity and/or blend δώστε fill, stroke, opacity ή/και blend
@msg pass newName, registration, exportType, nineSlice and/or type δώστε newName, registration, exportType, nineSlice ή/και type
Can't render this file because it contains an unexpected character in line 164 and column 5.
+12
View File
@@ -6029,3 +6029,15 @@ breakText Glyphs Glifos
Odia Oriya
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} y posteriores (versión {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Tipo de símbolo:
Dynamic Symbol Símbolo dinámico
Static Symbol Símbolo estático
Symbol Instance Appearance Apariencia de la instancia de símbolo
Reset Symbol Instance Restablecer instancia de símbolo
@msg type must be static or dynamic, not {v} type debe ser static o dynamic, no {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` es un símbolo estático: primero establezca su tipo de símbolo en Dinámico
@msg path must be a list of child indices path debe ser una lista de índices de hijos
@msg no object at path {shown} in the symbol's art no hay ningún objeto en la ruta {shown} del arte del símbolo
@msg give fill, stroke, opacity and/or blend indique fill, stroke, opacity y/o blend
@msg pass newName, registration, exportType, nineSlice and/or type indique newName, registration, exportType, nineSlice y/o type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6117,3 +6117,15 @@ breakText Glyphs Merkit
Odia Orija
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} ja uudemmat (versio {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Symbolin tyyppi:
Dynamic Symbol Dynaaminen symboli
Static Symbol Staattinen symboli
Symbol Instance Appearance Symboli-ilmentymän ulkoasu
Reset Symbol Instance Palauta symboli-ilmentymä
@msg type must be static or dynamic, not {v} type-arvon on oltava static tai dynamic, ei {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` on staattinen symboli: aseta ensin sen symbolin tyypiksi Dynaaminen
@msg path must be a list of child indices path-arvon on oltava luettelo alikohteiden indeksejä
@msg no object at path {shown} in the symbol's art symbolin kuvituksessa ei ole objektia polussa {shown}
@msg give fill, stroke, opacity and/or blend anna fill, stroke, opacity ja/tai blend
@msg pass newName, registration, exportType, nineSlice and/or type anna newName, registration, exportType, nineSlice ja/tai type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6084,3 +6084,15 @@ breakText Glyphs Glyphes
Odia Odia
Urdu Ourdou
VectorCraft {release} and later (version {n}) VectorCraft {release} et ultérieures (version {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Type de symbole :
Dynamic Symbol Symbole dynamique
Static Symbol Symbole statique
Symbol Instance Appearance Aspect de l'instance de symbole
Reset Symbol Instance Réinitialiser l'instance de symbole
@msg type must be static or dynamic, not {v} type doit être static ou dynamic, et non {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` est un symbole statique : définissez d'abord son type de symbole sur Dynamique
@msg path must be a list of child indices path doit être une liste d'indices d'enfants
@msg no object at path {shown} in the symbol's art aucun objet au chemin {shown} dans l'illustration du symbole
@msg give fill, stroke, opacity and/or blend indiquez fill, stroke, opacity et/ou blend
@msg pass newName, registration, exportType, nineSlice and/or type indiquez newName, registration, exportType, nineSlice et/ou type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6073,3 +6073,15 @@ breakText Glyphs Glifák
Odia Odia
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} és újabb (verzió: {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Szimbólum típusa:
Dynamic Symbol Dinamikus szimbólum
Static Symbol Statikus szimbólum
Symbol Instance Appearance Szimbólumpéldány megjelenése
Reset Symbol Instance Szimbólumpéldány visszaállítása
@msg type must be static or dynamic, not {v} a type értéke static vagy dynamic lehet, nem {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` statikus szimbólum: előbb állítsa a szimbólum típusát Dinamikusra
@msg path must be a list of child indices a path gyermekindexek listája kell legyen
@msg no object at path {shown} in the symbol's art a szimbólum grafikájában nincs objektum a(z) {shown} útvonalon
@msg give fill, stroke, opacity and/or blend adja meg a fill, stroke, opacity és/vagy blend értékét
@msg pass newName, registration, exportType, nineSlice and/or type adja meg a newName, registration, exportType, nineSlice és/vagy type értékét
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5985,3 +5985,15 @@ breakText Glyphs Glif
Odia Oriya
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} dan yang lebih baru (versi {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Jenis simbol:
Dynamic Symbol Simbol dinamis
Static Symbol Simbol statis
Symbol Instance Appearance Tampilan instans simbol
Reset Symbol Instance Atur ulang instans simbol
@msg type must be static or dynamic, not {v} type harus static atau dynamic, bukan {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` adalah simbol statis: setel dulu jenis simbolnya ke Dinamis
@msg path must be a list of child indices path harus berupa daftar indeks anak
@msg no object at path {shown} in the symbol's art tidak ada objek di jalur {shown} dalam karya seni simbol
@msg give fill, stroke, opacity and/or blend berikan fill, stroke, opacity dan/atau blend
@msg pass newName, registration, exportType, nineSlice and/or type berikan newName, registration, exportType, nineSlice dan/atau type
Can't render this file because it contains an unexpected character in line 2185 and column 5.
+12
View File
@@ -5706,3 +5706,15 @@ breakText Glyphs Glifi
Odia Odia
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} e successive (versione {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Tipo di simbolo:
Dynamic Symbol Simbolo dinamico
Static Symbol Simbolo statico
Symbol Instance Appearance Aspetto dell'istanza di simbolo
Reset Symbol Instance Reimposta istanza di simbolo
@msg type must be static or dynamic, not {v} type deve essere static o dynamic, non {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` è un simbolo statico: imposta prima il tipo di simbolo su Dinamico
@msg path must be a list of child indices path deve essere un elenco di indici figlio
@msg no object at path {shown} in the symbol's art nessun oggetto al percorso {shown} nella grafica del simbolo
@msg give fill, stroke, opacity and/or blend specifica fill, stroke, opacity e/o blend
@msg pass newName, registration, exportType, nineSlice and/or type specifica newName, registration, exportType, nineSlice e/o type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5773,3 +5773,15 @@ breakText Glyphs 字形
Odia オディア語
Urdu ウルドゥー語
VectorCraft {release} and later (version {n}) VectorCraft {release} 以降 (バージョン {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: シンボルの種類:
Dynamic Symbol ダイナミックシンボル
Static Symbol スタティックシンボル
Symbol Instance Appearance シンボルインスタンスのアピアランス
Reset Symbol Instance シンボルインスタンスをリセット
@msg type must be static or dynamic, not {v} type は static または dynamic にしてください({v} は無効です)
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` はスタティックシンボルです。先にシンボルの種類をダイナミックに設定してください
@msg path must be a list of child indices path は子のインデックスのリストにしてください
@msg no object at path {shown} in the symbol's art シンボルのアートのパス {shown} にオブジェクトがありません
@msg give fill, stroke, opacity and/or blend fill、stroke、opacity、blend のいずれかを指定してください
@msg pass newName, registration, exportType, nineSlice and/or type newName、registration、exportType、nineSlice、type のいずれかを指定してください
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6076,3 +6076,15 @@ breakText Glyphs გლიფები
Odia ორია
Urdu ურდუ
VectorCraft {release} and later (version {n}) VectorCraft {release} და უფრო ახალი (ვერსია {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: სიმბოლოს ტიპი:
Dynamic Symbol დინამიკური სიმბოლო
Static Symbol სტატიკური სიმბოლო
Symbol Instance Appearance სიმბოლოს ეგზემპლარის იერსახე
Reset Symbol Instance სიმბოლოს ეგზემპლარის ჩამოყრა
@msg type must be static or dynamic, not {v} type უნდა იყოს static ან dynamic და არა {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` სტატიკური სიმბოლოა: ჯერ სიმბოლოს ტიპად დააყენეთ „დინამიკური“
@msg path must be a list of child indices path უნდა იყოს შვილობილი ინდექსების სია
@msg no object at path {shown} in the symbol's art სიმბოლოს გრაფიკაში გზაზე {shown} ობიექტი არ არის
@msg give fill, stroke, opacity and/or blend მიუთითეთ fill, stroke, opacity და/ან blend
@msg pass newName, registration, exportType, nineSlice and/or type მიუთითეთ newName, registration, exportType, nineSlice და/ან type
Can't render this file because it is too large.
+12
View File
@@ -5669,3 +5669,15 @@ breakText Glyphs 글리프
Odia 오리야어
Urdu 우르두어
VectorCraft {release} and later (version {n}) VectorCraft {release} 이상 (버전 {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: 심볼 유형:
Dynamic Symbol 동적 심볼
Static Symbol 정적 심볼
Symbol Instance Appearance 심볼 인스턴스 모양새
Reset Symbol Instance 심볼 인스턴스 재설정
@msg type must be static or dynamic, not {v} type은 static 또는 dynamic이어야 합니다({v}는 안 됨)
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}`은(는) 정적 심볼입니다. 먼저 심볼 유형을 동적으로 설정하세요
@msg path must be a list of child indices path는 자식 인덱스 목록이어야 합니다
@msg no object at path {shown} in the symbol's art 심볼 아트의 경로 {shown}에 개체가 없습니다
@msg give fill, stroke, opacity and/or blend fill, stroke, opacity 및/또는 blend를 지정하세요
@msg pass newName, registration, exportType, nineSlice and/or type newName, registration, exportType, nineSlice 및/또는 type을 지정하세요
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6029,3 +6029,15 @@ breakText Glyphs Glyphs
Odia Odia
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} en nieuwer (versie {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Symbooltype:
Dynamic Symbol Dynamisch symbool
Static Symbol Statisch symbool
Symbol Instance Appearance Uiterlijk van symboolinstantie
Reset Symbol Instance Symboolinstantie opnieuw instellen
@msg type must be static or dynamic, not {v} type moet static of dynamic zijn, niet {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` is een statisch symbool: stel eerst het symbooltype in op Dynamisch
@msg path must be a list of child indices path moet een lijst met onderliggende indexen zijn
@msg no object at path {shown} in the symbol's art geen object op pad {shown} in de illustratie van het symbool
@msg give fill, stroke, opacity and/or blend geef fill, stroke, opacity en/of blend op
@msg pass newName, registration, exportType, nineSlice and/or type geef newName, registration, exportType, nineSlice en/of type op
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5942,3 +5942,15 @@ breakText Glyphs Glify
Odia Orija
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} i nowsze (wersja {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Typ symbolu:
Dynamic Symbol Symbol dynamiczny
Static Symbol Symbol statyczny
Symbol Instance Appearance Wygląd wystąpienia symbolu
Reset Symbol Instance Resetuj wystąpienie symbolu
@msg type must be static or dynamic, not {v} type musi mieć wartość static lub dynamic, a nie {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` jest symbolem statycznym: najpierw ustaw jego typ symbolu na Dynamiczny
@msg path must be a list of child indices path musi być listą indeksów elementów podrzędnych
@msg no object at path {shown} in the symbol's art brak obiektu w ścieżce {shown} w grafice symbolu
@msg give fill, stroke, opacity and/or blend podaj fill, stroke, opacity i/lub blend
@msg pass newName, registration, exportType, nineSlice and/or type podaj newName, registration, exportType, nineSlice i/lub type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6006,3 +6006,15 @@ breakText Glyphs Glifos
Odia Oriá
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} e posteriores (versão {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Tipo de símbolo:
Dynamic Symbol Símbolo dinâmico
Static Symbol Símbolo estático
Symbol Instance Appearance Aparência da instância de símbolo
Reset Symbol Instance Redefinir instância de símbolo
@msg type must be static or dynamic, not {v} type deve ser static ou dynamic, não {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` é um símbolo estático: defina primeiro o tipo de símbolo como Dinâmico
@msg path must be a list of child indices path deve ser uma lista de índices de filhos
@msg no object at path {shown} in the symbol's art nenhum objeto no caminho {shown} na arte do símbolo
@msg give fill, stroke, opacity and/or blend informe fill, stroke, opacity e/ou blend
@msg pass newName, registration, exportType, nineSlice and/or type informe newName, registration, exportType, nineSlice e/ou type
Can't render this file because it contains an unexpected character in line 6 and column 47.
+12
View File
@@ -6064,3 +6064,15 @@ breakText Glyphs Глифы
Odia Ория
Urdu Урду
VectorCraft {release} and later (version {n}) VectorCraft {release} и новее (версия {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Тип символа:
Dynamic Symbol Динамический символ
Static Symbol Статический символ
Symbol Instance Appearance Оформление экземпляра символа
Reset Symbol Instance Сбросить экземпляр символа
@msg type must be static or dynamic, not {v} type должен быть static или dynamic, а не {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` — статический символ: сначала задайте для него тип символа «Динамический»
@msg path must be a list of child indices path должен быть списком индексов дочерних объектов
@msg no object at path {shown} in the symbol's art в графике символа нет объекта по пути {shown}
@msg give fill, stroke, opacity and/or blend укажите fill, stroke, opacity и/или blend
@msg pass newName, registration, exportType, nineSlice and/or type укажите newName, registration, exportType, nineSlice и/или type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6139,3 +6139,15 @@ breakText Glyphs Glyfer
Odia Oriya
Urdu Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} och senare (version {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Symboltyp:
Dynamic Symbol Dynamisk symbol
Static Symbol Statisk symbol
Symbol Instance Appearance Symbolinstansens utseende
Reset Symbol Instance Återställ symbolinstans
@msg type must be static or dynamic, not {v} type måste vara static eller dynamic, inte {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` är en statisk symbol: ställ först in symboltypen på Dynamisk
@msg path must be a list of child indices path måste vara en lista med underordnade index
@msg no object at path {shown} in the symbol's art inget objekt vid sökvägen {shown} i symbolens bild
@msg give fill, stroke, opacity and/or blend ange fill, stroke, opacity och/eller blend
@msg pass newName, registration, exportType, nineSlice and/or type ange newName, registration, exportType, nineSlice och/eller type
Can't render this file because it contains an unexpected character in line 5 and column 25.
+12
View File
@@ -6044,3 +6044,15 @@ breakText Glyphs Glifler
Odia Oriya dili
Urdu Urduca
VectorCraft {release} and later (version {n}) VectorCraft {release} ve sonrası (sürüm {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Sembol Türü:
Dynamic Symbol Dinamik Sembol
Static Symbol Statik Sembol
Symbol Instance Appearance Sembol Örneği Görünümü
Reset Symbol Instance Sembol Örneğini Sıfırla
@msg type must be static or dynamic, not {v} type static veya dynamic olmalıdır, {v} değil
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` statik bir semboldür: önce Sembol Türünü Dinamik olarak ayarlayın
@msg path must be a list of child indices path alt öğe dizinlerinden oluşan bir liste olmalıdır
@msg no object at path {shown} in the symbol's art sembolün çiziminde {shown} yolunda nesne yok
@msg give fill, stroke, opacity and/or blend fill, stroke, opacity ve/veya blend verin
@msg pass newName, registration, exportType, nineSlice and/or type newName, registration, exportType, nineSlice ve/veya type verin
Can't render this file because it contains an unexpected character in line 5 and column 25.
+12
View File
@@ -5686,3 +5686,15 @@ breakText Glyphs Гліфи
Odia Одія
Urdu Урду
VectorCraft {release} and later (version {n}) VectorCraft {release} і новіші (версія {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Тип символу:
Dynamic Symbol Динамічний символ
Static Symbol Статичний символ
Symbol Instance Appearance Оформлення екземпляра символу
Reset Symbol Instance Скинути екземпляр символу
@msg type must be static or dynamic, not {v} type має бути static або dynamic, а не {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` — статичний символ: спершу встановіть для нього тип символу «Динамічний»
@msg path must be a list of child indices path має бути списком індексів дочірніх об’єктів
@msg no object at path {shown} in the symbol's art у графіці символу немає об’єкта за шляхом {shown}
@msg give fill, stroke, opacity and/or blend вкажіть fill, stroke, opacity та/або blend
@msg pass newName, registration, exportType, nineSlice and/or type вкажіть newName, registration, exportType, nineSlice та/або type
Can't render this file because it contains an unexpected character in line 178 and column 5.
+12
View File
@@ -5689,3 +5689,15 @@ breakText Glyphs Glyph
Odia Tiếng Odia
Urdu Tiếng Urdu
VectorCraft {release} and later (version {n}) VectorCraft {release} trở lên (phiên bản {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: Kiểu ký hiệu:
Dynamic Symbol Ký hiệu động
Static Symbol Ký hiệu tĩnh
Symbol Instance Appearance Diện mạo của đối tượng ký hiệu
Reset Symbol Instance Đặt lại đối tượng ký hiệu
@msg type must be static or dynamic, not {v} type phải là static hoặc dynamic, không phải {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` là ký hiệu tĩnh: trước tiên hãy đặt Kiểu ký hiệu thành Động
@msg path must be a list of child indices path phải là danh sách chỉ số con
@msg no object at path {shown} in the symbol's art không có đối tượng tại đường dẫn {shown} trong hình của ký hiệu
@msg give fill, stroke, opacity and/or blend hãy cung cấp fill, stroke, opacity và/hoặc blend
@msg pass newName, registration, exportType, nineSlice and/or type hãy cung cấp newName, registration, exportType, nineSlice và/hoặc type
Can't render this file because it contains an unexpected character in line 200 and column 5.
+12
View File
@@ -5929,3 +5929,15 @@ breakText Glyphs 字形
Odia 奥里亚语
Urdu 乌尔都语
VectorCraft {release} and later (version {n}) VectorCraft {release} 及更高版本 (版本 {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: 符号类型:
Dynamic Symbol 动态符号
Static Symbol 静态符号
Symbol Instance Appearance 符号实例外观
Reset Symbol Instance 重置符号实例
@msg type must be static or dynamic, not {v} type 必须是 static 或 dynamic,而不是 {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` 是静态符号:请先将其符号类型设为动态
@msg path must be a list of child indices path 必须是子项索引列表
@msg no object at path {shown} in the symbol's art 符号图稿中路径 {shown} 处没有对象
@msg give fill, stroke, opacity and/or blend 请提供 fill、stroke、opacity 和/或 blend
@msg pass newName, registration, exportType, nineSlice and/or type 请提供 newName、registration、exportType、nineSlice 和/或 type
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5726,3 +5726,15 @@ breakText Glyphs 字符
Odia 歐利亞文
Urdu 烏都文
VectorCraft {release} and later (version {n}) VectorCraft {release} 及更新版本 (版本 {n})
# Symbol Options › Symbol Type (dynamic symbols)
Symbol Type: 符號類型:
Dynamic Symbol 動態符號
Static Symbol 靜態符號
Symbol Instance Appearance 符號實體外觀
Reset Symbol Instance 重設符號實體
@msg type must be static or dynamic, not {v} type 必須是 static 或 dynamic,而不是 {v}
@msg `{name}` is a static symbol: set its Symbol Type to Dynamic first `{name}` 是靜態符號:請先將其符號類型設為動態
@msg path must be a list of child indices path 必須是子項索引清單
@msg no object at path {shown} in the symbol's art 符號圖稿中路徑 {shown} 處沒有物件
@msg give fill, stroke, opacity and/or blend 請提供 fill、stroke、opacity 和/或 blend
@msg pass newName, registration, exportType, nineSlice and/or type 請提供 newName、registration、exportType、nineSlice 和/或 type
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -731,7 +731,7 @@ fn previews_follow_the_swatches_images_and_pattern_options_their_art_uses() {
}
vectorcraft_brush::store(d, &brushes);
for (name, art) in [("Ink Symbol", &inked), ("Picture Symbol", &pictured), ("Stripes Symbol", &striped), ("Fade Symbol", &faded)] {
d.symbols.push(Symbol { name: name.into(), art: Arc::new(art.clone()) });
d.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(art.clone()) });
}
Ok(())
})
+4
View File
@@ -136,6 +136,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
if is_live {
actions.push((tl!("Expand Shape"), "object.expandShape"));
}
// A dynamic symbol instance with appearance changes of its own: Reset shows the symbol's again.
if super::symbols::changed_instances_selected(app) {
actions.push((tl!("Reset"), "symbol.instance.reset"));
}
if multi_color(app, ui.ctx()) {
actions.push((tl!("Recolor"), "ui.recolorDialog"));
}
@@ -1666,3 +1666,21 @@ fn offset_path_and_simplify_are_not_quick_actions_for_type_alone() {
let texts = tall(&mut app, &ctx, vec![]);
assert!(shows(&texts, "Offset Path") && shows(&texts, "Simplify"), "type and a path: {texts:?}");
}
/// A dynamic symbol instance with appearance changes of its own has Reset in Quick Actions, which
/// drops them (#4938); without changes there is no Reset.
#[test]
fn reset_is_a_quick_action_for_a_changed_dynamic_instance() {
let mut app = new_app();
let (a, b) = (rect(&mut app, 20.0, 20.0, 40.0, 40.0), rect(&mut app, 80.0, 20.0, 40.0, 40.0));
let inst = app.run("symbol.new", json!({"ids": [a, b], "type": "dynamic"})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [inst]})).unwrap();
let ctx = egui::Context::default();
assert!(!shows(&tall(&mut app, &ctx, vec![]), "Reset"), "nothing to reset");
app.run("symbol.instance.setAppearance", json!({"path": [0], "fill": "#ff0000"})).unwrap();
let texts = tall(&mut app, &ctx, vec![]);
press(&mut app, &ctx, text_at(&texts, "Reset").center());
let id = vectorcraft_doc::NodeId(inst.as_u64().unwrap());
assert!(app.session.doc().unwrap().doc.node(id).unwrap().extras().symbol_overrides.is_empty());
assert!(!shows(&tall(&mut app, &ctx, vec![]), "Reset"));
}
+28 -8
View File
@@ -137,18 +137,29 @@ fn current(app: &mut VectorcraftApp) -> Option<String> {
app.run("symbol.list", json!({})).ok().and_then(|v| v["current"].as_str().map(str::to_string))
}
fn has_instances_selected(app: &VectorcraftApp) -> bool {
/// Does the selection hold (in it or in its groups) a symbol instance that `wanted` takes?
fn instance_selected(app: &VectorcraftApp, wanted: impl Fn(&Node) -> bool) -> bool {
app.session.active().is_some_and(|st| {
st.selection.objects.iter().any(|id| {
let mut any = false;
if let Some(n) = st.doc.node(*id) {
n.walk(&mut |c| any |= matches!(c.kind, NodeKind::SymbolInstance { .. }));
n.walk(&mut |c| any |= matches!(c.kind, NodeKind::SymbolInstance { .. }) && wanted(c));
}
any
})
})
}
fn has_instances_selected(app: &VectorcraftApp) -> bool {
instance_selected(app, |_| true)
}
/// Does the selection hold a symbol instance with appearance changes of its own (a dynamic
/// symbol's), which Reset drops?
pub(crate) fn changed_instances_selected(app: &VectorcraftApp) -> bool {
instance_selected(app, |c| !c.extras().symbol_overrides.is_empty())
}
/// The symbols of the selected objects when every one is a symbol instance; `None` otherwise.
fn selected_instance_symbols(app: &VectorcraftApp) -> Option<Vec<String>> {
let st = app.session.active()?;
@@ -242,7 +253,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
});
let zone = ui.interact(list_rect, ui.scope_id().with("symbols-drop"), Sense::hover());
if let Some(ids) = widgets::art_drop(ui, &zone)
&& let Err(e) = app.run("symbol.new", json!({ "ids": ids }))
&& let Err(e) = app.run("symbol.new", json!({ "ids": ids, "type": "dynamic" }))
{
app.status(e);
}
@@ -269,9 +280,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
ui.add_space((ui.available_width() - 2.0 * 28.0).max(0.0));
if widgets::icon_button_enabled(ui, "dc-new-item", tl!("New Symbol"), false, has_sel, 24.0).clicked() {
// Alt skips Symbol Options: the symbol is made under the default name.
let cmd = if ui.input(|i| i.modifiers.alt) { "symbol.new" } else { "ui.symbolOptions" };
app.run(cmd, json!({})).ok();
// Alt skips Symbol Options: the symbol is made under the default name, dynamic as the
// dialog's default.
let (cmd, p) = if ui.input(|i| i.modifiers.alt) { ("symbol.new", json!({"type": "dynamic"})) } else { ("ui.symbolOptions", json!({})) };
app.run(cmd, p).ok();
}
// Alt deletes an unused symbol without asking; a symbol with instances still asks.
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Symbol"), false, sel.is_some(), 24.0).clicked()
@@ -303,12 +315,13 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
{
crate::dialogs::symbol_delete::ask(app, n, false);
}
let items: [(&str, bool, &str, Value); 6] = [
let items: [(&str, bool, &str, Value); 7] = [
(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()),
(tl!("Break Link to Symbol"), inst, "symbol.breakLink", json!({})),
(tl!("Reset Symbol Instance"), changed_instances_selected(app), "symbol.instance.reset", json!({})),
(tl!("Select All Instances"), sel.is_some(), "symbol.selectInstances", name),
];
for (label, enabled, cmd, params) in items {
@@ -600,10 +613,12 @@ mod tests {
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");
assert_eq!(app.ui.dialog.as_ref().unwrap().str("type"), "dynamic", "Symbol Type: a new symbol is dynamic");
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
let list = app.run("symbol.list", json!({})).unwrap();
assert_eq!(list["symbols"][0]["exportType"], "movieClip");
assert_eq!(list["symbols"][0]["type"], "dynamic");
// 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();
@@ -614,13 +629,18 @@ mod tests {
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.
// Symbol Options on the current symbol renames, keeps Export Type and sets Symbol 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"));
app.ui.dialog.as_mut().unwrap().fields.insert("type".into(), json!("static"));
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");
assert_eq!(app.run("symbol.list", json!({})).unwrap()["symbols"][1]["type"], "static");
app.run("ui.symbolOptions", json!({"edit": true, "name": "Mark"})).unwrap();
assert_eq!(app.ui.dialog.as_ref().unwrap().str("type"), "static");
app.ui.dialog = None;
// Enable Guides for 9-Slice Scaling stores the guides (percent in the dialog) and reopens as set.
assert!(app.run("symbol.list", json!({})).unwrap()["symbols"][1]["nineSlice"].is_null());
app.run("ui.symbolOptions", json!({"edit": true, "name": "Mark"})).unwrap();

Some files were not shown because too many files have changed in this diff Show More