Merge pull request #6742 from dotCooCoo/fix/long-path-frame

Render: a long path is checked against the rasterizer's limits from its kept boxes
This commit is contained in:
BFlat
2026-11-12 09:08:25 +07:00
committed by GitHub
5 changed files with 97 additions and 21 deletions
+44 -11
View File
@@ -46,6 +46,8 @@ struct Sub {
ends: Vec<usize>,
/// The box of each run of segments ([`segment_bounds`]).
boxes: Vec<Option<Rect>>,
/// The box of the points of each run of segments: their ends and their control points.
controls: Vec<Option<Rect>>,
/// The box of the miter tips of `tips_of` at the joints whose first segment is in each run.
tips: Vec<Option<Rect>>,
}
@@ -122,6 +124,15 @@ fn end(el: &PathEl) -> Option<Point> {
}
}
/// 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<usize> {
(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<Rect> {
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<PathSeg> {
@@ -297,10 +326,7 @@ impl Sub {
let mut boxes: Vec<usize> = 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<usize> = 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<usize> = self.ends.get(r1..)?.iter().map(|&e| e.checked_add(new)?.checked_sub(old)).collect::<Option<_>>()?;
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.
+6 -5
View File
@@ -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
+1 -1
View File
@@ -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;
+17 -1
View File
@@ -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<usize> = 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<usize> = const { std::cell::Cell::new(0) };
}
/// Paint `p` of a slice ([`Slices`]) moved by `by`: a gradient's start, end and focal point moved.
+29 -3
View File
@@ -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<Node>, 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<Node>, 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");
}