Render: the frame after anchors of a long path move updates only the segments that touch them

The renderer keeps the path of a path or compound path of 1,024 anchors
or more by object id, with the bounds of its segments and of its miter
spikes in runs of 128 segments. When anchors move and the subpaths and
anchor counts stay the same, the next frame rewrites only the path
elements of the segments that start or end at a moved anchor, and
measures again only the runs that hold them and the runs of the
segments with length before them. Other edits make the path again as
before. The pixels and the cull bounds are the same.

- StrokeLayer::bounds_on is unchanged. The miter tip of one joint
  (miter_tip) and the stroke box with given tips (bounds_with_tips) are
  new functions beside it, and Appearance::stroked_bounds_by takes the
  box of each shaped stroke, so the cull bounds of a kept path use the
  kept tips.
- geom::bez::has_length is the predicate reach.rs used for segments
  that draw, made public.

Docs: docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-05 06:28:22 +07:00
committed by bflatastic
parent a9c1fe085b
commit d9d33c9e4b
12 changed files with 686 additions and 12 deletions
+50 -2
View File
@@ -4,6 +4,7 @@ use super::{
holds_instance, holds_plugin_effect, images, kurbo, lru, mask_value, painted_items, plain_image, polygon_length, rects_overlap,
shape_masked_image, single_plain_fill, text_geom_snapped, white_shape_mask,
};
use crate::kept_paths::{self, KeptPath};
impl Renderer {
/// Cached cull bounds of a node (containers union their cached children), symbol instances
@@ -28,12 +29,56 @@ impl Renderer {
}
acc
}
_ => brush_fx::cull_bounds(symbols, a),
_ => self.kept_bounds(symbols, a).unwrap_or_else(|| brush_fx::cull_bounds(symbols, a)),
};
self.geom.insert(key, GeomEntry { node: a.clone(), bounds: b, path: None, stamp: self.stamp });
b
}
/// The kept path of long path or compound path `a` ([`KeptPath`]), made, or brought up to date
/// from the one kept for its id, when it is not `a`'s yet. `None` when `a` has none.
fn kept_path(&mut self, a: &Arc<Node>) -> Option<&mut KeptPath> {
if !kept_paths::kept(a) {
return None;
}
let stamp = self.stamp;
let made = match self.kept.get_mut(&a.id) {
Some(k) if Arc::ptr_eq(&k.node, a) => true,
Some(k) => {
// The object the path was made from, when no longer drawn, shares the path through
// its `geom` entry: dropping that entry lets the path change in place.
let old = Arc::as_ptr(&k.node) as usize;
if self.geom.get(&old).is_some_and(|e| Arc::ptr_eq(&e.node, &k.node) && e.stamp != stamp) {
self.geom.remove(&old);
}
k.update(a)
}
None => false,
};
if !made {
let k = KeptPath::new(a, stamp)?;
self.kept.insert(a.id, k);
}
let k = self.kept.get_mut(&a.id)?;
k.stamp = stamp;
Some(k)
}
/// Cull bounds of long path or compound path `a` measured from its kept path
/// ([`Self::kept_path`]): the same as [`brush_fx::cull_bounds`]. `None` when it has no kept path
/// or is measured another way ([`brush_fx::path_cull_bounds`]).
fn kept_bounds(&mut self, symbols: &SymbolBoxes, a: &Arc<Node>) -> Option<Option<Rect>> {
let k = self.kept_path(a)?;
brush_fx::path_cull_bounds(symbols, a, || {
let b = k.geometric()?;
let bp = k.path.clone();
Some(a.appearance.stroked_bounds_by(b, |s| {
let tips = k.tips(s);
s.bounds_with_tips(&bp, b, tips)
}))
})
}
/// Drop the cached bounds of symbol instances and of the objects that hold them when `doc`'s
/// symbols are not those of the document last drawn (by name and art): those bounds measured
/// the instances by their symbol's art.
@@ -105,7 +150,10 @@ impl Renderer {
{
return Some(p.clone());
}
let p = Arc::new(a.stroke_path()?);
let p = match self.kept_path(a) {
Some(k) => k.path.clone(),
None => Arc::new(a.stroke_path()?),
};
let bounds = self.bounds_of(symbols, a);
self.geom.insert(key, GeomEntry { node: a.clone(), bounds, path: Some(p.clone()), stamp: self.stamp });
Some(p)