M5: cache text glyph geometry per node (Arc identity)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
515653f556
commit
a2fd35cd32
|
|
@ -37,6 +37,14 @@ fn main() {
|
||||||
}
|
}
|
||||||
d.insert(Some(l), usize::MAX, node).unwrap();
|
d.insert(Some(l), usize::MAX, node).unwrap();
|
||||||
}
|
}
|
||||||
|
// Text objects (1 per 20 shapes).
|
||||||
|
for i in 0..n / 20 {
|
||||||
|
let x = rng(&mut seed) * 1500.0;
|
||||||
|
let y = rng(&mut seed) * 1150.0 + 20.0;
|
||||||
|
let id = d.alloc_id();
|
||||||
|
let t = drawcraft_doc::TextObject::point(Point::new(x, y), &format!("Label {i} — DrawCraft"), drawcraft_doc::CharStyle { size: 10.0 + (i % 5) as f64 * 4.0, ..Default::default() });
|
||||||
|
d.insert(Some(l), usize::MAX, Node::new(id, drawcraft_doc::NodeKind::Text(Box::new(t)))).unwrap();
|
||||||
|
}
|
||||||
let mut r = Renderer::new();
|
let mut r = Renderer::new();
|
||||||
let opts = RenderOptions::default();
|
let opts = RenderOptions::default();
|
||||||
for (label, w, h, view) in [
|
for (label, w, h, view) in [
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ struct GeomEntry {
|
||||||
|
|
||||||
/// Reusable renderer (keeps the render context, decoded images and glyph caches between frames).
|
/// Reusable renderer (keeps the render context, decoded images and glyph caches between frames).
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
|
texts: HashMap<usize, (Arc<Node>, Arc<TextGeom>)>,
|
||||||
/// Single-threaded context used when the document has raster filters (vello's filters require it).
|
/// Single-threaded context used when the document has raster filters (vello's filters require it).
|
||||||
ctx_st: Option<RenderContext>,
|
ctx_st: Option<RenderContext>,
|
||||||
/// Worker threads for the multithreaded rasterizer (0 = single-threaded).
|
/// Worker threads for the multithreaded rasterizer (0 = single-threaded).
|
||||||
|
|
@ -136,6 +137,7 @@ struct Frame<'a> {
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
texts: HashMap::new(),
|
||||||
ctx_st: None,
|
ctx_st: None,
|
||||||
threads: default_threads(),
|
threads: default_threads(),
|
||||||
geom: HashMap::new(),
|
geom: HashMap::new(),
|
||||||
|
|
@ -308,6 +310,16 @@ impl Renderer {
|
||||||
self.stats.drawn += 1;
|
self.stats.drawn += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if let NodeKind::Text(t) = &a.kind
|
||||||
|
&& a.opacity >= 1.0
|
||||||
|
&& a.blend == drawcraft_color::BlendMode::Normal
|
||||||
|
&& !fx::has_fx(a)
|
||||||
|
{
|
||||||
|
let g = self.text_geom_of(a, t);
|
||||||
|
self.draw_text_geom(ctx, f, a, t, &g);
|
||||||
|
self.stats.drawn += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.draw_node(ctx, f, a, true);
|
self.draw_node(ctx, f, a, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -525,44 +537,57 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_text(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject) {
|
fn draw_text(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject) {
|
||||||
let layout = drawcraft_text::layout(drawcraft_text::FontDb::global(), t);
|
let g = text_geom(t);
|
||||||
|
self.draw_text_geom(ctx, f, n, t, &g);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cached glyph geometry for a text node (keyed by Arc identity like paths).
|
||||||
|
fn text_geom_of(&mut self, a: &Arc<Node>, t: &TextObject) -> Arc<TextGeom> {
|
||||||
|
let key = Arc::as_ptr(a) as usize;
|
||||||
|
if let Some((node, g)) = self.texts.get(&key)
|
||||||
|
&& Arc::ptr_eq(node, a)
|
||||||
|
{
|
||||||
|
return g.clone();
|
||||||
|
}
|
||||||
|
let g = Arc::new(text_geom(t));
|
||||||
|
if self.texts.len() > 4096 {
|
||||||
|
self.texts.clear();
|
||||||
|
}
|
||||||
|
self.texts.insert(key, (a.clone(), g.clone()));
|
||||||
|
g
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_text_geom(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject, g: &TextGeom) {
|
||||||
let xf = f.view * t.xf;
|
let xf = f.view * t.xf;
|
||||||
if f.opts.outline {
|
if f.opts.outline {
|
||||||
for g in &layout.glyphs {
|
let mut p = g.all.clone();
|
||||||
let mut p = g.outline.clone();
|
|
||||||
p.apply_affine(xf);
|
p.apply_affine(xf);
|
||||||
ctx.set_transform(Affine::IDENTITY);
|
ctx.set_transform(Affine::IDENTITY);
|
||||||
ctx.set_stroke(kurbo::Stroke::new(1.0));
|
ctx.set_stroke(kurbo::Stroke::new(1.0));
|
||||||
ctx.set_paint(peniko::Color::BLACK);
|
ctx.set_paint(peniko::Color::BLACK);
|
||||||
ctx.stroke_path(&p);
|
ctx.stroke_path(&p);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let tb = t.xf.transform_rect_bbox(layout.bounds);
|
let tb = t.xf.transform_rect_bbox(g.bounds);
|
||||||
// Object-level appearance fills/strokes apply on top of character fills (like Illustrator).
|
// Object-level appearance fills/strokes apply on top of character fills (like Illustrator).
|
||||||
for (i, run) in t.runs.iter().enumerate() {
|
for (i, run) in t.runs.iter().enumerate() {
|
||||||
let mut path = BezPath::new();
|
let Some(path) = g.runs.get(i) else { continue };
|
||||||
for g in layout.glyphs.iter().filter(|g| g.run == i) {
|
|
||||||
path.extend(g.outline.iter());
|
|
||||||
}
|
|
||||||
if path.elements().is_empty() {
|
if path.elements().is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ctx.set_transform(xf);
|
ctx.set_transform(xf);
|
||||||
ctx.set_fill_rule(peniko::Fill::NonZero);
|
ctx.set_fill_rule(peniko::Fill::NonZero);
|
||||||
if paint::set_paint(ctx, &run.style.fill, layout.bounds, f.doc) {
|
if paint::set_paint(ctx, &run.style.fill, g.bounds, f.doc) {
|
||||||
ctx.fill_path(&path);
|
self.fold_alpha(ctx, &run.style.fill);
|
||||||
|
ctx.fill_path(path);
|
||||||
}
|
}
|
||||||
if !run.style.stroke.is_none() && run.style.stroke_width > 0.0 && paint::set_paint(ctx, &run.style.stroke, layout.bounds, f.doc) {
|
if !run.style.stroke.is_none() && run.style.stroke_width > 0.0 && paint::set_paint(ctx, &run.style.stroke, g.bounds, f.doc) {
|
||||||
ctx.set_stroke(kurbo::Stroke::new(run.style.stroke_width));
|
ctx.set_stroke(kurbo::Stroke::new(run.style.stroke_width));
|
||||||
ctx.stroke_path(&path);
|
ctx.stroke_path(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !n.appearance.items.is_empty() {
|
if !n.appearance.items.is_empty() {
|
||||||
let mut all = BezPath::new();
|
let mut all = g.all.clone();
|
||||||
for g in &layout.glyphs {
|
|
||||||
all.extend(g.outline.iter());
|
|
||||||
}
|
|
||||||
all.apply_affine(t.xf);
|
all.apply_affine(t.xf);
|
||||||
for item in &n.appearance.items {
|
for item in &n.appearance.items {
|
||||||
ctx.set_transform(f.view);
|
ctx.set_transform(f.view);
|
||||||
|
|
@ -637,6 +662,26 @@ fn default_threads() -> u16 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Glyph outlines grouped by run, plus the whole text as one path.
|
||||||
|
struct TextGeom {
|
||||||
|
runs: Vec<BezPath>,
|
||||||
|
all: BezPath,
|
||||||
|
bounds: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_geom(t: &TextObject) -> TextGeom {
|
||||||
|
let layout = drawcraft_text::layout(drawcraft_text::FontDb::global(), t);
|
||||||
|
let mut runs = vec![BezPath::new(); t.runs.len()];
|
||||||
|
let mut all = BezPath::new();
|
||||||
|
for g in &layout.glyphs {
|
||||||
|
if let Some(r) = runs.get_mut(g.run) {
|
||||||
|
r.extend(g.outline.iter());
|
||||||
|
}
|
||||||
|
all.extend(g.outline.iter());
|
||||||
|
}
|
||||||
|
TextGeom { runs, all, bounds: layout.bounds }
|
||||||
|
}
|
||||||
|
|
||||||
fn rects_overlap(a: Rect, b: Rect) -> bool {
|
fn rects_overlap(a: Rect, b: Rect) -> bool {
|
||||||
a.x0 <= b.x1 && b.x0 <= a.x1 && a.y0 <= b.y1 && b.y0 <= a.y1
|
a.x0 <= b.x1 && b.x0 <= a.x1 && a.y0 <= b.y1 && b.y0 <= a.y1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue