Compare commits

..
4 changed files with 54 additions and 53 deletions
+3 -1
View File
@@ -125,7 +125,9 @@ pub fn draw(ctx: &mut RenderContext, bevel: &Bevel, fill: Option<Rgba>, path: &B
let longest = bounds.width().max(bounds.height()) * k;
let s = (MAX_SIDE / longest).min(1.0) * k;
let (pw, ph) = (((bounds.width() * s).ceil() as u32).clamp(1, 2048), ((bounds.height() * s).ceil() as u32).clamp(1, 2048));
let polys = crate::pathgrad::polygons(path);
// Each pixel tests every edge, so a dense outline is simplified to the raster's edge budget.
let pixel = (bounds.width() / f64::from(pw)).min(bounds.height() / f64::from(ph));
let polys = crate::pathgrad::simplify(crate::pathgrad::polygons(path), pixel, crate::pathgrad::edge_budget(pw, ph));
if polys.is_empty() {
return;
}
+15 -1
View File
@@ -11,6 +11,8 @@ use vello_cpu::{RenderContext, peniko};
/// Nesting depth of clips and layers past which a glyph opens no more (hostile fonts).
const MAX_DEPTH: usize = 64;
/// Fills and gradient stops one colour glyph may paint; the rest of a hostile glyph is skipped.
const MAX_WORK: usize = 100_000;
/// Paint `gid` of `face` as a colour glyph, its origin mapped by `at` (y down, points) at `scale`
/// points per font unit. `fg` is the text colour (palette index 0xFFFF) and `alpha` fades it all.
@@ -26,7 +28,7 @@ pub fn paint(ctx: &mut RenderContext, face: &FontFace, gid: u32, at: Affine, sca
.unwrap_or_default();
// Font units are y up; the slide is y down.
let base = at * Affine::scale(scale) * Affine::scale_non_uniform(1.0, -1.0);
let mut p = Painter { ctx, face, base, stack: vec![Affine::IDENTITY], palette, fg, opened: Vec::new() };
let mut p = Painter { ctx, face, base, stack: vec![Affine::IDENTITY], palette, fg, opened: Vec::new(), work: 0 };
p.ctx.push_layer(None, None, Some(alpha.clamp(0.0, 1.0) as f32), None, None);
let ok = glyph.paint(face.location(), &mut p).is_ok();
// A malformed glyph may leave layers open: close them all.
@@ -50,6 +52,8 @@ struct Painter<'a> {
/// Clips and composite groups pushed by the glyph, innermost last: true when a layer was
/// opened on the context for it (false past `MAX_DEPTH`, so its pop is skipped too).
opened: Vec<bool>,
/// Fills plus gradient stops painted so far, against [`MAX_WORK`].
work: usize,
}
fn affine(t: &Transform) -> Affine {
@@ -174,6 +178,16 @@ impl ColorPainter for Painter<'_> {
}
fn fill(&mut self, brush: Brush<'_>) {
let stops = match &brush {
Brush::Solid { .. } => 0,
Brush::LinearGradient { color_stops, .. } | Brush::RadialGradient { color_stops, .. } | Brush::SweepGradient { color_stops, .. } => {
color_stops.len()
}
};
self.work = self.work.saturating_add(1 + stops);
if self.work > MAX_WORK {
return;
}
match brush {
Brush::Solid { palette_index, alpha } => {
let c = self.color(palette_index, alpha);
+33 -14
View File
@@ -25,6 +25,16 @@ const MAX_EDGE_WORK: u64 = 64_000_000;
/// Fewest edges a simplified polygon keeps, so a coarse outline still has a shape.
const MIN_POLY_EDGES: usize = 8;
/// Most points `polygons` keeps from one path; a path flattening past this (a huge finite curve) is
/// not drawn rather than held in memory first.
const MAX_POINTS: usize = 2_000_000;
/// The edge budget for a `pw` x `ph` raster where each pixel tests every edge.
pub(crate) fn edge_budget(pw: u32, ph: u32) -> usize {
let pixels = u64::from(pw).saturating_mul(u64::from(ph)).max(1);
usize::try_from(MAX_EDGE_WORK / pixels).unwrap_or(usize::MAX).max(MIN_POLY_EDGES)
}
/// Colour position (0 at the focus, 1 at the edge) of `p` for path `kind` in `bounds` with the
/// focus rect `focus` (fractions inset from left, top, right, bottom), `outline` being the
/// shape's flattened polygon(s) for `shape`.
@@ -114,21 +124,31 @@ fn shape_position(outline: &[Vec<Point>], c: Point, p: Point) -> f64 {
pub fn polygons(path: &BezPath) -> Vec<Vec<Point>> {
let mut out: Vec<Vec<Point>> = Vec::new();
let mut cur: Vec<Point> = Vec::new();
kurbo::flatten(path.iter(), 0.25, |el| match el {
PathEl::MoveTo(p) => {
if cur.len() > 1 {
out.push(std::mem::take(&mut cur));
}
cur = vec![p];
let mut total = 0usize;
kurbo::flatten(path.iter(), 0.25, |el| {
total += 1;
if total > MAX_POINTS {
return;
}
PathEl::LineTo(p) => cur.push(p),
PathEl::ClosePath => {
if let Some(f) = cur.first().copied() {
cur.push(f);
match el {
PathEl::MoveTo(p) => {
if cur.len() > 1 {
out.push(std::mem::take(&mut cur));
}
cur = vec![p];
}
PathEl::LineTo(p) => cur.push(p),
PathEl::ClosePath => {
if let Some(f) = cur.first().copied() {
cur.push(f);
}
}
_ => {}
}
_ => {}
});
if total > MAX_POINTS {
return Vec::new();
}
if cur.len() > 1 {
out.push(cur);
}
@@ -146,7 +166,7 @@ pub fn polygons(path: &BezPath) -> Vec<Vec<Point>> {
/// `outline` cut down to at most about `max_edges` edges in total: first by dropping points
/// closer than `tol` to the last kept one (detail finer than a raster pixel), then, if still too
/// many, by keeping every k-th point. Each polygon stays closed. Returns it unchanged when it fits.
fn simplify(outline: Vec<Vec<Point>>, tol: f64, max_edges: usize) -> Vec<Vec<Point>> {
pub(crate) fn simplify(outline: Vec<Vec<Point>>, tol: f64, max_edges: usize) -> Vec<Vec<Point>> {
let edges = |o: &[Vec<Point>]| o.iter().map(|p| p.len().saturating_sub(1)).sum::<usize>();
if edges(&outline) <= max_edges {
return outline;
@@ -233,8 +253,7 @@ pub fn fill(ctx: &mut RenderContext, kind: &str, focus: [f64; 4], stops: &[(f64,
let s = (MAX_SIDE / longest).min(1.0) * k;
let (pw, ph) = (((bounds.width() * s).ceil() as u32).clamp(1, 4096), ((bounds.height() * s).ceil() as u32).clamp(1, 4096));
let outline = if kind == "shape" {
let pixels = u64::from(pw).saturating_mul(u64::from(ph)).max(1);
let max_edges = usize::try_from(MAX_EDGE_WORK / pixels).unwrap_or(usize::MAX).max(MIN_POLY_EDGES);
let max_edges = edge_budget(pw, ph);
let pixel = (bounds.width() / f64::from(pw)).min(bounds.height() / f64::from(ph));
simplify(polygons(path), pixel, max_edges)
} else {
+3 -37
View File
@@ -112,21 +112,6 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
}
}
/// Slide index -> the first section starting there, built once instead of searched per slide.
fn section_starts(doc: &deckcraft_model::Presentation) -> std::collections::HashMap<usize, usize> {
let mut index_of = std::collections::HashMap::new();
for (i, s) in doc.slides.iter().enumerate() {
index_of.entry(s.id).or_insert(i);
}
let mut starts = std::collections::HashMap::new();
for (si, s) in doc.sections.iter().enumerate() {
if let Some(&i) = s.slides.first().and_then(|id| index_of.get(id)) {
starts.entry(i).or_insert(si);
}
}
starts
}
/// Render the scrollable thumbnail list, scrolling the current slide into view when it changes.
fn list(app: &mut SlideApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
@@ -150,10 +135,11 @@ fn list(app: &mut SlideApp, ui: &mut Ui) {
let ppp = ui.ctx().pixels_per_point();
let mut budget = 3u32;
let mut rects = vec![];
let section_starts = section_starts(&doc);
let section_starts: Vec<(usize, usize)> =
doc.sections.iter().enumerate().filter_map(|(si, s)| s.slides.first().and_then(|id| doc.slide_index(*id)).map(|i| (i, si))).collect();
let mut need_more = false;
for i in 0..doc.slides.len() {
if let Some(si) = section_starts.get(&i)
if let Some((_, si)) = section_starts.iter().find(|(start, _)| *start == i)
&& let Some(sec) = doc.sections.get(*si)
{
ui.add_space(4.0);
@@ -448,26 +434,6 @@ mod tests {
h
}
#[test]
fn section_starts_map_each_first_slide_to_its_first_section() {
let mut s = Session::with_new();
for _ in 0..3 {
s.execute("slide.new", &json!({})).unwrap();
}
let mut doc = (*s.active().unwrap().doc).clone();
let ids: Vec<_> = doc.slides.iter().map(|sl| sl.id).collect();
doc.sections = vec![
deckcraft_model::Section { name: "a".into(), slides: vec![ids[0], ids[1]] },
deckcraft_model::Section { name: "dup".into(), slides: vec![ids[0]] },
deckcraft_model::Section { name: "c".into(), slides: vec![ids[2]] },
deckcraft_model::Section { name: "empty".into(), slides: vec![] },
];
let starts = section_starts(&doc);
assert_eq!(starts.get(&0), Some(&0), "the first section starting at a slide wins");
assert_eq!(starts.get(&2), Some(&2));
assert_eq!(starts.len(), 2);
}
/// Vertical scroll offset of the thumbnail pane, as recorded each frame by `show`.
fn thumbs_scroll(h: &Harness<'_, SlideApp>) -> f32 {
h.state().ui.thumbs_scroll