mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 06:07:06 -06:00
Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user