#1172: share an object's appearance with its undo history until it changes

Every edit of an object copied its whole appearance (its fills, strokes and effects, about
540 bytes for a basic object) into the new node, though a move leaves it as it is. Over 45
whole-selection moves of 10,000 paths, undo history grew by 589 MB; now 349 MB (41 % less), and
a nudge or a drag of the selection takes 12 ms instead of 15.

- Node::appearance is a vectorcraft_doc::Shared<Appearance>: copy-on-write. A clone shares it,
  and the first change through DerefMut copies it (Arc::make_mut). Reads, writes, equality (also
  against a plain Appearance) and the saved form are those of the Appearance itself, so call
  sites read and change it as before. Sites that build or assign a whole appearance add .into(),
  and the few that need a plain copy take (*n.appearance).clone().
- Node::transform changes the appearance only when it has gradients to transform, and pattern
  paints are transformed only when there are some: a move no longer copies an appearance it
  doesn't change.

The workspace suite (5,301 tests) and clippy pass with the change; the doc and engine suites
pass again after the transform guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
This commit is contained in:
arhammusheer
2026-10-11 23:27:21 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 7652439b40
commit 0724aafbb6
42 changed files with 159 additions and 71 deletions
+5 -5
View File
@@ -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<Value> {
// 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<AppearanceItem>) -> 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;