diff --git a/crates/render/src/kept_paths.rs b/crates/render/src/kept_paths.rs index 293e82466..a6deb8e33 100644 --- a/crates/render/src/kept_paths.rs +++ b/crates/render/src/kept_paths.rs @@ -46,6 +46,8 @@ struct Sub { ends: Vec, /// The box of each run of segments ([`segment_bounds`]). boxes: Vec>, + /// The box of the points of each run of segments: their ends and their control points. + controls: Vec>, /// The box of the miter tips of `tips_of` at the joints whose first segment is in each run. tips: Vec>, } @@ -122,6 +124,15 @@ fn end(el: &PathEl) -> Option { } } +/// The box of the points of segment `s`: its ends and its control points. +fn segment_controls(s: PathSeg) -> Rect { + match s { + PathSeg::Line(l) => Rect::from_points(l.p0, l.p1), + PathSeg::Quad(q) => Rect::from_points(q.p0, q.p2).union_pt(q.p1), + PathSeg::Cubic(c) => Rect::from_points(c.p0, c.p3).union_pt(c.p1).union_pt(c.p2), + } +} + /// The ends of the runs of a subpath of `segs` segments measured whole: [`RUN`] segments each. fn whole_runs(segs: usize) -> Vec { (1..=segs.div_ceil(RUN)).map(|r| (r * RUN).min(segs)).collect() @@ -146,8 +157,10 @@ impl Sub { /// Subpath `sp`, its MoveTo at `start` in `els`, measured whole, with the tips of `st`. fn measured(sp: &SubPath, start: usize, els: &[PathEl], st: Option<&StrokeLayer>) -> Self { let segs = sp.segment_count(); - let mut sub = Sub { start, segs, anchors: sp.anchors.len(), closed: sp.closed, ends: whole_runs(segs), boxes: vec![], tips: vec![] }; + let ends = whole_runs(segs); + let mut sub = Sub { start, segs, anchors: sp.anchors.len(), closed: sp.closed, ends, boxes: vec![], controls: vec![], tips: vec![] }; sub.boxes = (0..sub.runs()).map(|r| sub.run_box(els, r)).collect(); + sub.controls = (0..sub.runs()).map(|r| sub.run_controls(els, r)).collect(); if let Some(st) = st { sub.tips = (0..sub.runs()).map(|r| sub.run_tips(els, r, st)).collect(); } @@ -191,6 +204,22 @@ impl Sub { self.range(r).filter_map(|i| self.seg(els, i)).map(segment_bounds).reduce(|a, b| a.union(b)) } + /// The box of the points of the segments of run `r`: their ends and their control points. + fn run_controls(&self, els: &[PathEl], r: usize) -> Option { + self.range(r).filter_map(|i| self.seg(els, i)).map(segment_controls).reduce(|a, b| a.union(b)) + } + + /// Measure again the box and the box of the points of run `r`. + fn measure_run(&mut self, els: &[PathEl], r: usize) { + let (b, c) = (self.run_box(els, r), self.run_controls(els, r)); + if let Some(slot) = self.boxes.get_mut(r) { + *slot = b; + } + if let Some(slot) = self.controls.get_mut(r) { + *slot = c; + } + } + /// The segment with length that follows segment `i` (round to the start when closed), or /// `None` when there is none other than `i`. fn next_drawn(&self, els: &[PathEl], i: usize) -> Option { @@ -297,10 +326,7 @@ impl Sub { let mut boxes: Vec = if all { (0..self.runs()).collect() } else { segs.iter().map(|&i| self.run_of(i)).collect() }; boxes.dedup(); for r in boxes { - let b = self.run_box(els, r); - if let Some(slot) = self.boxes.get_mut(r) { - *slot = b; - } + self.measure_run(els, r); } let Some(st) = st else { return }; // A joint's tip changes with its segments and with which segment with length comes next: @@ -380,11 +406,8 @@ impl Sub { self.anchors = n1; self.closed = c1; let els: &[PathEl] = els; - for (r, _) in stale.iter().enumerate().filter(|(_, s)| **s) { - let b = self.run_box(els, r); - if let Some(slot) = self.boxes.get_mut(r) { - *slot = b; - } + for r in stale.iter().enumerate().filter(|(_, s)| **s).map(|(r, _)| r) { + self.measure_run(els, r); } let Some(st) = st else { return Some(()) }; let mut runs: Vec = stale.iter().enumerate().filter(|(_, s)| **s).map(|(r, _)| r).collect(); @@ -422,7 +445,7 @@ impl Sub { let len = last.checked_sub(first)?; let segs = self.segs.checked_add(new)?.checked_sub(old)?; let after: Vec = self.ends.get(r1..)?.iter().map(|&e| e.checked_add(new)?.checked_sub(old)).collect::>()?; - if r1 > self.boxes.len() || r1 > stale.len() { + if r1 > self.boxes.len() || r1 > self.controls.len() || r1 > stale.len() { return None; } let count = if len > 2 * RUN { len.div_ceil(RUN) } else { usize::from(len > 0) }; @@ -430,6 +453,7 @@ impl Sub { self.ends.extend(after); self.ends.splice(r0..r1, (1..=count).map(|j| if j == count { last } else { first + j * RUN })); self.boxes.splice(r0..r1, std::iter::repeat_n(None, count)); + self.controls.splice(r0..r1, std::iter::repeat_n(None, count)); if tips { self.tips.splice(r0.min(self.tips.len())..r1.min(self.tips.len()), std::iter::repeat_n(None, count)); } @@ -602,6 +626,15 @@ impl KeptPath { }) } + /// The box of the points of the path's elements, as [`BezPath::control_box`] measures it: each + /// subpath's MoveTo and the boxes of the points of its runs, joined. + pub(crate) fn control_box(&self) -> Rect { + let els = self.path.elements(); + let start = |s: &Sub| els.get(s.start).and_then(end).filter(|_| s.anchors > 0).map(|p| Rect::from_points(p, p)); + let runs = |s: &Sub| s.controls.iter().flatten().copied().reduce(|a, b| a.union(b)); + self.parts.iter().flatten().flat_map(|s| [start(s), runs(s)]).flatten().reduce(|a, b| a.union(b)).unwrap_or_default() + } + /// The box of the miter tips of `st` at every joint of the path ([`StrokeLayer::miter_tip`]), /// for [`StrokeLayer::bounds_with_tips`]: measured whole when the tips kept are another /// stroke's, then kept up to date with the path. diff --git a/crates/render/src/node_checks.rs b/crates/render/src/node_checks.rs index bc516567d..a4d9fda7a 100644 --- a/crates/render/src/node_checks.rs +++ b/crates/render/src/node_checks.rs @@ -1,4 +1,4 @@ -use super::{Affine, AppearanceItem, Arc, BezPath, Document, Node, NodeKind, StrokeLayer, brush_fx, effects}; +use super::{Affine, AppearanceItem, Arc, Document, Node, NodeKind, Rect, StrokeLayer, brush_fx, effects}; /// Most pixels from the origin that a shape handed to the rasterizer may reach, or a stroke be /// wide. A document's art stays within 4 million points, 2.6 billion pixels at the closest zoom. @@ -6,10 +6,11 @@ use super::{Affine, AppearanceItem, Arc, BezPath, Document, Node, NodeKind, Stro /// for more memory than there is, either of which ends the app: it is left undrawn. const MAX_DEVICE: f64 = 1e10; -/// Can shape `bp` of `n` be drawn through `view`: its control points (finite) within -/// [`MAX_DEVICE`] pixels, and its visible strokes no wider? -pub(crate) fn drawable(bp: &BezPath, view: Affine, n: &Node) -> bool { - let b = view.transform_rect_bbox(bp.control_box()); +/// Can a shape of `n` whose control points have the box `control` (`BezPath::control_box`) be +/// drawn through `view`: its control points (finite) within [`MAX_DEVICE`] pixels, and its visible +/// strokes no wider? +pub(crate) fn drawable(control: Rect, view: Affine, n: &Node) -> bool { + let b = view.transform_rect_bbox(control); let scale = view.determinant().abs().sqrt(); let widest = n.appearance.items.iter().filter_map(|i| if let AppearanceItem::Stroke(st) = i { Some(st.width) } else { None }).fold(0.0, f64::max); [b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite() && v.abs() <= MAX_DEVICE) && (widest * scale).is_finite() && widest * scale <= MAX_DEVICE diff --git a/crates/render/src/renderer.rs b/crates/render/src/renderer.rs index 431e0aa97..fc729c1cc 100644 --- a/crates/render/src/renderer.rs +++ b/crates/render/src/renderer.rs @@ -19,7 +19,7 @@ use super::{ }; #[cfg(test)] -pub(crate) use paint::{SLICES_MADE, STROKES_OUTLINED, anchors_moved, offset_between}; +pub(crate) use paint::{CONTROL_BOXES_MEASURED, SLICES_MADE, STROKES_OUTLINED, anchors_moved, offset_between}; mod images; mod nodes; diff --git a/crates/render/src/renderer/paint.rs b/crates/render/src/renderer/paint.rs index fd205496a..00204f8f1 100644 --- a/crates/render/src/renderer/paint.rs +++ b/crates/render/src/renderer/paint.rs @@ -22,7 +22,7 @@ impl Renderer { } pub(super) fn draw_shape(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, bp: &BezPath, rule: FillRule) { - if !drawable(bp, f.view, n) { + if !drawable(self.control_box(n, bp), f.view, n) { return; } if fx::has_fx(n) { @@ -73,6 +73,19 @@ impl Renderer { } } + /// The box of the control points of shape `bp` of `n`: kept with its path when `bp` is the + /// path kept for `n`'s id ([`crate::kept_paths::KeptPath::control_box`]), else measured. + fn control_box(&self, n: &Node, bp: &BezPath) -> Rect { + if let Some(k) = self.kept.get(&n.id) + && std::ptr::eq(Arc::as_ptr(&k.path), bp) + { + return k.control_box(); + } + #[cfg(test)] + CONTROL_BOXES_MEASURED.with(|c| c.set(c.get() + 1)); + bp.control_box() + } + /// Multiply the folded object opacity into a solid paint. pub(super) fn fold_alpha(&self, ctx: &mut RenderContext, ink: Ink, p: &vectorcraft_color::Paint) { if self.alpha < 1.0 @@ -471,6 +484,9 @@ thread_local! { /// How many sets of gradient stroke slices [`Renderer::fill_path_gradient`] has made on this /// thread. pub(crate) static SLICES_MADE: std::cell::Cell = const { std::cell::Cell::new(0) }; + /// How many shapes' control boxes [`Renderer::draw_shape`] has measured from their path on this + /// thread. + pub(crate) static CONTROL_BOXES_MEASURED: std::cell::Cell = const { std::cell::Cell::new(0) }; } /// Paint `p` of a slice ([`Slices`]) moved by `by`: a gradient's start, end and focal point moved. diff --git a/crates/render/src/tests/keptpath.rs b/crates/render/src/tests/keptpath.rs index a2f8b3ad8..e7097c485 100644 --- a/crates/render/src/tests/keptpath.rs +++ b/crates/render/src/tests/keptpath.rs @@ -72,8 +72,8 @@ fn check(r: &mut Renderer, d: &Document, id: NodeId, what: &str) { let k = r.kept.get(&id).unwrap_or_else(|| panic!("{what}: its path kept")); assert!(std::ptr::eq(Arc::as_ptr(&k.node), n), "{what}: its path kept"); // The subpaths kept are those of the path measured whole, each run holds from one segment to - // twice the segments of a run, and the boxes and miter tips kept, run by run, are those - // measured again from the path. + // twice the segments of a run, and the boxes and miter tips kept, run by run, and the control + // box are those measured again from the path. let whole = crate::kept_paths::KeptPath::new(&Arc::new(n.clone()), 0).unwrap(); let (layout, fresh) = (k.layout(), whole.layout()); assert!(layout.iter().map(|l| l.0).eq(fresh.iter().map(|l| l.0)), "{what}: the same subpaths as the path measured whole"); @@ -83,6 +83,7 @@ fn check(r: &mut Renderer, d: &Document, id: NodeId, what: &str) { } let (runs, again, _) = k.runs(); assert!(runs == again, "{what}: the same boxes and tips as measured again"); + assert_eq!(k.control_box(), k.path.control_box(), "{what}: the same control box as measured again"); } /// Check `d` as [`check`] does, then the document before the edit (undone) and `d` again (redone). @@ -463,7 +464,7 @@ fn random_edit(g: &mut Seeded, p: &mut PathData) -> String { /// Bring `k` up to date with `n` and check that it holds what [`KeptPath::new`] makes from `n`: /// the same path elements and subpaths, runs that hold each subpath's segments, the boxes and miter -/// tips of `st` measured again over its runs, and the same bounds and tips in all. +/// tips of `st` measured again over its runs, and the same bounds, tips and control box in all. fn same_as_made_again(k: &mut crate::kept_paths::KeptPath, n: &Arc, st: &StrokeLayer, what: &str) { assert!(k.update(n), "{what}: brought up to date"); let mut fresh = crate::kept_paths::KeptPath::new(n, 0).unwrap_or_else(|| panic!("{what}: kept")); @@ -478,6 +479,7 @@ fn same_as_made_again(k: &mut crate::kept_paths::KeptPath, n: &Arc, st: &S assert!(runs == again, "{what}: the same boxes and tips as measured again"); assert_eq!(k.geometric(), fresh.geometric(), "{what}: the same bounds"); assert_eq!(k.tips(st), fresh.tips(st), "{what}: the same miter tips"); + assert_eq!(k.control_box(), fresh.path.control_box(), "{what}: the same control box"); } #[test] @@ -578,3 +580,27 @@ fn moving_one_anchor_of_a_long_path_changes_its_kept_path_in_place() { check(&mut r, &d, id, "one anchor moved"); assert_eq!(kept(&r), before, "the kept path changed in place"); } + +#[test] +fn a_long_path_is_drawn_without_measuring_all_of_its_control_points_in_each_frame() { + let mut d = Document::new(1000.0, 1000.0); + let n = Node::path(d.alloc_id(), PathData::single(zigzag(3000, 0.0)), look(vec![stroke(6.0, LineJoin::Miter, LineCap::Butt)])); + let id = add(&mut d, n); + let empty = { + let mut e = d.clone(); + Arc::make_mut(&mut e.layers[0]).children_mut().unwrap().clear(); + render(&mut Renderer::new(), &e).pixels + }; + let measured = || crate::renderer::CONTROL_BOXES_MEASURED.with(std::cell::Cell::get); + let before = measured(); + let mut r = Renderer::new(); + assert!(render(&mut r, &d).pixels != empty, "the path is drawn"); + render(&mut r, &d); + moved(&mut path_of(&mut d, id).subpaths[0].anchors[1500], Vec2::new(0.0, 40.0)); + check(&mut r, &d, id, "one anchor moved"); + assert_eq!(measured(), before, "drawn from the control box kept with its path"); + // An anchor far past what the rasterizer takes still leaves the path undrawn. + path_of(&mut d, id).subpaths[0].anchors[1500] = corner(12_000.0, 1e11); + assert!(render(&mut r, &d).pixels == empty, "an anchor 1e11 pt away: undrawn"); + assert_eq!(measured(), before, "drawn from the control box kept with its path"); +}