mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
#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:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
7652439b40
commit
0724aafbb6
@@ -443,7 +443,7 @@ fn clear_appearance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// 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<Value> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ fn lp_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.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<Value> {
|
||||
&& 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<Value> {
|
||||
..StrokeLayer::new(paint, width * unit)
|
||||
})],
|
||||
..Default::default()
|
||||
},
|
||||
None => fill_only(paint),
|
||||
}
|
||||
.into(),
|
||||
None => fill_only(paint).into(),
|
||||
};
|
||||
children.push(Arc::new(n));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -575,7 +575,7 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -661,7 +661,7 @@ fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
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) => {
|
||||
|
||||
@@ -524,7 +524,7 @@ fn default_paint(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
r.style.stroke = Paint::None;
|
||||
}
|
||||
}
|
||||
_ => n.appearance = Appearance::default_art(),
|
||||
_ => n.appearance = Appearance::default_art().into(),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -136,7 +136,7 @@ fn settings(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// `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<Node> {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ fn generated(d: &mut Document, src: &[Arc<Node>], 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::<Vec<_>>());
|
||||
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<Node>], 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ fn unitized(mut ap: Appearance, from: Option<Rect>) -> 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);
|
||||
}
|
||||
|
||||
@@ -561,7 +561,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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)?;
|
||||
|
||||
@@ -248,14 +248,14 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
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))?;
|
||||
|
||||
Reference in New Issue
Block a user