mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 04:11:23 +00:00
Merge pull request #5778 from storytold/perf/1280-bounds-without-bezpath
#1280: perf --deep measures pointing; PathData::bounds builds no path
This commit is contained in:
@@ -187,6 +187,47 @@ fn budget_row(name: &str, ms: f64, budget: f64, ok: bool) -> String {
|
||||
format!(" {name:<42} {ms:>8.2} ms {budget:>7.0} ms {}", if ok { "ok" } else { "OVER" })
|
||||
}
|
||||
|
||||
/// Tracked rows for pointing at things in an n-path document: what hovering, clicking, marquee
|
||||
/// selection and snapping cost when nothing narrows the search.
|
||||
fn pointing(n: usize, rows: &mut Vec<(String, f64)>) -> Result<(), String> {
|
||||
use vectorcraft_engine::tools::{Mods, PointerEvent, PointerKind};
|
||||
let err = |e: vectorcraft_engine::EngineError| e.to_string();
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.add_document(synthetic(n)?, None);
|
||||
let view = vectorcraft_engine::ViewInfo { zoom: 1.0, ..Default::default() };
|
||||
s.select_tool("selection", view).map_err(err)?;
|
||||
// Off the art (nothing under the pointer), the case every hover over empty canvas pays.
|
||||
let off = Point::new(-200.0, -200.0);
|
||||
let hover = |s: &mut vectorcraft_engine::Session| {
|
||||
let _ = s.pointer(&PointerEvent::new(PointerKind::Move, off.x, off.y), view);
|
||||
let _ = s.cursor(off, Mods::default(), view);
|
||||
};
|
||||
rows.push((format!("hover over empty canvas ({n} paths)"), median_ms(5, || hover(&mut s))));
|
||||
s.execute("select.all", &json!({})).map_err(err)?;
|
||||
rows.push((format!("hover over empty canvas, all selected ({n} paths)"), median_ms(5, || hover(&mut s))));
|
||||
let doc = s.doc().map_err(err)?.doc.clone();
|
||||
rows.push((format!("hit test, a miss ({n} paths)"), median_ms(5, || drop(hit::hit_test(&doc, off, hit::HitOptions::default())))));
|
||||
let r = Rect::new(600.0, 450.0, 1000.0, 750.0);
|
||||
rows.push((format!("marquee 400×300 ({n} paths)"), median_ms(5, || drop(hit::marquee(&doc, r, None, false)))));
|
||||
let view_rect = Rect::new(0.0, 0.0, 1600.0, 1200.0);
|
||||
rows.push((
|
||||
format!("smart guide targets in view ({n} paths)"),
|
||||
median_ms(5, || drop(vectorcraft_engine::tools::guides::Targets::collect(&doc, &[], Some(view_rect)))),
|
||||
));
|
||||
// Shift-clicking one more object into a selection of half the document.
|
||||
let ids: Vec<u64> = doc.layers[0].children().map(|c| c.iter().map(|n| n.id.0).collect()).unwrap_or_default();
|
||||
s.execute("select.set", &json!({"ids": ids[..ids.len() / 2]})).map_err(err)?;
|
||||
let mut k = ids.len() / 2;
|
||||
rows.push((
|
||||
format!("shift-click into a {}-object selection", ids.len() / 2),
|
||||
median_ms(5, || {
|
||||
k += 1;
|
||||
let _ = s.execute("select.toggle", &json!({"id": ids[k % ids.len()]}));
|
||||
}),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `--deep`: tracked rows (no budget yet) for work the budgets above don't reach: editing a whole
|
||||
/// selection, live effects, every Pathfinder operation, typing in a long story, and memory, with
|
||||
/// the peak so far where `rss_mb` reads one.
|
||||
@@ -282,6 +323,8 @@ fn deep(n: usize) -> Result<(), String> {
|
||||
}),
|
||||
));
|
||||
note("after typing");
|
||||
pointing(n, &mut rows)?;
|
||||
note("after pointing");
|
||||
// Four embedded 24 MP photos drawn at fit page by three renderers, as the app's canvas, render
|
||||
// worker and a panel each draw them.
|
||||
{
|
||||
|
||||
+48
-2
@@ -547,17 +547,30 @@ impl PathData {
|
||||
/// Tight geometric bounds (curve extrema, not control points), measured by [`curve_bounds`]: a
|
||||
/// segment with a control point that is not finite or past `f64::MAX / 16` in x or y is measured
|
||||
/// by its control points.
|
||||
///
|
||||
/// Walks the segments in place, without building the path: the same segments in the same order,
|
||||
/// so the result is bit for bit `curve_bounds(&self.to_bezpath())` (a path of lone anchors, with
|
||||
/// no segment, is `Rect::ZERO`, as there).
|
||||
pub fn bounds(&self) -> Option<Rect> {
|
||||
if self.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let bp = self.to_bezpath();
|
||||
if self.anchor_count() == 1
|
||||
&& let Some(a) = self.subpaths.iter().find_map(|s| s.anchors.first())
|
||||
{
|
||||
return Some(Rect::from_points(a.p, a.p));
|
||||
}
|
||||
Some(curve_bounds(&bp))
|
||||
let mut bbox: Option<Rect> = None;
|
||||
for sp in &self.subpaths {
|
||||
// (a closed subpath's last segment ends on its first anchor: its `ClosePath` adds no line)
|
||||
for i in 0..sp.segment_count() {
|
||||
let c = sp.segment(i);
|
||||
// `to_bezpath_into` writes a line where neither handle is out, else a cubic.
|
||||
let seg = segment_bounds(if sp.segment_is_line(i) { PathSeg::Line(kurbo::Line::new(c.p0, c.p3)) } else { PathSeg::Cubic(c) });
|
||||
bbox = Some(bbox.map_or(seg, |bb| bb.union(seg)));
|
||||
}
|
||||
}
|
||||
Some(bbox.unwrap_or_default())
|
||||
}
|
||||
/// Bounds including control handles (used for quick culling).
|
||||
pub fn control_bounds(&self) -> Option<Rect> {
|
||||
@@ -720,6 +733,39 @@ fn push_curve(cur: &mut Option<SubPath>, last: Point, c1: Point, c2: Point, p: P
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `bounds` walks the anchors in place: bit for bit what the kurbo path it stands for gives,
|
||||
/// for lines, curves, open and closed subpaths, lone anchors and empty subpaths.
|
||||
#[test]
|
||||
fn bounds_match_the_bezpath_bit_for_bit() {
|
||||
use proptest::prelude::*;
|
||||
fn reference(pd: &PathData) -> Option<Rect> {
|
||||
if pd.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if pd.anchor_count() == 1 {
|
||||
let a = pd.subpaths.iter().find_map(|s| s.anchors.first())?;
|
||||
return Some(Rect::from_points(a.p, a.p));
|
||||
}
|
||||
Some(curve_bounds(&pd.to_bezpath()))
|
||||
}
|
||||
let bits = |r: Option<Rect>| r.map(|r| [r.x0, r.y0, r.x1, r.y1].map(f64::to_bits));
|
||||
let coord = || prop_oneof![(-1e3f64..1e3), Just(0.0), Just(1e-300), Just(-7.25)];
|
||||
let pt = move || (coord(), coord()).prop_map(|(x, y)| Point::new(x, y));
|
||||
// Handles retracted (equal to the anchor) a third of the time: lines and curves mix.
|
||||
let handle = move || prop_oneof![Just(None), pt().prop_map(Some)];
|
||||
let anchor =
|
||||
(pt(), handle(), handle()).prop_map(|(p, i, o)| Anchor { p, h_in: i.unwrap_or(p), h_out: o.unwrap_or(p), kind: AnchorKind::Corner });
|
||||
let sub = (proptest::collection::vec(anchor, 0..6), any::<bool>()).prop_map(|(a, c)| SubPath::new(a, c));
|
||||
let path = proptest::collection::vec(sub, 0..4).prop_map(PathData::new);
|
||||
proptest!(ProptestConfig::with_cases(2000), |(pd in path)| {
|
||||
prop_assert_eq!(bits(pd.bounds()), bits(reference(&pd)));
|
||||
});
|
||||
// Two lone anchors: no segment, as the path has none.
|
||||
let lone = PathData::new(vec![SubPath::polyline(&[Point::new(5.0, 5.0)], false), SubPath::polyline(&[Point::new(9.0, 1.0)], true)]);
|
||||
assert_eq!(lone.bounds(), Some(Rect::ZERO));
|
||||
assert_eq!(bits(lone.bounds()), bits(reference(&lone)));
|
||||
}
|
||||
|
||||
fn square() -> PathData {
|
||||
PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(10.0, 10.0), Point::new(0.0, 10.0)], true))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user