Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions examples/dxf_viewer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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);

Expand All @@ -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:?}");
Expand All @@ -259,6 +268,7 @@ impl ApplicationHandler for TabulonDxfViewer<'_> {
gestures: GestureState::default(),
defer_reprojection: true,
pick: None,
layout_cache,
});
}
Err(e) => {
Expand Down Expand Up @@ -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);
Expand All @@ -624,6 +638,7 @@ impl ApplicationHandler for TabulonDxfViewer<'_> {
pick: None,
gestures: GestureState::default(),
defer_reprojection: false,
layout_cache,
});

reproject = true;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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<ItemHandle, (DirectIsometry, Size)>) -> Self {
let mut builder = StaticAABB2DIndexBuilder::<f32>::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,
Expand Down
2 changes: 1 addition & 1 deletion examples/vello_simple/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
214 changes: 150 additions & 64 deletions tabulon_vello/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
use tabulon::{
DirectIsometry, GraphicsBag, GraphicsItem, ItemHandle,
peniko::{
Color, Fill,
Brush, Color, Fill,
kurbo::{Affine, Size, Vec2},
},
render_layer::RenderLayer,
shape::{FatPaint, FatShape},
text::{AttachmentPoint, FatText},
};

use parley::{FontContext, LayoutContext, PositionedLayoutItem};
use parley::{FontContext, Layout, LayoutContext, PositionedLayoutItem};
use vello::{Scene, peniko::Fill::NonZero};

extern crate alloc;
Expand All @@ -35,6 +35,9 @@ pub struct Environment {
pub(crate) layout_cx: LayoutContext<Option<Color>>,
}

/// Convenience type for layout caches.
pub type LayoutCache = BTreeMap<ItemHandle, Layout<Option<Color>>>;

impl Environment {
/// Add a [`RenderLayer`] to a Vello [`Scene`].
#[tracing::instrument(skip_all)]
Expand 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) {
Expand Down Expand Up @@ -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),
..
Expand All @@ -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<ItemHandle, (DirectIsometry, Size)>) {
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(
Expand Down Expand Up @@ -209,6 +240,61 @@ impl Environment {
}
}

/// Draw a layout to a scene.
fn draw_layout(
scene: &mut Scene,
transform: Affine,
layout: &Layout<Option<Color>>,
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);
Expand Down