mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 23:00:35 +00:00
Merge pull request #5262 from thisisgomaa/perf/compound-path-geom-cache
Render: a compound path's members are assembled once and kept with the node
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -546,3 +546,23 @@ fn fields_before_the_type_are_buffered_within_the_value_budget() {
|
||||
assert!(with_buffered_value_budget(0, || read(&written)).is_ok(), "a saved object buffers nothing: {written}");
|
||||
assert!(read(reordered).is_ok(), "no budget outside a read");
|
||||
}
|
||||
|
||||
/// A compound path's path is its members' paths one after another, open and closed, empty
|
||||
/// members and single anchors included.
|
||||
#[test]
|
||||
fn compound_stroke_path_is_its_members_in_order() {
|
||||
use vectorcraft_geom::{Anchor, PathData, SubPath};
|
||||
let open = PathData::single(SubPath::new([(0.0, 0.0), (5.0, 1.0), (9.0, 7.0)].map(|(x, y)| Anchor::corner(Point::new(x, y))).to_vec(), false));
|
||||
let lone = PathData::single(SubPath::new(vec![Anchor::corner(Point::new(3.0, 3.0))], false));
|
||||
let members =
|
||||
[shapes::rectangle(Rect::new(0.0, 0.0, 4.0, 2.0)), open, PathData::default(), lone, shapes::ellipse(Rect::new(10.0, 10.0, 20.0, 16.0))];
|
||||
let mut expected = vectorcraft_geom::BezPath::new();
|
||||
for pd in &members {
|
||||
expected.extend(pd.to_bezpath());
|
||||
}
|
||||
let children = members.iter().enumerate().map(|(i, pd)| Arc::new(Node::path(NodeId(i as u64 + 1), pd.clone(), Appearance::default()))).collect();
|
||||
let n = Node::new(NodeId(99), NodeKind::Compound { children, rule: vectorcraft_geom::FillRule::EvenOdd });
|
||||
let bp = n.stroke_path().unwrap();
|
||||
assert_eq!(bp.elements(), expected.elements());
|
||||
assert!(!bp.elements().is_empty());
|
||||
}
|
||||
|
||||
@@ -172,9 +172,11 @@ impl Renderer {
|
||||
/// so the nodes they hold go with them).
|
||||
fn end_frame(&mut self) {
|
||||
let g = self.stamp;
|
||||
if self.geom.len() > 1024 {
|
||||
self.geom.retain(|_, e| g - e.stamp <= 3);
|
||||
}
|
||||
// Swept however few the entries: each keeps its node alive, and an object dragged or
|
||||
// edited makes a new node (with its layer) in every frame. Kept until there were 1,024 of
|
||||
// them, a drag left up to that many old copies of the object alive, each with all its
|
||||
// members (and, for a compound path, its assembled path).
|
||||
self.geom.retain(|_, e| g - e.stamp <= 3);
|
||||
if self.strokes.len() > 1024 {
|
||||
self.strokes.retain(|_, e| g - e.stamp <= 3);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,16 @@ impl Renderer {
|
||||
/// The cached path of path or compound path `n` ([`Self::path_of`]) when the frame is in
|
||||
/// Outline view and `n` is `fx_owner`, else `None`.
|
||||
fn outline_path(&mut self, f: &Frame, n: &Node) -> Option<Arc<BezPath>> {
|
||||
let owner = self.fx_owner.as_ref().filter(|o| f.opts.outline && std::ptr::eq(&***o, n)).cloned()?;
|
||||
if !f.opts.outline {
|
||||
return None;
|
||||
}
|
||||
self.owner_path(f, n)
|
||||
}
|
||||
|
||||
/// The cached path of path or compound path `n` ([`Self::path_of`]) when `n` is `fx_owner`,
|
||||
/// else `None`.
|
||||
fn owner_path(&mut self, f: &Frame, n: &Node) -> Option<Arc<BezPath>> {
|
||||
let owner = self.fx_owner.as_ref().filter(|o| std::ptr::eq(&***o, n)).cloned()?;
|
||||
self.path_of(f.symbols, &owner)
|
||||
}
|
||||
|
||||
@@ -470,17 +479,13 @@ impl Renderer {
|
||||
self.draw_shape(ctx, f, n, &bp, *rule);
|
||||
}
|
||||
}
|
||||
NodeKind::Compound { children, rule } => {
|
||||
if let Some(bp) = self.outline_path(f, n) {
|
||||
return self.draw_shape(ctx, f, n, &bp, *rule);
|
||||
NodeKind::Compound { rule, .. } => {
|
||||
// Its members assembled once and kept with the node in `geom`, as plain paths are
|
||||
// (`path_of`): a compound path of thousands of members is not assembled again in
|
||||
// every frame.
|
||||
if let Some(bp) = self.owner_path(f, n).or_else(|| n.stroke_path().map(Arc::new)) {
|
||||
self.draw_shape(ctx, f, n, &bp, *rule);
|
||||
}
|
||||
let mut bp = BezPath::new();
|
||||
for c in children {
|
||||
if let Some(p) = c.path_data() {
|
||||
bp.extend(p.to_bezpath());
|
||||
}
|
||||
}
|
||||
self.draw_shape(ctx, f, n, &bp, *rule);
|
||||
}
|
||||
NodeKind::Text(t) => self.draw_text(ctx, f, n, t),
|
||||
NodeKind::Image(im) => self.draw_image_in(ctx, f, im, None, images::image_tint(n, f.ink), n.extras().pixelated),
|
||||
|
||||
@@ -2378,3 +2378,47 @@ fn text_runs_draw_their_own_glyphs_from_the_shared_outlines() {
|
||||
}
|
||||
assert!(g.run(7).elements().is_empty());
|
||||
}
|
||||
|
||||
/// A compound path is drawn from the path the renderer keeps for it, as a plain path is: the next
|
||||
/// frame draws the same path, and draws what a new renderer draws.
|
||||
/// Once the compound path changes (each frame of a drag makes a new one), the entries of the old
|
||||
/// one, which keep it alive, go after a few frames, however few entries the renderer holds.
|
||||
#[test]
|
||||
fn compound_paths_draw_from_kept_paths_and_let_old_ones_go() {
|
||||
let _fonts = fonts_steady();
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let layer = d.layers[0].id;
|
||||
let members: Vec<Arc<Node>> = (0..30)
|
||||
.map(|i| {
|
||||
let x = f64::from(i % 6) * 15.0;
|
||||
let y = f64::from(i / 6) * 15.0;
|
||||
let p = vectorcraft_geom::shapes::ellipse(Rect::new(x, y, x + 10.0, y + 10.0));
|
||||
Arc::new(Node::path(d.alloc_id(), p, vectorcraft_doc::Appearance::default()))
|
||||
})
|
||||
.collect();
|
||||
let id = d.alloc_id();
|
||||
let mut compound = Node::new(id, NodeKind::Compound { children: members, rule: vectorcraft_geom::FillRule::EvenOdd });
|
||||
compound.appearance = vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0).into();
|
||||
d.insert(Some(layer), 0, compound).unwrap();
|
||||
let opts = RenderOptions::default();
|
||||
let kept = |r: &Renderer, d: &Document| {
|
||||
let n = d.layers[0].children().unwrap()[0].clone();
|
||||
r.geom.get(&(Arc::as_ptr(&n) as usize)).and_then(|e| e.path.clone())
|
||||
};
|
||||
let mut r = Renderer::new();
|
||||
let first = r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels;
|
||||
let path = kept(&r, &d).expect("the compound path's path is kept");
|
||||
assert!(r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels == first);
|
||||
assert!(kept(&r, &d).is_some_and(|p| Arc::ptr_eq(&p, &path)), "the second frame draws the kept path");
|
||||
assert!(first == Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts).pixels);
|
||||
// Moved: a new compound path (and layer) in each frame.
|
||||
let old = d.layers[0].children().unwrap()[0].clone();
|
||||
let mut moved = d.clone();
|
||||
for _ in 0..5 {
|
||||
moved.node_mut(id).unwrap().transform(Affine::translate((1.0, 0.0)), false);
|
||||
r.render(&moved, 100, 100, Affine::IDENTITY, &opts);
|
||||
}
|
||||
drop(d);
|
||||
assert!(r.geom.len() < 1024);
|
||||
assert_eq!(Arc::strong_count(&old), 1, "the renderer no longer keeps the compound path drawn before the move");
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
# Gaps
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (G15: clicks on paths and compound paths are culled by their control points; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (G15: a compound path's assembled path is kept between frames; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -619,6 +619,11 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
## G15 Performance budgets and a GPU renderer
|
||||
|
||||
- **Improved (2026-11-02):** the renderer keeps a compound path's assembled path with the node, as it keeps a plain
|
||||
path's, instead of assembling its members again in every frame; the path is assembled into a buffer sized up front.
|
||||
Unused geometry entries are dropped after a few frames however few there are (before, only past 1,024 entries), so
|
||||
the copies a drag makes in every frame no longer stay alive with all their members
|
||||
(`crates/render/src/tests/memory.rs`, `crates/doc/src/node/tests.rs`). Not timed; `vectorcraft-cli perf` not rerun.
|
||||
- **Improved (2026-11-02):** a click or hover culls paths and compound paths by the box of their control points
|
||||
(grown by their strokes' reach) instead of measuring along their curves, and tests a compound path's outline and
|
||||
fill against only the members whose control box, grown by the tolerance, holds the point; painted strokes are
|
||||
@@ -724,6 +729,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | minor | G15: the renderer keeps a compound path's assembled path between frames and drops unused geometry entries after a few frames however few there are. Estimate unchanged. |
|
||||
| 2026-11-02 | minor | G15: hit testing culls paths and compound paths by their control points' box and tests a compound path's outline and fill against only its members near the click; the same objects are hit. Estimate unchanged. |
|
||||
| 2026-11-03 | minor | Symbol editing shows draggable 9-slice guides within the artwork (#5280). Guides outside the artwork remain unsupported. The G5 estimate remains 14–24 h. |
|
||||
| 2026-10-31 | minor | G12: Units › East Asian Type acts (#394): Q and Ha (a quarter of a millimetre each) are units that preference offers; 4 Preferences options remain stored but not acted on. Estimate unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user