mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>()
|
||||
});
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() });
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]))
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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() };
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) }))
|
||||
}
|
||||
@@ -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) }))
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -41,6 +41,7 @@ mod dashalign;
|
||||
mod distort;
|
||||
mod docsetup;
|
||||
mod draw2;
|
||||
mod dynamic_symbols;
|
||||
mod editcolors;
|
||||
mod effectedit;
|
||||
mod emptytype;
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 }))?;
|
||||
|
||||
@@ -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)) });
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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(())
|
||||
})
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user