diff --git a/crates/render/src/chart.rs b/crates/render/src/chart.rs index d13f65223..53794987a 100644 --- a/crates/render/src/chart.rs +++ b/crates/render/src/chart.rs @@ -1,5 +1,7 @@ //! Charts: title, plot area with axes and gridlines, series marks, legend. +use std::collections::HashMap; + use deckcraft_color::{ColorTransform, Rgba, SchemeSlot}; use deckcraft_model::Shape; use deckcraft_model::chart::{Chart, ChartType, TrendlineType, axis_scale}; @@ -85,9 +87,9 @@ fn bar_outline(ctx: &Ctx, s: &deckcraft_model::chart::Series) -> Option<(Rgba, f /// Paint of one bar: a `` fill wins, and a single-series chart that varies colours by point uses one accent per point /// (not waterfalls, which are never read from a ``). Pattern fills draw as patterns and /// `a:noFill` leaves the bar empty, as in PowerPoint. -fn bar_paint(ctx: &Ctx, c: &Chart, si: usize, pi: usize, s: &deckcraft_model::chart::Series) -> Paint { - if let Some((_, f)) = s.point_fills.iter().find(|(k, _)| *k == pi) { - return paint_of(ctx, Some(f), series_fill(ctx, c, si, Some(s))); +fn bar_paint(ctx: &Ctx, c: &Chart, si: usize, pi: usize, s: &deckcraft_model::chart::Series, fills: Option<&HashMap>) -> Paint { + if let Some(f) = fills.and_then(|m| m.get(&pi)) { + return paint_of(ctx, Some(*f), series_fill(ctx, c, si, Some(s))); } if c.vary_colors && c.series.len() == 1 && s.fill.is_none() && c.kind != ChartType::Waterfall { return Paint::Color(series_color(ctx, c, pi)); @@ -883,6 +885,8 @@ fn draw_lines(ctx: &mut RenderContext, draw: &mut LineDrawing<'_, '_>) { let col = if area_kind { series_fill(rctx, c, si, Some(s)) } else { series_line(rctx, c, si, s) }; let series_stats = s.error_bars.as_ref().map(|_| deckcraft_model::chart::SeriesStats::of(&s.values)); let mut pts = vec![]; + // Whether the series has any x value, found once rather than per point. + let any_x = s.x_values.iter().flatten().next().is_some(); for i in 0..ncat { let Some(v) = s.values.get(i).copied().flatten() else { continue }; let v = if stacked { @@ -896,11 +900,11 @@ fn draw_lines(ctx: &mut RenderContext, draw: &mut LineDrawing<'_, '_>) { }; let x = match kind { ChartType::Scatter | ChartType::Bubble => { - match (s.x_values.get(i).copied().flatten().and_then(to_x), s.x_values.iter().flatten().next()) { + match (s.x_values.get(i).copied().flatten().and_then(to_x), any_x) { (Some(x), _) => x, // No x values at all: the points are numbered 1, 2, 3... - (None, None) => to_x(i as f64 + 1.0).unwrap_or(plot.x0 + band * (slot(i) + 0.5)), - (None, Some(_)) => continue, + (None, false) => to_x(i as f64 + 1.0).unwrap_or(plot.x0 + band * (slot(i) + 0.5)), + (None, true) => continue, } } _ => plot.x0 + band * (slot(i) + 0.5), @@ -970,6 +974,19 @@ fn draw_bars(ctx: &mut RenderContext, draw: &BarDrawing<'_, '_>) -> Vec<(f64, f6 let group = bar + step * (n - 1.0); // Error bars need each series' mean and spread: worked out once, not per point. let stats: Vec = bars.iter().map(|(_, s)| deckcraft_model::chart::SeriesStats::of(&s.values)).collect(); + // Each series' per-point fills by point (the first for a point wins), not searched per bar. + let point_fills: Vec> = bars + .iter() + .map(|(_, s)| { + let mut m = HashMap::new(); + for (k, f) in &s.point_fills { + m.entry(*k).or_insert(f); + } + m + }) + .collect(); + // A waterfall's running bars, worked out once for all its categories. + let waterfall_bars = if waterfall { c.waterfall_bars() } else { Vec::new() }; for i in 0..ncat { let mut pos_acc = 0.0; let mut neg_acc = 0.0; @@ -983,7 +1000,7 @@ fn draw_bars(ctx: &mut RenderContext, draw: &BarDrawing<'_, '_>) -> Vec<(f64, f6 let v = s.values.get(i).copied().flatten().unwrap_or(0.0) / total; let start = band * slot(i) + (band - group) / 2.0 + if stacked { 0.0 } else { step * bi as f64 }; let (a, b) = if waterfall { - c.waterfall_bars().get(i).copied().unwrap_or_default() + waterfall_bars.get(i).copied().unwrap_or_default() } else if stacked { if v >= 0.0 { let r = (pos_acc, pos_acc + v); @@ -1015,7 +1032,7 @@ fn draw_bars(ctx: &mut RenderContext, draw: &BarDrawing<'_, '_>) -> Vec<(f64, f6 }; Paint::Color(series_color(rctx, c, slot)) } else { - bar_paint(rctx, c, *si, i, s) + bar_paint(rctx, c, *si, i, s, point_fills.get(bi)) }; fill_with(ctx, &paint, r.abs(), m); if !waterfall && let Some((oc, ow)) = bar_outline(rctx, s).filter(|o| o.1 > 0.0) { diff --git a/crates/render/src/placed.rs b/crates/render/src/placed.rs index 4dc9e1082..2af8152be 100644 --- a/crates/render/src/placed.rs +++ b/crates/render/src/placed.rs @@ -131,9 +131,11 @@ fn place(out: &mut Placed, rctx: &Ctx, fields: &SlideFields, s: &Shape, parent: } ShapeKind::Table(t) => { // Cell text, laid out exactly as the table draws it. + // The table's style is looked up once, not once per cell. + let def = table::resolve_style(rctx, t); for (r, c, rect) in table::cell_rects(t, &table::row_heights(rctx, t, fields)) { let Some(cell) = t.cell(r, c).filter(|cell| !cell.text.is_empty()) else { continue }; - let body = table::cell_body(rctx, t, r, c, cell); + let body = table::cell_body_with_def(rctx, def, t, r, c, cell); let tmp = Shape::table_cell(body.clone()); place_text(out, rctx, fields, s.id, &tmp, &body, rect, m); } diff --git a/crates/render/src/renderer.rs b/crates/render/src/renderer.rs index e7cd5ff35..0b66b30e6 100644 --- a/crates/render/src/renderer.rs +++ b/crates/render/src/renderer.rs @@ -33,6 +33,10 @@ pub struct Renderer { /// Visits spent on outer-shadow silhouette passes, kept apart so they cannot use up the budget /// of the normal drawing that follows. shadow_work: usize, + /// `RenderOpts::skip` as a set, with the slice (address, length) it was built from: the + /// slice can be as long as the slide, and is tested for every shape drawn. Forgotten when a + /// frame starts, so another frame's slice at the same address is never mistaken for it. + skip: Option<(usize, usize, std::collections::HashSet)>, } impl Default for Renderer { @@ -48,6 +52,7 @@ impl Default for Renderer { in_shadow_pass: false, render_work: 0, shadow_work: 0, + skip: None, } } } @@ -163,6 +168,7 @@ impl Renderer { pub(crate) fn slide(&mut self, pres: &Presentation, slide: &Slide, index: usize, opts: &RenderOpts) -> Image { self.render_work = 0; self.shadow_work = 0; + self.skip = None; self.in_shadow_pass = false; let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, slide)); let f = Frame { pres, opts, fields: Self::fields(pres, index, opts) }; @@ -178,6 +184,7 @@ impl Renderer { pub(crate) fn slide_part(&mut self, pres: &Presentation, slide: &Slide, index: usize, part: SlidePart, opts: &RenderOpts) -> Image { self.render_work = 0; self.shadow_work = 0; + self.skip = None; self.in_shadow_pass = false; let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, slide)); let f = Frame { pres, opts, fields: Self::fields(pres, index, opts) }; @@ -212,6 +219,7 @@ impl Renderer { let mix = if mix.is_finite() { mix.clamp(0.0, 1.0) } else { 1.0 }; self.render_work = 0; self.shadow_work = 0; + self.skip = None; self.in_shadow_pass = false; let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, old.0) || needs_filters(pres, new.0)); let (Some(co), Some(cn)) = (Ctx::for_slide(pres, old.0), Ctx::for_slide(pres, new.0)) else { return self.finish(ctx, w, h) }; @@ -260,6 +268,7 @@ impl Renderer { pub(crate) fn layout(&mut self, pres: &Presentation, mi: usize, li: Option, opts: &RenderOpts) -> Image { self.render_work = 0; self.shadow_work = 0; + self.skip = None; self.in_shadow_pass = false; let (w, h) = output_size(pres, opts); let mut ctx = RenderContext::new_with(w, h, vello_cpu::RenderSettings { num_threads: 0, ..Default::default() }); @@ -393,8 +402,25 @@ impl Renderer { geo } + /// Whether `id` is in `skip`, looked up in a set built once per frame and slice. + fn skipped(&mut self, skip: &[ShapeId], id: ShapeId) -> bool { + if skip.is_empty() { + return false; + } + let key = (skip.as_ptr() as usize, skip.len()); + match &self.skip { + Some((p, n, set)) if (*p, *n) == key => set.contains(&id), + _ => { + let set: std::collections::HashSet = skip.iter().copied().collect(); + let hit = set.contains(&id); + self.skip = Some((key.0, key.1, set)); + hit + } + } + } + fn shape_at(&mut self, ctx: &mut RenderContext, f: &Frame, rctx: &Ctx, s: &Shape, parent: Affine, depth: usize) { - if depth > 32 || (s.hidden && !f.opts.show_hidden) || f.opts.skip.contains(&s.id) { + if depth > 32 || (s.hidden && !f.opts.show_hidden) || self.skipped(f.opts.skip, s.id) { return; } let work = if self.in_shadow_pass { &mut self.shadow_work } else { &mut self.render_work }; diff --git a/crates/render/src/stroke.rs b/crates/render/src/stroke.rs index 8310cf0c9..fc1403f6b 100644 --- a/crates/render/src/stroke.rs +++ b/crates/render/src/stroke.rs @@ -191,7 +191,8 @@ pub(crate) fn limit_dash_count(path: &BezPath, pat: Vec) -> Vec { return pat; } let len: f64 = path.segments().map(|s| s.arclen(1e-2)).sum(); - let dashes = len / period; + // Every dash of the pattern counts, not each repeat of it: a custom pattern can hold many. + let dashes = len / period * pat.len().div_ceil(2) as f64; if !len.is_finite() { return vec![]; } diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index fb25c7f0b..2da29cb37 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -990,4 +990,37 @@ fn a_tiny_dash_on_a_huge_shape_draws_without_running_out_of_memory() { p.line_to((4.0e6, 0.0)); let pat = limit_dash_count(&p, vec![1.0, 1.0]); assert!((4.0e6 / (pat[0] + pat[1]) - MAX_DASHES).abs() < 1.0, "{pat:?}"); + // A custom pattern of 1,000 dashes counts each of them: the limit is on dashes drawn. + let pat = limit_dash_count(&p, vec![1.0; 2000]); + let period: f64 = pat.iter().sum(); + assert!((4.0e6 / period * 1000.0 - MAX_DASHES).abs() < 1.0, "{}", period); +} + +/// A bar chart with 20,000 points, each with its own fill, and a sparse scatter of the same size: +/// the per-point fills and the scatter's "has any x" are looked up, not searched per point. +#[test] +fn many_point_fills_and_sparse_scatter_x_render() { + use deckcraft_model::ShapeKind; + use deckcraft_model::chart::{Chart, ChartType}; + use deckcraft_model::style::Fill; + let n = 20_000; + let chart = |kind: ChartType| { + let mut c = Chart::sample(kind); + c.series.truncate(1); + c.categories = (0..n).map(|i| i.to_string()).collect(); + c.series[0].values = (0..n).map(|i| Some(i as f64)).collect(); + c + }; + let mut bars = chart(ChartType::Column); + bars.series[0].point_fills = (0..n).rev().map(|i| (i, Fill::solid(ColorRef::rgb(Rgba::rgb(200, 0, 0))))).collect(); + let mut scatter = chart(ChartType::Scatter); + scatter.series[0].x_values = (0..n).map(|i| (i + 1 == n).then_some(1.0)).collect(); + let shape = |id: u32, c: Chart| Shape { + id: ShapeId(id), + xfrm: Some(Xfrm::new(10.0, 10.0, 400.0, 300.0)), + kind: ShapeKind::Chart(Box::new(c)), + ..Default::default() + }; + let img = render(vec![shape(2, bars), shape(3, scatter)]); + assert!(img.width > 0); }