diff --git a/crates/brush/src/defaults.rs b/crates/brush/src/defaults.rs index 0a9f4367..8cf74673 100644 --- a/crates/brush/src/defaults.rs +++ b/crates/brush/src/defaults.rs @@ -26,7 +26,7 @@ fn group(children: Vec) -> Node { fn compound(paths: Vec, c: Color) -> Node { let children = paths.into_iter().map(|p| Arc::new(Node::path(NodeId(0), p, Appearance::default()))).collect(); let mut n = Node::new(NodeId(0), NodeKind::Compound { children, rule: FillRule::EvenOdd }); - n.appearance = fill(c); + n.appearance = fill(c).into(); n } @@ -121,7 +121,7 @@ pub(crate) fn stitch_tile() -> Node { let dash = path(shapes::rounded_rectangle(Rect::new(1.5, -0.75, 8.5, 0.75), 0.75), Color::BLACK); // Invisible spacer that fixes the tile width. let mut spacer = path(shapes::rectangle(Rect::new(0.0, -0.75, 10.0, 0.75)), Color::BLACK); - spacer.appearance = Appearance::default(); + spacer.appearance = Appearance::default().into(); group(vec![spacer, dash]) } diff --git a/crates/brush/src/tests.rs b/crates/brush/src/tests.rs index 631be2d5..b012fb97 100644 --- a/crates/brush/src/tests.rs +++ b/crates/brush/src/tests.rs @@ -118,7 +118,7 @@ fn art_brush_arc_length_mapping_is_linear() { Node::path(NodeId(0), shapes::rectangle(Rect::new(5.0, -1.0, 6.0, 1.0)), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0)); let frame = rect_art(10.0, 2.0); let mut spacer = frame.clone(); - spacer.appearance = Appearance::default(); + spacer.appearance = Appearance::default().into(); let art = Node::group(NodeId(0), vec![std::sync::Arc::new(spacer), std::sync::Arc::new(marker)]); let out = stroke_pieces(&art_brush(art, ArtScale::Stretch), &line(0.0, 0.0, 300.0, 0.0), &stroke(1.0)); let g = &out[0]; diff --git a/crates/cad/src/import/build.rs b/crates/cad/src/import/build.rs index 404b6a16..b729d800 100644 --- a/crates/cad/src/import/build.rs +++ b/crates/cad/src/import/build.rs @@ -479,7 +479,7 @@ impl<'d, 'a> Build<'d, 'a> { .into_iter() .map(|sp| { let mut c = Node::new(self.doc.alloc_id(), kind(PathData::single(sp))); - c.appearance = appearance.clone(); + c.appearance = appearance.clone().into(); Arc::new(c) }) .collect(); @@ -487,7 +487,7 @@ impl<'d, 'a> Build<'d, 'a> { } else { Node::new(self.doc.alloc_id(), kind(path)) }; - n.appearance = appearance; + n.appearance = appearance.into(); Some(n) } diff --git a/crates/doc/src/appearance.rs b/crates/doc/src/appearance.rs index e7ad78a8..5b1dd736 100644 --- a/crates/doc/src/appearance.rs +++ b/crates/doc/src/appearance.rs @@ -942,7 +942,7 @@ impl Appearance { }) } /// Gradient paints of the fills and strokes. - fn gradients(&self) -> impl Iterator { + pub(crate) fn gradients(&self) -> impl Iterator { self.items.iter().filter_map(|i| match i { AppearanceItem::Fill(FillLayer { paint: Paint::Gradient(g), .. }) | AppearanceItem::Stroke(StrokeLayer { paint: Paint::Gradient(g), .. }) => Some(&**g), diff --git a/crates/doc/src/blend.rs b/crates/doc/src/blend.rs index 5695834a..e52bd366 100644 --- a/crates/doc/src/blend.rs +++ b/crates/doc/src/blend.rs @@ -619,7 +619,7 @@ impl Lerp { } }; n.id = NodeId(0); - n.appearance = lerp_appearance(&a.appearance, &b.appearance, t); + n.appearance = lerp_appearance(&a.appearance, &b.appearance, t).into(); n.opacity = lerp_f32(a.opacity, b.opacity, t as f32); n.blend = base.blend; n.visible = true; diff --git a/crates/doc/src/hit.rs b/crates/doc/src/hit.rs index 7cc00f83..12c58bad 100644 --- a/crates/doc/src/hit.rs +++ b/crates/doc/src/hit.rs @@ -463,7 +463,7 @@ mod tests { d.insert(Some(l), 0, Node::path(a, shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), Appearance::default_art())).unwrap(); let inner = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(100.0, 0.0, 150.0, 50.0)), Appearance::default()); let mut compound = Node::new(c, NodeKind::Compound { children: vec![Arc::new(inner)], rule: vectorcraft_geom::FillRule::NonZero }); - compound.appearance = Appearance::default_art(); + compound.appearance = Appearance::default_art().into(); d.insert(Some(l), 9, compound).unwrap(); let path_only = HitOptions { path_only: true, ..Default::default() }; for (id, inside, edge) in [(a, Point::new(25.0, 25.0), Point::new(0.5, 25.0)), (c, Point::new(125.0, 25.0), Point::new(100.5, 25.0))] { diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 831d67ea..ca7d744d 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -16,7 +16,9 @@ pub mod hit; mod id_index; pub mod inks; mod inline; +pub mod shared; pub use inline::{SYMBOL_HALF, SYMBOL_SIZES}; +pub use shared::Shared; pub mod links; pub mod live; pub mod marks; diff --git a/crates/doc/src/live.rs b/crates/doc/src/live.rs index 51641b14..0759e293 100644 --- a/crates/doc/src/live.rs +++ b/crates/doc/src/live.rs @@ -1524,7 +1524,7 @@ fn pattern_art(n: &Node, tiles: PatternTiles) -> Option { }; let plain = |items: Vec| { Arc::new(Node { - appearance: Appearance { items, ..Default::default() }, + appearance: Appearance { items, ..Default::default() }.into(), opacity: 1.0, blend: Default::default(), isolate: false, @@ -1560,7 +1560,7 @@ fn pattern_art(n: &Node, tiles: PatternTiles) -> Option { } Some(Node { kind: NodeKind::Group { children: pieces, clip: false }, - appearance: Appearance { effects: n.appearance.effects.clone(), ..Default::default() }, + appearance: Appearance { effects: n.appearance.effects.clone(), ..Default::default() }.into(), ..n.clone() }) } diff --git a/crates/doc/src/node.rs b/crates/doc/src/node.rs index 48d7fc7b..9eb4b0cd 100644 --- a/crates/doc/src/node.rs +++ b/crates/doc/src/node.rs @@ -514,7 +514,7 @@ pub struct Node { #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub knockout_shape: bool, #[serde(default, skip_serializing_if = "crate::skip::is_default")] - pub appearance: Appearance, + pub appearance: crate::Shared, /// Opacity mask (Transparency panel). Its art lives here, outside the layer tree. #[serde(default, skip_serializing_if = "Option::is_none")] pub mask: Option>, @@ -594,7 +594,7 @@ impl Node { isolate: false, knockout: Knockout::Neutral, knockout_shape: false, - appearance: Appearance::default(), + appearance: Appearance::default().into(), mask: None, trace: None, wrap: None, @@ -611,7 +611,7 @@ impl Node { } pub fn path(id: NodeId, path: PathData, appearance: Appearance) -> Self { let mut n = Self::new(id, NodeKind::Path { path, rule: FillRule::NonZero, live: None, clipping: false, guide: false }); - n.appearance = appearance; + n.appearance = appearance.into(); n } pub fn group(id: NodeId, children: Vec>) -> Self { @@ -854,7 +854,11 @@ impl Node { self.appearance.scale_effects(k, f); } } - self.appearance.transform_gradients(a); + // Only through `DerefMut` when there is something to change: the appearance is shared + // copy-on-write (`Shared`), and a move must not copy what it leaves as it is. + if self.appearance.gradients().next().is_some() { + self.appearance.transform_gradients(a); + } if sc.patterns { crate::pattern::transform_pattern_paints(self, a); } diff --git a/crates/doc/src/pattern.rs b/crates/doc/src/pattern.rs index aa185815..39f4e11a 100644 --- a/crates/doc/src/pattern.rs +++ b/crates/doc/src/pattern.rs @@ -343,6 +343,14 @@ pub fn uses_pattern(n: &Node, name: Option<&str>) -> bool { /// Apply `m` to the pattern placement of every pattern paint of `n` (Transform Patterns). pub fn transform_pattern_paints(n: &mut Node, m: Affine) { + // Without pattern paints there is nothing to change: don't copy the shared appearance. + let patterned = |it: &AppearanceItem| { + matches!(it, AppearanceItem::Fill(f) if matches!(f.paint, Paint::Pattern { .. })) + || matches!(it, AppearanceItem::Stroke(s) if matches!(s.paint, Paint::Pattern { .. })) + }; + if !n.appearance.items.iter().any(patterned) { + return; + } for it in n.appearance.items.iter_mut() { let p = match it { AppearanceItem::Fill(f) => &mut f.paint, diff --git a/crates/doc/src/shared.rs b/crates/doc/src/shared.rs new file mode 100644 index 00000000..208ccd4c --- /dev/null +++ b/crates/doc/src/shared.rs @@ -0,0 +1,72 @@ +//! [`Shared`]: a value shared between copies of a node until one of them changes it. + +use std::sync::Arc; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// A copy-on-write value: cloning shares it, and the first change through `DerefMut` makes the +/// changed copy its own (`Arc::make_mut`). A node moved or recoloured keeps sharing what it didn't +/// change with the version the undo history holds. Reads, writes, equality and the saved form are +/// exactly those of the value itself. +#[derive(Clone, Default)] +pub struct Shared(Arc); + +impl Shared { + pub fn new(v: T) -> Self { + Shared(Arc::new(v)) + } + /// Whether `a` and `b` share one value (an unchanged copy). + pub fn ptr_eq(a: &Self, b: &Self) -> bool { + Arc::ptr_eq(&a.0, &b.0) + } +} + +impl std::ops::Deref for Shared { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} + +impl std::ops::DerefMut for Shared { + fn deref_mut(&mut self) -> &mut T { + Arc::make_mut(&mut self.0) + } +} + +impl From for Shared { + fn from(v: T) -> Self { + Shared::new(v) + } +} + +impl PartialEq for Shared { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) || *self.0 == *other.0 + } +} + +/// A shared value equals the plain value it holds (`node.appearance == style.appearance`). +impl PartialEq for Shared { + fn eq(&self, other: &T) -> bool { + *self.0 == *other + } +} + +impl std::fmt::Debug for Shared { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Serialize for Shared { + fn serialize(&self, s: S) -> Result { + self.0.serialize(s) + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for Shared { + fn deserialize>(d: D) -> Result { + T::deserialize(d).map(Shared::new) + } +} diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 148f2c01..6538b2de 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -166,7 +166,7 @@ fn piece(d: &mut Document, n: &Node, path: PathData, rule: FillRule, item: Appea Node { id, name: None, - appearance: Appearance { items: vec![item], ..Default::default() }, + appearance: Appearance { items: vec![item], ..Default::default() }.into(), opacity: 1.0, blend: Default::default(), isolate: false, diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index bf405990..e37a0d20 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -141,7 +141,7 @@ pub(crate) fn as_art(n: &Node, symbol_art: Option<&Node>) -> Option { if let NodeKind::Path { clipping, .. } = &mut clip.kind { *clipping = true; } - let image = Node { appearance: Appearance::default(), ..Node::new(n.id, n.kind.clone()) }; + let image = Node { appearance: Appearance::default().into(), ..Node::new(n.id, n.kind.clone()) }; Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image)], clip: true }) } _ => outline_art(n, symbol_art)?, diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index 2d5fa3eb..5465c448 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -200,7 +200,7 @@ fn art(n: &Node, expand: bool) -> Option { } let mut art = n.clone(); art.kind = NodeKind::Group { children, clip: false }; - art.appearance = Appearance { effects: remaining, ..Default::default() }; + art.appearance = Appearance { effects: remaining, ..Default::default() }.into(); Some(art) } diff --git a/crates/engine/src/cmd/appearance.rs b/crates/engine/src/cmd/appearance.rs index ff48b838..b4c8ad42 100644 --- a/crates/engine/src/cmd/appearance.rs +++ b/crates/engine/src/cmd/appearance.rs @@ -443,7 +443,7 @@ fn clear_appearance(s: &mut Session, p: &Value) -> Result { /// Clear Appearance on `n`: no fill, no stroke, and the object's Opacity row back to Default. A /// group keeps no fill or stroke rows of its own. fn clear(n: &mut Node) { - n.appearance = if is_group(n) { Appearance::default() } else { Appearance::basic(Paint::None, Paint::None, 1.0) }; + n.appearance = if is_group(n) { Appearance::default().into() } else { Appearance::basic(Paint::None, Paint::None, 1.0).into() }; n.opacity = 1.0; n.blend = BlendMode::Normal; } @@ -496,7 +496,7 @@ fn reduce_basic(s: &mut Session, p: &Value) -> Result { // keeps the rows it had. let fill = (fill.is_some() || !is_group(n)).then(|| AppearanceItem::Fill(FillLayer::new(fill.map_or(Paint::None, |f| f.paint().clone())))); - n.appearance = Appearance { items: fill.into_iter().collect(), ..Default::default() }; + n.appearance = Appearance { items: fill.into_iter().collect(), ..Default::default() }.into(); if let Some(AppearanceItem::Stroke(mut st)) = stroke { st.effects.clear(); st.opacity = 1.0; @@ -758,7 +758,7 @@ impl Picked { let st = &t.runs[0].style; (st.basic_appearance(), Some(t.local_bounds()), Some((st.clone(), t.para.clone()))) } - _ => (n.appearance.clone(), n.geometric_bounds(), None), + _ => ((*n.appearance).clone(), n.geometric_bounds(), None), }; Self { appearance, bounds, opacity: n.opacity, blend: n.blend, text } } @@ -831,7 +831,7 @@ impl Picked { } let placed = self.placed(n.geometric_bounds()); if ap.fill == FillAttrs::ALL && ap.stroke == StrokeAttrs::ALL { - n.appearance = placed; + n.appearance = placed.into(); } else { focal_attrs(&mut n.appearance, &placed, ap); } diff --git a/crates/engine/src/cmd/buildcmds.rs b/crates/engine/src/cmd/buildcmds.rs index ef3a10fe..107d8996 100644 --- a/crates/engine/src/cmd/buildcmds.rs +++ b/crates/engine/src/cmd/buildcmds.rs @@ -478,7 +478,7 @@ fn lp_merge(s: &mut Session, p: &Value) -> Result { .filter_map(|e| { let pd = e.path_data()?; let sp = pd.subpaths.first().filter(|sp| sp.segment_count() > 0)?; - Some((kurbo::ParamCurve::eval(&sp.segment(0), 0.5), e.appearance.clone())) + Some((kurbo::ParamCurve::eval(&sp.segment(0), 0.5), (*e.appearance).clone())) }) .collect(); let mut sources = source_nodes(&old); @@ -505,7 +505,7 @@ fn lp_merge(s: &mut Session, p: &Value) -> Result { && let Some(ch) = g.children_mut() && let Some(e) = ch.iter_mut().find(|c| c.id == eid) { - Arc::make_mut(e).appearance = app; + Arc::make_mut(e).appearance = app.into(); } } for r in roots.iter().filter(|r| **r != target) { @@ -777,8 +777,9 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result { ..StrokeLayer::new(paint, width * unit) })], ..Default::default() - }, - None => fill_only(paint), + } + .into(), + None => fill_only(paint).into(), }; children.push(Arc::new(n)); } diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 5b4dd8c1..84370f57 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -97,7 +97,7 @@ pub(crate) fn add_look(s: &mut Session, label: &str, kind: NodeKind, look: NewAr let (par, idx, _) = d.position(target).ok_or(crate::EngineError::NoNode(target))?; let mut mask = d.node(target).cloned().ok_or(crate::EngineError::NoNode(target))?; mask.id = d.alloc_id(); - mask.appearance = Appearance::basic(vectorcraft_color::Paint::None, vectorcraft_color::Paint::None, 0.0); + mask.appearance = Appearance::basic(vectorcraft_color::Paint::None, vectorcraft_color::Paint::None, 0.0).into(); if let NodeKind::Path { clipping, .. } = &mut mask.kind { *clipping = true; } diff --git a/crates/engine/src/cmd/fileio/affinity.rs b/crates/engine/src/cmd/fileio/affinity.rs index 92c8b3a4..70dff78f 100644 --- a/crates/engine/src/cmd/fileio/affinity.rs +++ b/crates/engine/src/cmd/fileio/affinity.rs @@ -236,13 +236,13 @@ impl Builder { let appearance = self.appearance(fills, strokes, to_doc); if children.is_empty() { let mut p = self.base(n, NodeKind::Path { path: data, rule, live: None, clipping: false, guide: false }); - p.appearance = appearance; + p.appearance = appearance.into(); p } else { // A shape clips its children: VectorCraft's clipping path keeps its paint, its // fill behind the clipped art and its stroke over it, as Affinity draws it. let mut clip = Node::new(self.doc.alloc_id(), NodeKind::Path { path: data, rule, live: None, clipping: true, guide: false }); - clip.appearance = appearance; + clip.appearance = appearance.into(); let mut kids = vec![Arc::new(clip)]; kids.extend(children); self.base(n, NodeKind::Group { children: kids, clip: true }) @@ -259,7 +259,7 @@ impl Builder { guide: false, }, ); - clip.appearance = self.appearance(background, strokes, to_doc); + clip.appearance = self.appearance(background, strokes, to_doc).into(); let mut kids = vec![Arc::new(clip)]; kids.extend(children); let mut g = self.base(n, NodeKind::Group { children: kids, clip: true }); diff --git a/crates/engine/src/cmd/flatten.rs b/crates/engine/src/cmd/flatten.rs index ad010c13..8d11365b 100644 --- a/crates/engine/src/cmd/flatten.rs +++ b/crates/engine/src/cmd/flatten.rs @@ -655,7 +655,7 @@ impl FlattenReport { art.iter_mut().for_each(opaque); for path in areas { let mut n = shape_node(&mut tmp, path.clone(), None); - n.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 0.0)), Paint::None, 0.0); + n.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 0.0)), Paint::None, 0.0).into(); art.push(n); } Some(isolated_doc(&tmp, art)) @@ -1054,7 +1054,8 @@ fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan: Plan, o: n.appearance = Appearance { items: vec![AppearanceItem::Fill(FillLayer { overprint: fill.overprint, ..FillLayer::new(fill.paint) })], ..Appearance::default() - }; + } + .into(); n.opacity = fill.alpha.min(1.0); children.push(Arc::new(n)); vector += 1; diff --git a/crates/engine/src/cmd/menucmds.rs b/crates/engine/src/cmd/menucmds.rs index 84082d02..7e013da8 100644 --- a/crates/engine/src/cmd/menucmds.rs +++ b/crates/engine/src/cmd/menucmds.rs @@ -575,7 +575,7 @@ fn crop_image(s: &mut Session, p: &Value) -> Result { let mut bare = node.clone(); bare.opacity = 1.0; bare.blend = Default::default(); - bare.appearance = Appearance::default(); + bare.appearance = Appearance::default().into(); bare.visible = true; let tmp = isolated_doc(&st.doc, vec![bare]); let region = Rect::new(r.x0, r.y0, r.x0 + (r.width() * scale).round().max(1.0) / scale, r.y0 + (r.height() * scale).round().max(1.0) / scale); diff --git a/crates/engine/src/cmd/newart.rs b/crates/engine/src/cmd/newart.rs index 1d7607a2..429ac840 100644 --- a/crates/engine/src/cmd/newart.rs +++ b/crates/engine/src/cmd/newart.rs @@ -94,7 +94,7 @@ impl NewArt { /// Give new object `n` (in `d`) this look: its placed gradients land relative to its bounds, and /// it links to the style (type keeps its own transparency). pub(crate) fn apply(&self, d: &mut vectorcraft_doc::Document, n: &mut Node) { - n.appearance = self.appearance.clone(); + n.appearance = self.appearance.clone().into(); if self.unit_box && n.appearance.has_placed_gradient() && let Some(b) = n.geometric_bounds() diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 5c75c1a9..385d394e 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -661,7 +661,7 @@ fn compound_make(s: &mut Session, _: &Value) -> Result { let back = paths.first().map_or(0.0, |p| direction(p)); for (i, mut p) in paths.into_iter().enumerate() { let pn = Arc::make_mut(&mut p); - pn.appearance = Appearance::default(); + pn.appearance = Appearance::default().into(); let flip = i > 0 && back != 0.0 && direction(pn) == back; if let NodeKind::Path { path, live, .. } = &mut pn.kind { *live = None; @@ -742,7 +742,7 @@ pub(super) fn as_clipping_path(c: &mut Node) -> Result<()> { if !matches!(c.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } | NodeKind::Text(_)) { return Err(EngineError::Other("the top object must be a path, compound path or text object to use as a clipping mask".into())); } - c.appearance = Appearance::basic(Paint::None, Paint::None, 0.0); + c.appearance = Appearance::basic(Paint::None, Paint::None, 0.0).into(); match &mut c.kind { NodeKind::Path { clipping, .. } => *clipping = true, NodeKind::Text(t) => { diff --git a/crates/engine/src/cmd/paint.rs b/crates/engine/src/cmd/paint.rs index 2bc62004..fc9424e1 100644 --- a/crates/engine/src/cmd/paint.rs +++ b/crates/engine/src/cmd/paint.rs @@ -524,7 +524,7 @@ fn default_paint(s: &mut Session, p: &Value) -> Result { r.style.stroke = Paint::None; } } - _ => n.appearance = Appearance::default_art(), + _ => n.appearance = Appearance::default_art().into(), } } Ok(()) diff --git a/crates/engine/src/cmd/pathops.rs b/crates/engine/src/cmd/pathops.rs index adf6afe7..89ab4b82 100644 --- a/crates/engine/src/cmd/pathops.rs +++ b/crates/engine/src/cmd/pathops.rs @@ -203,10 +203,10 @@ struct Style { impl Style { fn of(n: &Node) -> Self { - Style { appearance: n.appearance.clone(), opacity: n.opacity, blend: n.blend } + Style { appearance: (*n.appearance).clone(), opacity: n.opacity, blend: n.blend } } fn apply(&self, n: &mut Node) { - n.appearance = self.appearance.clone(); + n.appearance = self.appearance.clone().into(); n.opacity = self.opacity; n.blend = self.blend; } @@ -377,7 +377,7 @@ fn run_pf(s: &mut Session, op: PathfinderOp) -> Result { // Outline: open edges stroked with the face's former fill. let fill = st.appearance.fill_paint(); let paint = if fill.is_none() { st.appearance.stroke_paint() } else { fill }; - n.appearance = Appearance { items: vec![AppearanceItem::Stroke(StrokeLayer::new(paint, 1.0))], ..Default::default() }; + n.appearance = Appearance { items: vec![AppearanceItem::Stroke(StrokeLayer::new(paint, 1.0))], ..Default::default() }.into(); } children.push(Arc::new(n)); } @@ -519,7 +519,7 @@ pub(crate) fn outlined_stroke( overprint: st.overprint, ..vectorcraft_doc::appearance::FillLayer::new(st.paint.clone()) }; - n.appearance = Appearance { items: vec![AppearanceItem::Fill(fill)], ..Default::default() }; + n.appearance = Appearance { items: vec![AppearanceItem::Fill(fill)], ..Default::default() }.into(); Some(n) } @@ -554,7 +554,7 @@ pub(crate) fn outline_strokes(d: &mut Document, brushes: &[vectorcraft_brush::Br /// A plain copy of path `l` (new ids, no transparency, name or mask) painted with `items`. pub(crate) fn styled_copy(d: &mut Document, l: &Node, items: Vec) -> Node { let mut f = d.reid(l); - f.appearance = Appearance { items, ..Default::default() }; + f.appearance = Appearance { items, ..Default::default() }.into(); f.opacity = 1.0; f.blend = BlendMode::Normal; f.name = None; diff --git a/crates/engine/src/cmd/rasterfx.rs b/crates/engine/src/cmd/rasterfx.rs index 3339502b..013ed127 100644 --- a/crates/engine/src/cmd/rasterfx.rs +++ b/crates/engine/src/cmd/rasterfx.rs @@ -136,7 +136,7 @@ fn settings(s: &mut Session, p: &Value) -> Result { /// `art` in a clip group clipped by `path` filled by `rule` (a clipping path that paints nothing). pub(crate) fn clip_group(d: &mut Document, path: PathData, rule: FillRule, art: Node) -> Node { let mut clip = super::pathops::shape_node(d, path, None); - clip.appearance = Appearance::basic(Paint::None, Paint::None, 0.0); + clip.appearance = Appearance::basic(Paint::None, Paint::None, 0.0).into(); match &mut clip.kind { NodeKind::Path { clipping, rule: r, .. } => (*clipping, *r) = (true, rule), NodeKind::Compound { rule: r, .. } => *r = rule, @@ -363,7 +363,7 @@ fn split_items(out: &mut Document, n: &Node) -> Option { } let mut stroke_piece = |d: &mut Document, path: &PathData, rule: FillRule, st: &StrokeLayer| { let mut p = super::pathops::shape_node(d, path.clone(), None); - p.appearance = Appearance { items: vec![AppearanceItem::Stroke(st.clone())], ..Default::default() }; + p.appearance = Appearance { items: vec![AppearanceItem::Stroke(st.clone())], ..Default::default() }.into(); if let NodeKind::Path { rule: r, .. } | NodeKind::Compound { rule: r, .. } = &mut p.kind { *r = rule; } diff --git a/crates/engine/src/cmd/shaper.rs b/crates/engine/src/cmd/shaper.rs index b0966ccf..b7863217 100644 --- a/crates/engine/src/cmd/shaper.rs +++ b/crates/engine/src/cmd/shaper.rs @@ -334,7 +334,7 @@ fn generated(d: &mut Document, src: &[Arc], spec: &ShaperSpec, map: &Build let edit = edits.get(&i); let Some(source) = edit.and_then(|e| e.paint_source).or_else(|| fill_source(src, region)).and_then(|i| src.get(i)) else { continue }; let path = po::unite_all(&indices.iter().filter_map(|i| map.regions.get(*i).map(|r| (&r.0.path, FillRule::NonZero))).collect::>()); - let style = edit.and_then(|e| e.paint.clone()).unwrap_or_else(|| source.appearance.clone()); + let style = edit.and_then(|e| e.paint.clone()).unwrap_or_else(|| (*source.appearance).clone()); let mut n = Node::path(d.alloc_id(), path, fill_style(style)); n.opacity = source.opacity; n.blend = source.blend; @@ -358,7 +358,7 @@ fn generated(d: &mut Document, src: &[Arc], spec: &ShaperSpec, map: &Build continue; } let Some(source) = src.get(piece.key as usize) else { continue }; - let style = edge_edits.get(&i).and_then(|e| e.paint.clone()).unwrap_or_else(|| source.appearance.clone()); + let style = edge_edits.get(&i).and_then(|e| e.paint.clone()).unwrap_or_else(|| (*source.appearance).clone()); if style.stroke_paint().is_none() { continue; } @@ -448,7 +448,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> { for i in related { let Some(selector) = face_selector(&map, &src, i) else { continue }; if let Some(e) = spec.faces.iter_mut().rev().find(|e| e.selector == selector) { - e.paint = Some(n.appearance.clone()); + e.paint = Some((*n.appearance).clone()); } else { spec.faces.push(FaceEdit { selector, @@ -456,7 +456,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> { erase_edges: false, paint_source: None, merge, - paint: Some(n.appearance.clone()), + paint: Some((*n.appearance).clone()), }); } } @@ -472,7 +472,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> { && piece_point(&piece.path).is_some_and(|point| n.path_data().and_then(|p| p.nearest(point)).is_some_and(|p| p.4 < 1e-5)); if contained && let Some(mut e) = edge_selector(&map, &src, piece) { e.erase = false; - e.paint = Some(n.appearance.clone()); + e.paint = Some((*n.appearance).clone()); spec.edges.push(e); } } diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index 275a5364..c6f720fc 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -217,7 +217,7 @@ fn unitized(mut ap: Appearance, from: Option) -> Appearance { /// and stroke. Placed gradients come relative to the unit box (from the bounds of the object /// they're on). The pen pressure of strokes stays with their own path. pub(crate) fn captured(n: &Node) -> Appearance { - let mut ap = n.appearance.clone().without_pressure(); + let mut ap = (*n.appearance).clone().without_pressure(); if !ap.items.is_empty() { return unitized(ap, n.geometric_bounds()); } @@ -261,7 +261,7 @@ fn override_chars(n: &mut Node, ap: &Appearance) { /// Give `n` style `g`: its appearance (a group or layer too: its own, above its contents; type its /// characters' colour too with `chars`, Override Character Color) and transparency. fn style_node(n: &mut Node, g: &GraphicStyle, chars: bool) { - n.appearance = g.appearance_on(n).into_owned(); + n.appearance = g.appearance_on(n).into_owned().into(); if chars { override_chars(n, &g.appearance); } diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 364693d4..d830d824 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -561,7 +561,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result { d.remove(pid)?; let id = d.alloc_id(); let mut n = Node::new(id, NodeKind::Text(Box::new(t))); - n.appearance = Appearance::default(); + n.appearance = Appearance::default().into(); n.opacity = node.opacity; n.name = node.name.clone(); d.insert(par, idx, n)?; diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index 5230c5c5..99c960c9 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -248,14 +248,14 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result { } let st = t.runs.get(g.run).map(|r| r.style.clone()).unwrap_or_else(|| t.first_style()); let mut node = shape_node(d, path, None); - node.appearance = st.appearance(); + node.appearance = st.appearance().into(); children.push(Arc::new(node)); } // Underline and strikethrough bars become paths of their run's paint too (#847). for (run, bar) in vectorcraft_text::decorations(&lay, vectorcraft_text::FontDb::global(), t) { let st = t.runs.get(run).map(|r| r.style.clone()).unwrap_or_else(|| t.first_style()); let mut node = shape_node(d, PathData::from_bezpath(&bar).transformed(t.xf), None); - node.appearance = st.appearance(); + node.appearance = st.appearance().into(); children.push(Arc::new(node)); } let (par, idx, _) = d.position(tid).ok_or(EngineError::NoNode(tid))?; diff --git a/crates/engine/src/tests_containers.rs b/crates/engine/src/tests_containers.rs index dc11f0c0..211bca2e 100644 --- a/crates/engine/src/tests_containers.rs +++ b/crates/engine/src/tests_containers.rs @@ -103,7 +103,7 @@ fn effects_and_fills_apply_to_layers_by_id() { // Two undo steps take both off again. run(&mut s, "edit.undo", json!({})); run(&mut s, "edit.undo", json!({})); - assert_eq!(node(&s, layer).appearance, Default::default()); + assert_eq!(node(&s, layer).appearance, vectorcraft_doc::Appearance::default()); // Nothing selected and no ids: an error, not a silent no-op. assert!(s.execute("appearance.addFill", &json!({})).is_err()); } diff --git a/crates/engine/src/tests_envelope_distort.rs b/crates/engine/src/tests_envelope_distort.rs index 246b13ef..300a6ec5 100644 --- a/crates/engine/src/tests_envelope_distort.rs +++ b/crates/engine/src/tests_envelope_distort.rs @@ -34,7 +34,7 @@ fn sel(s: &mut Session, ids: &[NodeId]) { fn rect(s: &mut Session) -> NodeId { let a = id_of(&s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 100})).unwrap()); s.edit("red", |d, _| { - d.node_mut(a).unwrap().appearance = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0); + d.node_mut(a).unwrap().appearance = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0).into(); Ok(()) }) .unwrap(); diff --git a/crates/engine/src/tests_eyedropper.rs b/crates/engine/src/tests_eyedropper.rs index 9dd0a9bd..d3f83a01 100644 --- a/crates/engine/src/tests_eyedropper.rs +++ b/crates/engine/src/tests_eyedropper.rs @@ -126,7 +126,7 @@ fn shift_alt_click_appends_the_appearance() { let mut s = session(); let (_, dst) = source_and_target(&mut s); click(&mut s, 325.0, 35.0, Mods { alt: true, shift: true, ..Default::default() }); - let items = node(&s, dst).appearance.items; + let items = node(&s, dst).appearance.items.clone(); assert_eq!(items.len(), 4, "the source's fill and stroke on top of the target's"); assert_eq!((items[0].paint().color().unwrap().to_hex(), items[2].paint().color().unwrap().to_hex()), ("#ffffff".into(), "#ff0000".into())); } diff --git a/crates/engine/src/tests_pathops.rs b/crates/engine/src/tests_pathops.rs index 8a2d9396..91b991fb 100644 --- a/crates/engine/src/tests_pathops.rs +++ b/crates/engine/src/tests_pathops.rs @@ -229,7 +229,7 @@ fn outline_stroke_of_line() { let r = s.execute("shape.line", &json!({"x1": 0, "y1": 50, "x2": 100, "y2": 50})).unwrap(); let l = NodeId(r["id"].as_u64().unwrap()); s.edit("t", |d, _| { - d.node_mut(l).unwrap().appearance = Appearance::basic(Paint::None, Paint::solid(Color::rgb(1.0, 0.0, 0.0)), 10.0); + d.node_mut(l).unwrap().appearance = Appearance::basic(Paint::None, Paint::solid(Color::rgb(1.0, 0.0, 0.0)), 10.0).into(); Ok(()) }) .unwrap(); @@ -304,7 +304,7 @@ fn clean_up_removes_junk() { let keep = rect(&mut s, 0.0, 0.0, 10.0, 10.0); let unpainted = rect(&mut s, 20.0, 0.0, 10.0, 10.0); s.edit("t", |d, _| { - d.node_mut(unpainted).unwrap().appearance = Appearance::basic(Paint::None, Paint::None, 1.0); + d.node_mut(unpainted).unwrap().appearance = Appearance::basic(Paint::None, Paint::None, 1.0).into(); Ok(()) }) .unwrap(); diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 4ef288a8..2a3204aa 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -1330,7 +1330,7 @@ impl<'a> Reader<'a> { } let kind = NodeKind::Path { path, rule: gs.rule, live: None, clipping: clip, guide }; let mut node = self.new_node(kind, &gs); - node.appearance = Appearance { items, ..Appearance::default() }; + node.appearance = Appearance { items, ..Appearance::default() }.into(); node.opacity = gs.opacity; node.blend = gs.blend; node.knockout_shape = gs.knockout_shape; diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index 1bbf8337..10c462f6 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -1688,7 +1688,7 @@ pub(crate) fn collapse(nodes: &mut [Arc], shadings: &[NodeId]) { && shadings.contains(&fill.id) && let (NodeKind::Path { path, rule, clipping: true, .. }, NodeKind::Path { .. }) = (&clip.kind, &fill.kind) { - let mut p = Node::path(fill.id, path.clone(), fill.appearance.clone()); + let mut p = Node::path(fill.id, path.clone(), (*fill.appearance).clone()); if let NodeKind::Path { rule: r, .. } = &mut p.kind { *r = *rule; } diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 447018e2..99c69e7d 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -469,7 +469,7 @@ fn import_invalid_dash_array_strokes_solid() { // And the exporter never writes an invalid array back. let mut d2 = doc(100.0, 100.0); let mut n = rect_node(Rect::new(10.0, 10.0, 90.0, 90.0), Color::rgb(1.0, 0.0, 0.0)); - n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0); + n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0).into(); if let Some(AppearanceItem::Stroke(st)) = n.appearance.items.last_mut() { st.dash = Some(Dash { pattern: vec![-5.0, 3.0], offset: 0.0, align_corners: false }); } diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index b8098790..80ca5c52 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -947,7 +947,7 @@ mod tests { let l = d.layers[0].id; let id = d.layers[0].children().unwrap()[0].id; let mut n = (*d.node(id).unwrap()).clone(); - n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 4.0); + n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 4.0).into(); if let Some(vectorcraft_doc::AppearanceItem::Stroke(s)) = n.appearance.items.iter_mut().find(|i| matches!(i, vectorcraft_doc::AppearanceItem::Stroke(_))) { diff --git a/crates/svg/src/import.rs b/crates/svg/src/import.rs index 4deefef7..67983cfb 100644 --- a/crates/svg/src/import.rs +++ b/crates/svg/src/import.rs @@ -860,7 +860,7 @@ impl Importer { text::fold_scale(&mut obj, grow); let mut n = self.named(&name, NodeKind::Text(Box::new(obj))); if let Some(ap) = under { - n.appearance = ap; + n.appearance = ap.into(); } Some(vec![Arc::new(n)]) } @@ -1190,7 +1190,7 @@ impl Importer { .map(|sp| { let id = self.doc.alloc_id(); let mut c = Node::new(id, NodeKind::Path { path: PathData::single(sp), rule: r, live: None, clipping: false, guide: false }); - c.appearance = appearance.clone(); + c.appearance = appearance.clone().into(); Arc::new(c) }) .collect(); @@ -1198,7 +1198,7 @@ impl Importer { } else { self.named(p.id(), NodeKind::Path { path, rule: r, live: None, clipping: false, guide: false }) }; - n.appearance = appearance; + n.appearance = appearance.into(); Some(n) } diff --git a/crates/svg/tests/css_rules.rs b/crates/svg/tests/css_rules.rs index d3065416..4004c04c 100644 --- a/crates/svg/tests/css_rules.rs +++ b/crates/svg/tests/css_rules.rs @@ -188,7 +188,7 @@ fn shared_properties_read_as_svg_export_writes_them() { fn gradients_give_css_gradients() { let mut d = doc(); let mut n = rect(&mut d, (0.0, 0.0, 200.0, 100.0), 0.0); - n.appearance = Appearance::basic(Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))), Paint::None, 0.0); + n.appearance = Appearance::basic(Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))), Paint::None, 0.0).into(); let id = add(&mut d, n); let r = one(&d, id, &CssOptions::default()); assert_eq!(prop(&r, "background-image"), Some("linear-gradient(90deg, #ffffff 0%, #000000 100%)")); @@ -196,13 +196,13 @@ fn gradients_give_css_gradients() { let mut g = GradientPaint::new(Gradient::default()); g.geom = Some(GradientGeom { start: Point::new(100.0, 0.0), end: Point::new(100.0, 50.0), aspect: 1.0, focal: None }); let mut n = rect(&mut d, (0.0, 0.0, 200.0, 100.0), 0.0); - n.appearance = Appearance::basic(Paint::Gradient(Box::new(g)), Paint::None, 0.0); + n.appearance = Appearance::basic(Paint::Gradient(Box::new(g)), Paint::None, 0.0).into(); let id = add(&mut d, n); assert_eq!(prop(&one(&d, id, &CssOptions::default()), "background-image"), Some("linear-gradient(180deg, #ffffff 0%, #000000 50%)")); // Radial: a circle at the box's centre. let radial = GradientPaint::new(Gradient { kind: GradientKind::Radial, ..Gradient::default() }); let mut n = rect(&mut d, (0.0, 0.0, 100.0, 100.0), 0.0); - n.appearance = Appearance::basic(Paint::Gradient(Box::new(radial)), Paint::None, 0.0); + n.appearance = Appearance::basic(Paint::Gradient(Box::new(radial)), Paint::None, 0.0).into(); let id = add(&mut d, n); let bg = one(&d, id, &CssOptions::default()).props.into_iter().find(|(k, _)| *k == "background-image").unwrap().1; assert!(bg.starts_with("radial-gradient(circle ") && bg.contains(" at 50px 50px, #ffffff 0%, #000000 100%)"), "{bg}"); diff --git a/crates/svg/tests/roundtrip.rs b/crates/svg/tests/roundtrip.rs index c7433a7a..53c732ee 100644 --- a/crates/svg/tests/roundtrip.rs +++ b/crates/svg/tests/roundtrip.rs @@ -144,7 +144,7 @@ fn compound_evenodd_roundtrip() { let i = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(25.0, 25.0, 75.0, 75.0)), Appearance::default()); let cid = d.alloc_id(); let mut c = Node::new(cid, NodeKind::Compound { children: vec![Arc::new(o), Arc::new(i)], rule: FillRule::EvenOdd }); - c.appearance = Appearance::basic(solid("#123456"), Paint::None, 1.0); + c.appearance = Appearance::basic(solid("#123456"), Paint::None, 1.0).into(); let d = doc_with(vec![c]); let s = export(&d, &ExportOptions::default()); assert_eq!(s.matches("