diff --git a/examples/dxf_viewer/src/main.rs b/examples/dxf_viewer/src/main.rs index 1eed23b..43ecd34 100644 --- a/examples/dxf_viewer/src/main.rs +++ b/examples/dxf_viewer/src/main.rs @@ -67,7 +67,7 @@ use vello::wgpu; use tabulon_dxf::{EntityHandle, RestrokePaint, TDDrawing}; use tabulon::{ - GraphicsBag, GraphicsItem, ItemHandle, PaintHandle, + DirectIsometry, GraphicsBag, GraphicsItem, ItemHandle, PaintHandle, render_layer::RenderLayer, shape::{FatPaint, FatShape}, }; @@ -132,6 +132,9 @@ struct DrawingViewer { /// State of gesture processing (e.g. panning, zooming). gestures: GestureState, + + /// Cache of precomputed text layouts. + layout_cache: tabulon_vello::LayoutCache, } struct TabulonDxfViewer<'s> { @@ -221,9 +224,14 @@ impl ApplicationHandler for TabulonDxfViewer<'_> { let picking_index = EntityIndex::new(&drawing); let bounds = picking_index.bounds(); - let text_cull_index = TextCullIndex::new(&mut self.tv_environment, &drawing); + let (layout_cache, measurements) = + self.tv_environment.compute_text_layouts_and_measures( + &drawing.graphics, + &drawing.render_layer, + ); + + let text_cull_index = TextCullIndex::new(&measurements); - let mut scene = Scene::default(); let view_scale = (size.height as f64 / bounds.size().height) .min(size.width as f64 / bounds.size().width); @@ -243,9 +251,10 @@ impl ApplicationHandler for TabulonDxfViewer<'_> { let encode_started = Instant::now(); self.tv_environment.add_render_layer_to_scene( - &mut scene, + &mut self.scene, &drawing.graphics, &drawing.render_layer, + Some(&layout_cache), ); let encode_duration = Instant::now().saturating_duration_since(encode_started); eprintln!("Initial projection/encode took {encode_duration:?}"); @@ -259,6 +268,7 @@ impl ApplicationHandler for TabulonDxfViewer<'_> { gestures: GestureState::default(), defer_reprojection: true, pick: None, + layout_cache, }); } Err(e) => { @@ -604,7 +614,11 @@ impl ApplicationHandler for TabulonDxfViewer<'_> { let picking_index = EntityIndex::new(&drawing); let bounds = picking_index.bounds(); - let text_cull_index = TextCullIndex::new(&mut self.tv_environment, &drawing); + let (layout_cache, measurements) = self + .tv_environment + .compute_text_layouts_and_measures(&drawing.graphics, &drawing.render_layer); + + let text_cull_index = TextCullIndex::new(&measurements); let view_scale = (surface.config.height as f64 / bounds.size().height) .min(surface.config.width as f64 / bounds.size().width); @@ -624,6 +638,7 @@ impl ApplicationHandler for TabulonDxfViewer<'_> { pick: None, gestures: GestureState::default(), defer_reprojection: false, + layout_cache, }); reproject = true; @@ -760,6 +775,7 @@ impl ApplicationHandler for TabulonDxfViewer<'_> { &mut self.scene, &viewer.td.graphics, &culled_render_layer, + Some(&viewer.layout_cache), ); if let Some(pick) = viewer.pick { @@ -796,7 +812,7 @@ impl ApplicationHandler for TabulonDxfViewer<'_> { }); self.tv_environment - .add_render_layer_to_scene(&mut self.scene, &gb, &rl); + .add_render_layer_to_scene(&mut self.scene, &gb, &rl, None); } let reproject_duration = @@ -1102,15 +1118,14 @@ struct TextCullIndex { reason = "The loss of range and precision is acceptable." )] impl TextCullIndex { - fn new(tv_env: &mut tabulon_vello::Environment, d: &TDDrawing) -> Self { - let measurements = tv_env.measure_text_items(&d.graphics, &d.render_layer); + fn new(measurements: &BTreeMap) -> Self { let mut builder = StaticAABB2DIndexBuilder::::new(measurements.len()); let mut item_mapping = vec![]; for (ih, (di, s)) in measurements { - item_mapping.push(ih); - let bbox = (Affine::from(di) - * Rect::from_origin_size(Point::ZERO, s).to_path(DEFAULT_ACCURACY)) + item_mapping.push(*ih); + let bbox = (Affine::from(*di) + * Rect::from_origin_size(Point::ZERO, *s).to_path(DEFAULT_ACCURACY)) .bounding_box(); builder.add( bbox.min_x() as f32, diff --git a/examples/vello_simple/src/main.rs b/examples/vello_simple/src/main.rs index 0957456..3af8bb7 100644 --- a/examples/vello_simple/src/main.rs +++ b/examples/vello_simple/src/main.rs @@ -281,5 +281,5 @@ fn add_shapes_to_scene(tv_environment: &mut tabulon_vello::Environment, scene: & }, ); - tv_environment.add_render_layer_to_scene(scene, &gb, &rl); + tv_environment.add_render_layer_to_scene(scene, &gb, &rl, None); } diff --git a/tabulon_vello/src/lib.rs b/tabulon_vello/src/lib.rs index a606fc5..3ff6f15 100644 --- a/tabulon_vello/src/lib.rs +++ b/tabulon_vello/src/lib.rs @@ -6,7 +6,7 @@ use tabulon::{ DirectIsometry, GraphicsBag, GraphicsItem, ItemHandle, peniko::{ - Color, Fill, + Brush, Color, Fill, kurbo::{Affine, Size, Vec2}, }, render_layer::RenderLayer, @@ -14,7 +14,7 @@ use tabulon::{ text::{AttachmentPoint, FatText}, }; -use parley::{FontContext, LayoutContext, PositionedLayoutItem}; +use parley::{FontContext, Layout, LayoutContext, PositionedLayoutItem}; use vello::{Scene, peniko::Fill::NonZero}; extern crate alloc; @@ -35,6 +35,9 @@ pub struct Environment { pub(crate) layout_cx: LayoutContext>, } +/// Convenience type for layout caches. +pub type LayoutCache = BTreeMap>>; + impl Environment { /// Add a [`RenderLayer`] to a Vello [`Scene`]. #[tracing::instrument(skip_all)] @@ -43,8 +46,10 @@ impl Environment { scene: &mut Scene, graphics: &GraphicsBag, render_layer: &RenderLayer, + layout_cache: Option<&LayoutCache>, ) { - let Self { font_cx, layout_cx } = self; + let font_cx = &mut self.font_cx; + let layout_cx = &mut self.layout_cx; for idx in &render_layer.indices { match graphics.get(*idx) { @@ -79,21 +84,6 @@ impl Environment { }) => { let transform = graphics.get_transform(*transform); - let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, false); - for prop in style.inner().values() { - builder.push_default(prop.to_owned()); - } - let mut layout = builder.build(text); - layout.break_all_lines(*max_inline_size); - layout.align(*max_inline_size, *alignment, Default::default()); - let layout_size = Size { - width: max_inline_size.unwrap_or(layout.width()) as f64, - height: layout.height() as f64, - }; - - let placement_transform = Affine::from(*insertion) - * Affine::translate(-attachment_point.select(layout_size)); - let FatPaint { fill_paint: Some(fill_paint), .. @@ -102,58 +92,99 @@ impl Environment { continue; }; - for line in layout.lines() { - for item in line.items() { - let PositionedLayoutItem::GlyphRun(glyph_run) = item else { - continue; - }; - - let mut x = glyph_run.offset(); - let y = glyph_run.baseline(); - let run = glyph_run.run(); - - // Vello has a hard time drawing glyphs either very large or very - // small, so we render at 1000 units regardless, and then transform. - let fudge = run.font_size() as f64 / 1000.0; - - let synthesis = run.synthesis(); - scene - .draw_glyphs(run.font()) - // TODO: Color will come from styled text. - .brush(fill_paint) - .hint(false) - .transform(transform * placement_transform) - .glyph_transform(Some(if let Some(angle) = synthesis.skew() { - Affine::scale(fudge) - * Affine::skew(angle.to_radians().tan() as f64, 0.0) - } else { - Affine::scale(fudge) - })) - // Small font sizes are quantized, multiplying by - // 50 and then scaling by 1 / 50 at the glyph level - // works around this, but it is a hack. - .font_size(1000_f32) - .normalized_coords(run.normalized_coords()) - .draw( - Fill::NonZero, - glyph_run.glyphs().map(|g| { - let gx = x + g.x; - let gy = y - g.y; - x += g.advance; - vello::Glyph { - id: g.id, - x: gx, - y: gy, - } - }), - ); + if let Some(l) = layout_cache.and_then(|cache| cache.get(idx)) { + let layout_size = Size { + width: max_inline_size.unwrap_or(l.width()) as f64, + height: l.height() as f64, + }; + + let placement_transform = Affine::from(*insertion) + * Affine::translate(-attachment_point.select(layout_size)); + + draw_layout(scene, transform * placement_transform, l, fill_paint); + } else { + let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, false); + for prop in style.inner().values() { + builder.push_default(prop.to_owned()); } - } + let mut l = builder.build(text); + l.break_all_lines(*max_inline_size); + l.align(*max_inline_size, *alignment, Default::default()); + + let layout_size = Size { + width: max_inline_size.unwrap_or(l.width()) as f64, + height: l.height() as f64, + }; + + let placement_transform = Affine::from(*insertion) + * Affine::translate(-attachment_point.select(layout_size)); + + draw_layout(scene, transform * placement_transform, &l, fill_paint); + }; } } } } + /// Compute layouts and measures for text items in a [`RenderLayer`]. + #[tracing::instrument(skip_all)] + pub fn compute_text_layouts_and_measures( + &mut self, + graphics: &GraphicsBag, + render_layer: &RenderLayer, + ) -> (LayoutCache, BTreeMap) { + let mut layouts = BTreeMap::new(); + let mut measures = BTreeMap::new(); + + let font_cx = &mut self.font_cx; + let layout_cx = &mut self.layout_cx; + + for idx in &render_layer.indices { + let GraphicsItem::FatText(FatText { + text, + style, + max_inline_size, + alignment, + insertion, + attachment_point, + .. + }) = graphics.get(*idx) + else { + continue; + }; + + let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, false); + for prop in style.inner().values() { + builder.push_default(prop.to_owned()); + } + let mut layout = builder.build(text); + layout.break_all_lines(*max_inline_size); + layout.align(*max_inline_size, *alignment, Default::default()); + + let layout_size = Size { + width: max_inline_size.unwrap_or(layout.width()) as f64, + height: layout.height() as f64, + }; + + let rotated_offset = rotate_offset(*attachment_point, layout_size, insertion.angle); + + measures.insert( + *idx, + ( + DirectIsometry { + displacement: insertion.displacement - rotated_offset, + ..*insertion + }, + layout_size, + ), + ); + + layouts.insert(*idx, layout); + } + + (layouts, measures) + } + /// Measure text items in a [`RenderLayer`]. #[tracing::instrument(skip_all)] pub fn measure_text_items( @@ -209,6 +240,61 @@ impl Environment { } } +/// Draw a layout to a scene. +fn draw_layout( + scene: &mut Scene, + transform: Affine, + layout: &Layout>, + fill_paint: &Brush, +) { + for line in layout.lines() { + for item in line.items() { + let PositionedLayoutItem::GlyphRun(glyph_run) = item else { + continue; + }; + + let mut x = glyph_run.offset(); + let y = glyph_run.baseline(); + let run = glyph_run.run(); + + // Vello has a hard time drawing glyphs either very large or very + // small, so we render at 1000 units regardless, and then transform. + let fudge = run.font_size() as f64 / 1000.0; + + let synthesis = run.synthesis(); + scene + .draw_glyphs(run.font()) + // TODO: Color will come from styled text. + .brush(fill_paint) + .hint(false) + .transform(transform) + .glyph_transform(Some(if let Some(angle) = synthesis.skew() { + Affine::scale(fudge) * Affine::skew(angle.to_radians().tan() as f64, 0.0) + } else { + Affine::scale(fudge) + })) + // Small font sizes are quantized, multiplying by + // 50 and then scaling by 1 / 50 at the glyph level + // works around this, but it is a hack. + .font_size(1000_f32) + .normalized_coords(run.normalized_coords()) + .draw( + Fill::NonZero, + glyph_run.glyphs().map(|g| { + let gx = x + g.x; + let gy = y - g.y; + x += g.advance; + vello::Glyph { + id: g.id, + x: gx, + y: gy, + } + }), + ); + } + } +} + /// Calculate a top left equivalent insertion point for a layout size and attachment point. fn rotate_offset(attachment_point: AttachmentPoint, layout_size: Size, angle: f64) -> Vec2 { let attachment = attachment_point.select(layout_size);