Render: a compound path's members are assembled once and kept with the node

A compound path was assembled from its members in every frame it was
drawn, and Node::stroke_path grew its buffer by doubling, copying the
path into ever larger buffers for a compound path of many anchors. The
renderer's geometry cache was swept only once it held more than 1,024
entries, so while an object was dragged (a new node, and layer, in
every frame) the old copies stayed alive with all their members.

Now:
- a compound path is drawn from the path the renderer keeps for it in
  its geometry cache, as a plain path already was, and is assembled
  only when the node changes;
- Node::stroke_path sizes a compound path's buffer up front from its
  members' segment counts (the same elements, in the same order);
- unused geometry entries are dropped after a few frames however few
  entries there are.

Tests:
- crates/render/src/tests/memory.rs: the second frame draws the kept
  path and the same pixels as the first and as a new renderer; after a
  five-frame move the renderer no longer holds the compound path drawn
  before it. The test fails without the renderer change.
- crates/doc/src/node/tests.rs: a compound path's path is its members'
  paths in order (open, closed, empty and single-anchor members).

Not timed; vectorcraft-cli perf was not rerun.
This commit is contained in:
Ahmed Gomaa
2026-11-03 16:03:40 +07:00
committed by bflatastic
parent f0e72fb11a
commit 1d011ab8ad
6 changed files with 99 additions and 17 deletions
+7 -2
View File
@@ -185,8 +185,13 @@ impl Node {
match &self.kind {
NodeKind::Path { path, .. } => Some(path.to_bezpath()),
NodeKind::Compound { children, .. } => {
let mut bp = vectorcraft_geom::BezPath::new();
children.iter().filter_map(|c| c.path_data()).for_each(|pd| bp.extend(pd.to_bezpath()));
// Sized up front: grown by doubling, the path of a compound path of hundreds of
// thousands of anchors is copied into ever larger buffers, up to tens of MB, on
// every call.
let subpaths = || children.iter().filter_map(|c| c.path_data()).flat_map(|pd| &pd.subpaths);
let elements = subpaths().filter(|sp| !sp.anchors.is_empty()).map(|sp| 1 + sp.segment_count() + usize::from(sp.closed)).sum();
let mut bp = vectorcraft_geom::BezPath::from_vec(Vec::with_capacity(elements));
subpaths().for_each(|sp| sp.to_bezpath_into(&mut bp));
Some(bp)
}
_ => None,