use eframe::egui::{
Color32, Mesh, Pos2, Rect, Shape, Stroke, Ui, UiBuilder, Vec2, epaint::Vertex, pos2, vec2,
};
use odox_core::draw::Geometry;
use odox_core::{
Anchor, Color, Document, Element, Family, Fill, Gradient, GradientStyle, Length, Ns,
Properties, Transform,
};
use crate::find::Highlights;
use crate::flow::{Flow, Pictures};
use crate::flow_model::PageEditor;
use crate::format::{self, Palette};
pub struct Canvas<'a> {
pub document: &'a Document,
pub pictures: &'a mut Pictures,
pub page: Rect,
pub scale: f32,
pub palette: Palette,
pub page_editor: Option<&'a mut PageEditor>,
pub find: Option<Highlights<'a>>,
pub followed: Option<String>,
at: Option<usize>,
}
#[derive(Clone, Copy)]
struct Placement {
origin: Pos2,
x: Vec2,
y: Vec2,
}
impl Placement {
fn across(self, u: f32, v: f32) -> Pos2 {
self.origin + self.x * u + self.y * v
}
fn corners(self) -> [Pos2; 4] {
[
self.across(0.0, 0.0),
self.across(1.0, 0.0),
self.across(1.0, 1.0),
self.across(0.0, 1.0),
]
}
fn bounds(self) -> Rect {
Rect::from_points(&self.corners())
}
fn is_upright(self) -> bool {
self.x.y.abs() < 0.01 && self.y.x.abs() < 0.01 && self.x.x >= 0.0 && self.y.y >= 0.0
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Filled {
Yes,
No,
}
impl<'a> Canvas<'a> {
pub fn new(
document: &'a Document,
pictures: &'a mut Pictures,
page: Rect,
scale: f32,
palette: Palette,
) -> Self {
Self {
document,
pictures,
page,
scale,
palette,
page_editor: None,
find: None,
followed: None,
at: None,
}
}
pub fn slide_shape(&mut self, ui: &mut Ui, index: usize, shape: &Element) {
self.at = Some(index);
self.shape(ui, shape);
self.at = None;
}
}
impl Canvas<'_> {
pub fn background(&mut self, ui: &Ui, fill: &Fill) {
let page = self.page;
self.fill(
ui,
page,
fill,
None,
&[
page.left_top(),
page.right_top(),
page.right_bottom(),
page.left_bottom(),
],
);
}
pub fn shape(&mut self, ui: &mut Ui, shape: &Element) {
if shape.is(&Ns::Draw, "g") {
for child in shape.elements() {
self.shape(ui, child);
}
return;
}
let properties = self.style_of(shape);
let outline = self.stroke(&properties);
if shape.is(&Ns::Draw, "line") {
let ends = [self.point(shape, "x1", "y1"), self.point(shape, "x2", "y2")];
if let ([Some(from), Some(to)], Some(stroke)) = ([ends[0], ends[1]], outline) {
self.painter(ui)
.add(Shape::line_segment([from, to], stroke));
}
return;
}
if shape.is(&Ns::Draw, "connector") {
self.connector(ui, shape, &properties, outline);
return;
}
let Some(place) = self.placement(shape) else {
return;
};
let rect = place.bounds();
if shape.is(&Ns::Draw, "polygon") || shape.is(&Ns::Draw, "polyline") {
let points = points(shape, place);
if points.len() >= 2 {
if shape.is(&Ns::Draw, "polygon") {
self.fill(
ui,
rect,
&properties.graphic.fill(),
properties.graphic.opacity,
&points,
);
if let Some(stroke) = outline {
self.painter(ui).add(Shape::closed_line(points, stroke));
}
} else if let Some(stroke) = outline {
self.painter(ui).add(Shape::line(points, stroke));
}
}
return;
}
if shape.is(&Ns::Draw, "ellipse") || shape.is(&Ns::Draw, "circle") {
self.ellipse(ui, place, &properties, outline);
self.text(ui, shape, place);
return;
}
if shape.is(&Ns::Draw, "path") {
if let Some(geometry) = Geometry::read_path(shape) {
self.geometry(ui, &geometry, place, &properties, outline, Filled::Yes);
}
self.text(ui, shape, place);
return;
}
if shape.is(&Ns::Draw, "custom-shape") {
if let Some(geometry) = shape
.child(&Ns::Draw, "enhanced-geometry")
.and_then(Geometry::read)
{
self.geometry(ui, &geometry, place, &properties, outline, Filled::Yes);
}
self.text(ui, shape, place);
return;
}
if !shape.is(&Ns::Draw, "rect") && !shape.is(&Ns::Draw, "frame") {
return;
}
let corners = place.corners();
self.fill(
ui,
rect,
&properties.graphic.fill(),
properties.graphic.opacity,
&corners,
);
if let Some(stroke) = outline {
self.painter(ui)
.add(Shape::closed_line(corners.to_vec(), stroke));
}
self.text(ui, shape, place);
}
fn ellipse(
&mut self,
ui: &Ui,
place: Placement,
properties: &Properties,
outline: Option<Stroke>,
) {
let rect = place.bounds();
if place.is_upright() {
let (centre, radius) = (rect.center(), rect.size() / 2.0);
if let Some(colour) = self.flat(&properties.graphic.fill(), properties.graphic.opacity)
{
self.painter(ui)
.add(Shape::ellipse_filled(centre, radius, colour));
}
if let Some(stroke) = outline {
self.painter(ui)
.add(Shape::ellipse_stroke(centre, radius, stroke));
}
} else {
let points = ellipse(place);
self.fill(
ui,
rect,
&properties.graphic.fill(),
properties.graphic.opacity,
&points,
);
if let Some(stroke) = outline {
self.painter(ui).add(Shape::closed_line(points, stroke));
}
}
}
fn connector(
&mut self,
ui: &mut Ui,
shape: &Element,
properties: &Properties,
outline: Option<Stroke>,
) {
let (Some(from), Some(to)) = (self.point(shape, "x1", "y1"), self.point(shape, "x2", "y2"))
else {
return;
};
let rect = Rect::from_two_pos(from, to);
match Geometry::read_path(shape) {
Some(mut geometry) => {
geometry.refit();
let place = Placement {
origin: rect.left_top(),
x: vec2(rect.width(), 0.0),
y: vec2(0.0, rect.height()),
};
self.geometry(ui, &geometry, place, properties, outline, Filled::No);
}
None => {
if let Some(stroke) = outline {
self.painter(ui)
.add(Shape::line_segment([from, to], stroke));
}
}
}
self.text(
ui,
shape,
Placement {
origin: rect.left_top(),
x: vec2(rect.width(), 0.0),
y: vec2(0.0, rect.height()),
},
);
}
fn geometry(
&mut self,
ui: &Ui,
geometry: &Geometry,
placement: Placement,
properties: &Properties,
outline: Option<Stroke>,
filled: Filled,
) {
let view = geometry.view;
let rect = placement.bounds();
let place = |(x, y): (f32, f32)| {
placement.across((x - view.x) / view.width, (y - view.y) / view.height)
};
for stroke in &geometry.paths {
let points: Vec<Pos2> = stroke.points.iter().copied().map(place).collect();
if points.len() < 2 {
continue;
}
if stroke.fill && filled == Filled::Yes {
self.fill(
ui,
rect,
&properties.graphic.fill(),
properties.graphic.opacity,
&points,
);
}
if stroke.stroke
&& let Some(pen) = outline
{
let shape = if stroke.closed {
Shape::closed_line(points, pen)
} else {
Shape::line(points, pen)
};
self.painter(ui).add(shape);
}
}
}
fn text(&mut self, ui: &mut Ui, shape: &Element, place: Placement) {
let picture = shape.child(&Ns::Draw, "image").is_some();
if picture && !place.is_upright() {
self.turned_picture(ui, shape, place);
return;
}
let text_box = shape
.elements_indexed()
.find(|(_, e)| e.is(&Ns::Draw, "text-box"));
let (content, below) = if picture {
(shape.clone(), None)
} else if let Some((index, box_)) = text_box {
(box_.clone(), Some(index))
} else if shape.child(&Ns::Text, "p").is_some() || shape.child(&Ns::Text, "list").is_some()
{
(shape.clone(), None)
} else {
return;
};
let prefix = self.at.map(|at| match below {
Some(index) => vec![at, index],
None => vec![at],
});
if !place.is_upright() {
return;
}
let rect = place.bounds();
let anchor = if picture {
Anchor::Top
} else {
self.style_of(shape)
.graphic
.text_anchor
.unwrap_or(Anchor::Top)
};
let mut top = rect.top();
if anchor != Anchor::Top {
let sized = self.lay_out(ui, &content, rect, picture, true, prefix.as_ref());
top += (rect.height() - sized.min(rect.height())) * anchor.share();
}
let placed = Rect::from_min_max(pos2(rect.left(), top), rect.max);
self.lay_out(ui, &content, placed, picture, false, prefix.as_ref());
}
fn turned_picture(&mut self, ui: &Ui, shape: &Element, place: Placement) {
let document = self.document;
let Some(texture) = shape
.elements()
.filter(|child| child.is(&Ns::Draw, "image"))
.find_map(|image| {
let href = image.attr(&Ns::Xlink, "href")?;
self.pictures
.get(ui.ctx(), document, href)
.map(eframe::egui::TextureHandle::id)
})
else {
return;
};
let mut mesh = Mesh::with_texture(texture);
for (corner, (u, v)) in
place
.corners()
.into_iter()
.zip([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)])
{
mesh.vertices.push(Vertex {
pos: corner,
uv: pos2(u, v),
color: Color32::WHITE,
});
}
mesh.add_triangle(0, 1, 2);
mesh.add_triangle(0, 2, 3);
self.painter(ui).add(Shape::mesh(mesh));
}
fn lay_out(
&mut self,
ui: &mut Ui,
content: &Element,
rect: Rect,
picture: bool,
measuring: bool,
prefix: Option<&Vec<usize>>,
) -> f32 {
let (page, scale, palette) = (self.page, self.scale, self.palette);
let document = self.document;
let pictures = &mut *self.pictures;
let edit_mode = self.page_editor.is_some() && prefix.is_some();
let page_editor = self
.page_editor
.as_deref_mut()
.filter(|_| edit_mode && !measuring);
let mut builder = UiBuilder::new().max_rect(rect);
if measuring {
builder = builder.sizing_pass().invisible();
}
ui.scope_builder(builder, |ui| {
ui.set_clip_rect(rect.intersect(page));
let mut flow = Flow::new(document, pictures, scale);
flow.palette = palette;
flow.selectable = !edit_mode;
flow.page = page_editor;
flow.find = self.find.clone().filter(|_| prefix.is_some() && !measuring);
if let Some(prefix) = prefix {
flow.start_at(prefix.clone());
}
if picture {
flow.frame(ui, content, rect.width());
} else {
flow.blocks(ui, content, rect.width());
}
if flow.followed.is_some() {
self.followed = flow.followed.take();
}
})
.response
.rect
.height()
}
fn style_of(&self, shape: &Element) -> std::rc::Rc<Properties> {
if let Some(name) = shape.attr(&Ns::Presentation, "style-name") {
return self.document.styles.resolve(&Family::Presentation, name);
}
let name = shape.attr(&Ns::Draw, "style-name").unwrap_or_default();
self.document.styles.resolve(&Family::Graphic, name)
}
fn painter(&self, ui: &Ui) -> eframe::egui::Painter {
ui.painter()
.with_clip_rect(self.page.intersect(ui.clip_rect()))
}
fn placement(&self, shape: &Element) -> Option<Placement> {
let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
let width = at("width").map_or(0.0, Length::points);
let height = at("height").map_or(0.0, Length::points);
let on_page = |x: f32, y: f32| {
pos2(
self.page.left() + x * self.scale,
self.page.top() + y * self.scale,
)
};
if let Some(transform) = shape
.attr(&Ns::Draw, "transform")
.and_then(Transform::parse)
{
let left = at("x").map_or(0.0, Length::points);
let top = at("y").map_or(0.0, Length::points);
let corner = |u: f32, v: f32| {
let (x, y) = transform.apply((width.mul_add(u, left), height.mul_add(v, top)));
on_page(x, y)
};
let origin = corner(0.0, 0.0);
return Some(Placement {
origin,
x: corner(1.0, 0.0) - origin,
y: corner(0.0, 1.0) - origin,
});
}
Some(Placement {
origin: on_page(at("x")?.points(), at("y")?.points()),
x: vec2(width * self.scale, 0.0),
y: vec2(0.0, height * self.scale),
})
}
fn point(&self, shape: &Element, x: &str, y: &str) -> Option<Pos2> {
let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
Some(pos2(
self.page.left() + at(x)?.points() * self.scale,
self.page.top() + at(y)?.points() * self.scale,
))
}
fn stroke(&self, properties: &Properties) -> Option<Stroke> {
let colour = properties.graphic.stroke?;
let width = properties
.graphic
.stroke_width
.map_or(1.0, |w| w.points() * self.scale)
.max(1.0);
Some(Stroke::new(width, format::color32(colour)))
}
fn fill(&mut self, ui: &Ui, rect: Rect, fill: &Fill, opacity: Option<f32>, points: &[Pos2]) {
let document = self.document;
if let Fill::Image(name) = fill {
let Some(href) = document.styles.fill_image(name) else {
return;
};
let Some(texture) = self
.pictures
.get(ui.ctx(), document, href)
.map(eframe::egui::TextureHandle::id)
else {
return;
};
let tint = alpha(Color32::WHITE, opacity);
let mut mesh = Mesh::with_texture(texture);
for point in points {
mesh.vertices.push(Vertex {
pos: *point,
uv: pos2(
(point.x - rect.left()) / rect.width().max(f32::EPSILON),
(point.y - rect.top()) / rect.height().max(f32::EPSILON),
),
color: tint,
});
}
for [a, b, c] in triangulate(points) {
mesh.add_triangle(a, b, c);
}
self.painter(ui).add(Shape::mesh(mesh));
return;
}
let gradient = match fill {
Fill::None | Fill::Image(_) => return,
Fill::Solid(_) => None,
Fill::Gradient(name) => match document.styles.gradient(name) {
Some(gradient) => Some(gradient),
None => return,
},
};
let flat = match fill {
Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
_ => None,
};
let mut mesh = Mesh::default();
for point in points {
let colour = flat.unwrap_or_else(|| {
gradient.map_or(Color32::TRANSPARENT, |gradient| {
alpha(gradient_colour(*point, rect, gradient), opacity)
})
});
mesh.colored_vertex(*point, colour);
}
for [a, b, c] in triangulate(points) {
mesh.add_triangle(a, b, c);
}
self.painter(ui).add(Shape::mesh(mesh));
}
fn flat(&self, fill: &Fill, opacity: Option<f32>) -> Option<Color32> {
match fill {
Fill::None | Fill::Image(_) => None,
Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
Fill::Gradient(name) => {
let gradient = self.document.styles.gradient(name)?;
Some(alpha(blend(gradient.start, gradient.end, 0.5), opacity))
}
}
}
}
fn points(shape: &Element, placement: Placement) -> Vec<Pos2> {
let view: Vec<f32> = shape
.attr(&Ns::Svg, "viewBox")
.unwrap_or_default()
.split_whitespace()
.filter_map(|n| n.parse().ok())
.collect();
let [left, top, width, height] = view[..] else {
return Vec::new();
};
if width <= 0.0 || height <= 0.0 {
return Vec::new();
}
shape
.attr(&Ns::Draw, "points")
.unwrap_or_default()
.split_whitespace()
.filter_map(|pair| {
let (x, y) = pair.split_once(',')?;
let x: f32 = x.trim().parse().ok()?;
let y: f32 = y.trim().parse().ok()?;
Some(placement.across((x - left) / width, (y - top) / height))
})
.collect()
}
fn ellipse(placement: Placement) -> Vec<Pos2> {
const SIDES: usize = 64;
(0..SIDES)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let angle = std::f32::consts::TAU * i as f32 / SIDES as f32;
placement.across(
0.5f32.mul_add(angle.cos(), 0.5),
0.5f32.mul_add(angle.sin(), 0.5),
)
})
.collect()
}
fn alpha(colour: Color32, opacity: Option<f32>) -> Color32 {
match opacity {
Some(opacity) if opacity < 1.0 => colour.gamma_multiply(opacity),
_ => colour,
}
}
fn blend(from: Color, to: Color, t: f32) -> Color32 {
let mix = |a: u8, b: u8| {
let a = f32::from(a);
let b = f32::from(b);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
{
(a + (b - a) * t).round().clamp(0.0, 255.0) as u8
}
};
Color32::from_rgb(mix(from.r, to.r), mix(from.g, to.g), mix(from.b, to.b))
}
fn gradient_colour(point: Pos2, rect: Rect, gradient: &Gradient) -> Color32 {
match gradient.style {
GradientStyle::Linear | GradientStyle::Axial => {}
_ => return blend(gradient.start, gradient.end, 0.5),
}
let radians = gradient.angle.to_radians();
let axis = vec2(radians.sin(), -radians.cos());
let corners = [
rect.left_top(),
rect.right_top(),
rect.right_bottom(),
rect.left_bottom(),
];
let projections = corners.map(|corner| (corner - rect.center()).dot(axis));
let low = projections.iter().copied().fold(f32::MAX, f32::min);
let high = projections.iter().copied().fold(f32::MIN, f32::max);
let span = (high - low).max(f32::EPSILON);
let mut t = ((point - rect.center()).dot(axis) - low) / span;
let border = gradient.border.clamp(0.0, 0.99);
t = ((t - border) / (1.0 - border)).clamp(0.0, 1.0);
if gradient.style == GradientStyle::Axial {
t = (t - 0.5).abs() * 2.0;
}
blend(gradient.start, gradient.end, t)
}
fn triangulate(points: &[Pos2]) -> Vec<[u32; 3]> {
let count = points.len();
if count < 3 {
return Vec::new();
}
let fan = || -> Vec<[u32; 3]> {
(1..count - 1)
.map(|i| {
[
0,
u32::try_from(i).unwrap_or(0),
u32::try_from(i + 1).unwrap_or(0),
]
})
.collect()
};
let area: f32 = (0..count)
.map(|i| {
let (a, b) = (points[i], points[(i + 1) % count]);
a.x * b.y - b.x * a.y
})
.sum();
let winding = if area >= 0.0 { 1.0 } else { -1.0 };
let cross = |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
let inside = |a: Pos2, b: Pos2, c: Pos2, p: Pos2| {
cross(a, b, p) * winding >= 0.0
&& cross(b, c, p) * winding >= 0.0
&& cross(c, a, p) * winding >= 0.0
};
let mut remaining: Vec<usize> = (0..count).collect();
let mut triangles = Vec::with_capacity(count);
let mut stuck = 0;
while remaining.len() > 3 {
if stuck > remaining.len() {
return fan();
}
let mut clipped = false;
for position in 0..remaining.len() {
let corner = [
remaining[(position + remaining.len() - 1) % remaining.len()],
remaining[position],
remaining[(position + 1) % remaining.len()],
];
let ear = corner.map(|index| points[index]);
if cross(ear[0], ear[1], ear[2]) * winding <= 0.0 {
continue;
}
if remaining
.iter()
.filter(|other| !corner.contains(other))
.any(|other| inside(ear[0], ear[1], ear[2], points[*other]))
{
continue;
}
triangles.push(corner.map(|index| u32::try_from(index).unwrap_or(0)));
remaining.remove(position);
clipped = true;
stuck = 0;
break;
}
if !clipped {
stuck += 1;
}
}
if remaining.len() == 3 {
triangles.push([
u32::try_from(remaining[0]).unwrap_or(0),
u32::try_from(remaining[1]).unwrap_or(0),
u32::try_from(remaining[2]).unwrap_or(0),
]);
}
triangles
}
#[cfg(test)]
mod tests {
use super::triangulate;
use eframe::egui::{Pos2, pos2};
fn areas(points: &[Pos2]) -> (f32, f32) {
let cross =
|a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
let triangles: f32 = triangulate(points)
.iter()
.map(|[a, b, c]| {
cross(
points[*a as usize],
points[*b as usize],
points[*c as usize],
)
.abs()
})
.sum();
let outline: f32 = (0..points.len())
.map(|i| {
let (a, b) = (points[i], points[(i + 1) % points.len()]);
a.x * b.y - b.x * a.y
})
.sum::<f32>()
.abs();
(triangles, outline)
}
#[test]
fn a_concave_outline_is_cut_into_the_shape_and_not_its_hull() {
let l = [
pos2(0.0, 0.0),
pos2(2.0, 0.0),
pos2(2.0, 1.0),
pos2(1.0, 1.0),
pos2(1.0, 2.0),
pos2(0.0, 2.0),
];
let (triangles, outline) = areas(&l);
assert!(
(triangles - outline).abs() < 1e-3,
"{triangles} against {outline}"
);
assert!((outline - 6.0).abs() < 1e-3, "{outline}");
}
#[test]
fn a_cross_is_cut_correctly_too() {
let cross = [
pos2(1.0, 0.0),
pos2(2.0, 0.0),
pos2(2.0, 1.0),
pos2(3.0, 1.0),
pos2(3.0, 2.0),
pos2(2.0, 2.0),
pos2(2.0, 3.0),
pos2(1.0, 3.0),
pos2(1.0, 2.0),
pos2(0.0, 2.0),
pos2(0.0, 1.0),
pos2(1.0, 1.0),
];
let (triangles, outline) = areas(&cross);
assert!(
(triangles - outline).abs() < 1e-3,
"{triangles} against {outline}"
);
}
#[test]
fn winding_does_not_matter() {
let mut l = vec![
pos2(0.0, 0.0),
pos2(2.0, 0.0),
pos2(2.0, 1.0),
pos2(1.0, 1.0),
pos2(1.0, 2.0),
pos2(0.0, 2.0),
];
l.reverse();
let (triangles, outline) = areas(&l);
assert!(
(triangles - outline).abs() < 1e-3,
"{triangles} against {outline}"
);
}
#[test]
fn a_square_is_two_triangles() {
let square = [
pos2(0.0, 0.0),
pos2(1.0, 0.0),
pos2(1.0, 1.0),
pos2(0.0, 1.0),
];
assert_eq!(triangulate(&square).len(), 2);
let (triangles, outline) = areas(&square);
assert!((triangles - outline).abs() < 1e-4);
}
}