mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 06:35:03 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user