Skip to main content

odox_ui/
shapes.rs

1//! Drawing ODF's shapes: what a slide is made of, and what a text document or a
2//! spreadsheet can carry in a frame.
3//!
4//! A shape gives its own position and size in the coordinate space of the page
5//! it is on, so nothing here lays anything out. What it does is map that space
6//! onto a rectangle on screen, resolve the style the shape names, and paint the
7//! fill, the outline and the text in that order.
8//!
9//! **What is drawn is what a fixture proves.** ODF's shape vocabulary is far
10//! larger than this: `draw:custom-shape` alone carries a small vector language
11//! in `draw:enhanced-geometry`, with formulas and named equations. A shape this
12//! cannot draw is left undrawn rather than approximated into something the
13//! document does not say, and [`Canvas::shape`] names the ones that are.
14//
15// Author: David M. Anderson
16// Built with AI assistance (Claude, Anthropic)
17
18use eframe::egui::{
19    Color32, Mesh, Pos2, Rect, Shape, Stroke, Ui, UiBuilder, epaint::Vertex, pos2, vec2,
20};
21use odox_core::draw::Geometry;
22use odox_core::{
23    Color, Document, Element, Family, Fill, Gradient, GradientStyle, Length, Ns, Properties,
24};
25
26use crate::flow::{Flow, Pictures};
27use crate::format::{self, Palette};
28
29/// A page, and where on screen it is being drawn.
30pub struct Canvas<'a> {
31    /// The document, for its styles and its pictures.
32    pub document: &'a Document,
33    /// Pictures decoded so far.
34    pub pictures: &'a mut Pictures,
35    /// The rectangle the page occupies on screen.
36    pub page: Rect,
37    /// Screen points per ODF point.
38    pub scale: f32,
39    /// The colours to draw in where the document names none.
40    pub palette: Palette,
41}
42
43impl Canvas<'_> {
44    /// Fill the page.
45    pub fn background(&mut self, ui: &Ui, fill: &Fill) {
46        let page = self.page;
47        self.fill(
48            ui,
49            page,
50            fill,
51            None,
52            &[
53                page.left_top(),
54                page.right_top(),
55                page.right_bottom(),
56                page.left_bottom(),
57            ],
58        );
59    }
60
61    /// One shape, at the place on the page the document puts it.
62    ///
63    /// Drawn: `draw:rect`, `draw:ellipse`, `draw:circle`, `draw:polygon`,
64    /// `draw:polyline`, `draw:line`, `draw:custom-shape` — whose outline is
65    /// worked out by [`Geometry`] — a `draw:frame` holding a picture or a text
66    /// box, and `draw:g`, which is a group and is descended into. Left undrawn:
67    /// `draw:path`, `draw:connector` and `draw:measure`.
68    pub fn shape(&mut self, ui: &mut Ui, shape: &Element) {
69        if shape.is(&Ns::Draw, "g") {
70            for child in shape.elements() {
71                self.shape(ui, child);
72            }
73            return;
74        }
75
76        let properties = self.style_of(shape);
77        let outline = self.stroke(&properties);
78
79        // A line is the one shape positioned by its two ends rather than by a
80        // corner and a size.
81        if shape.is(&Ns::Draw, "line") {
82            let ends = [self.point(shape, "x1", "y1"), self.point(shape, "x2", "y2")];
83            if let ([Some(from), Some(to)], Some(stroke)) = ([ends[0], ends[1]], outline) {
84                self.painter(ui)
85                    .add(Shape::line_segment([from, to], stroke));
86            }
87            return;
88        }
89
90        let Some(rect) = self.rect(shape) else { return };
91
92        if shape.is(&Ns::Draw, "polygon") || shape.is(&Ns::Draw, "polyline") {
93            let points = points(shape, rect);
94            if points.len() >= 2 {
95                if shape.is(&Ns::Draw, "polygon") {
96                    self.fill(
97                        ui,
98                        rect,
99                        &properties.graphic.fill(),
100                        properties.graphic.opacity,
101                        &points,
102                    );
103                    if let Some(stroke) = outline {
104                        self.painter(ui).add(Shape::closed_line(points, stroke));
105                    }
106                } else if let Some(stroke) = outline {
107                    self.painter(ui).add(Shape::line(points, stroke));
108                }
109            }
110            return;
111        }
112
113        if shape.is(&Ns::Draw, "ellipse") || shape.is(&Ns::Draw, "circle") {
114            let (centre, radius) = (rect.center(), rect.size() / 2.0);
115            if let Some(colour) = self.flat(&properties.graphic.fill(), properties.graphic.opacity)
116            {
117                self.painter(ui)
118                    .add(Shape::ellipse_filled(centre, radius, colour));
119            }
120            if let Some(stroke) = outline {
121                self.painter(ui)
122                    .add(Shape::ellipse_stroke(centre, radius, stroke));
123            }
124            return;
125        }
126
127        if shape.is(&Ns::Draw, "custom-shape") {
128            // The outline is a path in a space of the shape's own, and the
129            // formulas in it have to be evaluated before there are any points.
130            if let Some(geometry) = shape
131                .child(&Ns::Draw, "enhanced-geometry")
132                .and_then(Geometry::read)
133            {
134                self.geometry(ui, &geometry, rect, &properties, outline);
135            }
136            self.text(ui, shape, rect);
137            return;
138        }
139
140        if !shape.is(&Ns::Draw, "rect") && !shape.is(&Ns::Draw, "frame") {
141            return;
142        }
143
144        let corners = [
145            rect.left_top(),
146            rect.right_top(),
147            rect.right_bottom(),
148            rect.left_bottom(),
149        ];
150        self.fill(
151            ui,
152            rect,
153            &properties.graphic.fill(),
154            properties.graphic.opacity,
155            &corners,
156        );
157        if let Some(stroke) = outline {
158            self.painter(ui)
159                .add(Shape::closed_line(corners.to_vec(), stroke));
160        }
161        self.text(ui, shape, rect);
162    }
163
164    /// A custom shape's outline, mapped from its own coordinate space onto the
165    /// rectangle it occupies.
166    fn geometry(
167        &mut self,
168        ui: &Ui,
169        geometry: &Geometry,
170        rect: Rect,
171        properties: &Properties,
172        outline: Option<Stroke>,
173    ) {
174        let view = geometry.view;
175        let place = |(x, y): (f32, f32)| {
176            pos2(
177                rect.left() + (x - view.x) / view.width * rect.width(),
178                rect.top() + (y - view.y) / view.height * rect.height(),
179            )
180        };
181        for stroke in &geometry.paths {
182            let points: Vec<Pos2> = stroke.points.iter().copied().map(place).collect();
183            if points.len() < 2 {
184                continue;
185            }
186            if stroke.fill {
187                self.fill(
188                    ui,
189                    rect,
190                    &properties.graphic.fill(),
191                    properties.graphic.opacity,
192                    &points,
193                );
194            }
195            if stroke.stroke
196                && let Some(pen) = outline
197            {
198                let shape = if stroke.closed {
199                    Shape::closed_line(points, pen)
200                } else {
201                    Shape::line(points, pen)
202                };
203                self.painter(ui).add(shape);
204            }
205        }
206    }
207
208    /// The paragraphs a shape holds, or the picture it frames.
209    fn text(&mut self, ui: &mut Ui, shape: &Element, rect: Rect) {
210        // A frame around a picture is handed over whole, because the renderer
211        // finds a frame among a parent's children and here the shape is the
212        // frame itself. A text box is the ordinary case and its paragraphs are
213        // its children.
214        let picture = shape.child(&Ns::Draw, "image").is_some();
215        let content = if picture {
216            shape.clone()
217        } else {
218            match shape.child(&Ns::Draw, "text-box") {
219                Some(box_) => box_.clone(),
220                None => return,
221            }
222        };
223        let (page, scale, palette) = (self.page, self.scale, self.palette);
224        let document = self.document;
225        let pictures = &mut *self.pictures;
226        ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
227            ui.set_clip_rect(rect.intersect(page));
228            let mut flow = Flow::new(document, pictures, scale);
229            flow.palette = palette;
230            if picture {
231                flow.frame(ui, &content, rect.width());
232            } else {
233                flow.blocks(ui, &content, rect.width());
234            }
235        });
236    }
237
238    /// The style a shape names, resolved.
239    ///
240    /// A shape on a slide names a `presentation` style where it is one of the
241    /// slide's own frames and a `graphic` style where it is a drawing; a master
242    /// page's decorations are the second kind. Both chains end in the same
243    /// properties.
244    fn style_of(&self, shape: &Element) -> std::rc::Rc<Properties> {
245        if let Some(name) = shape.attr(&Ns::Presentation, "style-name") {
246            return self.document.styles.resolve(&Family::Presentation, name);
247        }
248        let name = shape.attr(&Ns::Draw, "style-name").unwrap_or_default();
249        self.document.styles.resolve(&Family::Graphic, name)
250    }
251
252    /// Everything is clipped to the page: a master page's decorations are
253    /// routinely wider than the slide they decorate.
254    fn painter(&self, ui: &Ui) -> eframe::egui::Painter {
255        ui.painter()
256            .with_clip_rect(self.page.intersect(ui.clip_rect()))
257    }
258
259    /// Where a shape sits on screen, from its position and size on the page.
260    fn rect(&self, shape: &Element) -> Option<Rect> {
261        let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
262        let (x, y) = (at("x")?, at("y")?);
263        let width = at("width").map_or(0.0, |w| w.points() * self.scale);
264        let height = at("height").map_or(0.0, |h| h.points() * self.scale);
265        Some(Rect::from_min_size(
266            pos2(
267                self.page.left() + x.points() * self.scale,
268                self.page.top() + y.points() * self.scale,
269            ),
270            vec2(width, height),
271        ))
272    }
273
274    fn point(&self, shape: &Element, x: &str, y: &str) -> Option<Pos2> {
275        let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
276        Some(pos2(
277            self.page.left() + at(x)?.points() * self.scale,
278            self.page.top() + at(y)?.points() * self.scale,
279        ))
280    }
281
282    fn stroke(&self, properties: &Properties) -> Option<Stroke> {
283        let colour = properties.graphic.stroke?;
284        let width = properties
285            .graphic
286            .stroke_width
287            .map_or(1.0, |w| w.points() * self.scale)
288            .max(1.0);
289        Some(Stroke::new(width, format::color32(colour)))
290    }
291
292    /// Paint a fill inside an outline.
293    ///
294    /// Always a mesh, and always triangulated. A graphics toolkit fills a closed
295    /// path by cutting it into triangles, and the obvious way — a fan from the
296    /// first point, which is what `Shape::convex_polygon` does — is right only
297    /// for a convex outline. An arrow, a callout and a puzzle piece are none of
298    /// them convex, and a fan across one paints outside it. Colour varies over a
299    /// mesh by varying at its corners, so a gradient costs nothing more than
300    /// asking for the colour at each.
301    fn fill(&mut self, ui: &Ui, rect: Rect, fill: &Fill, opacity: Option<f32>, points: &[Pos2]) {
302        // The reference is copied out so that the picture cache can be filled
303        // while the document is being read from.
304        let document = self.document;
305
306        if let Fill::Image(name) = fill {
307            let Some(href) = document.styles.fill_image(name) else {
308                return;
309            };
310            let Some(texture) = self
311                .pictures
312                .get(ui.ctx(), document, href)
313                .map(eframe::egui::TextureHandle::id)
314            else {
315                return;
316            };
317            // Stretched over the shape's own rectangle: each corner takes the
318            // corner of the picture that the corner of the rectangle is at.
319            let tint = alpha(Color32::WHITE, opacity);
320            let mut mesh = Mesh::with_texture(texture);
321            for point in points {
322                mesh.vertices.push(Vertex {
323                    pos: *point,
324                    uv: pos2(
325                        (point.x - rect.left()) / rect.width().max(f32::EPSILON),
326                        (point.y - rect.top()) / rect.height().max(f32::EPSILON),
327                    ),
328                    color: tint,
329                });
330            }
331            for [a, b, c] in triangulate(points) {
332                mesh.add_triangle(a, b, c);
333            }
334            self.painter(ui).add(Shape::mesh(mesh));
335            return;
336        }
337
338        let gradient = match fill {
339            Fill::None | Fill::Image(_) => return,
340            Fill::Solid(_) => None,
341            Fill::Gradient(name) => match document.styles.gradient(name) {
342                Some(gradient) => Some(gradient),
343                None => return,
344            },
345        };
346        let flat = match fill {
347            Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
348            _ => None,
349        };
350
351        let mut mesh = Mesh::default();
352        for point in points {
353            let colour = flat.unwrap_or_else(|| {
354                gradient.map_or(Color32::TRANSPARENT, |gradient| {
355                    alpha(gradient_colour(*point, rect, gradient), opacity)
356                })
357            });
358            mesh.colored_vertex(*point, colour);
359        }
360        for [a, b, c] in triangulate(points) {
361            mesh.add_triangle(a, b, c);
362        }
363        self.painter(ui).add(Shape::mesh(mesh));
364    }
365
366    /// One colour for a fill, where the shape being drawn cannot carry a mesh.
367    fn flat(&self, fill: &Fill, opacity: Option<f32>) -> Option<Color32> {
368        match fill {
369            // Nothing to draw, and a picture that has no room in an ellipse,
370            // which is drawn as an ellipse rather than as a mesh.
371            Fill::None | Fill::Image(_) => None,
372            Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
373            Fill::Gradient(name) => {
374                let gradient = self.document.styles.gradient(name)?;
375                Some(alpha(blend(gradient.start, gradient.end, 0.5), opacity))
376            }
377        }
378    }
379}
380
381/// A polygon's points, mapped from the coordinate space it declares onto the
382/// rectangle it occupies.
383///
384/// `draw:points` is in the space `svg:viewBox` sets up, which is a shape's
385/// own and has nothing to do with the page's: a polygon 13.5cm wide states
386/// its points out of 13501. Without the mapping every polygon collapses into
387/// the top left corner.
388fn points(shape: &Element, rect: Rect) -> Vec<Pos2> {
389    let view: Vec<f32> = shape
390        .attr(&Ns::Svg, "viewBox")
391        .unwrap_or_default()
392        .split_whitespace()
393        .filter_map(|n| n.parse().ok())
394        .collect();
395    let [left, top, width, height] = view[..] else {
396        return Vec::new();
397    };
398    if width <= 0.0 || height <= 0.0 {
399        return Vec::new();
400    }
401    shape
402        .attr(&Ns::Draw, "points")
403        .unwrap_or_default()
404        .split_whitespace()
405        .filter_map(|pair| {
406            let (x, y) = pair.split_once(',')?;
407            let x: f32 = x.trim().parse().ok()?;
408            let y: f32 = y.trim().parse().ok()?;
409            Some(pos2(
410                rect.left() + (x - left) / width * rect.width(),
411                rect.top() + (y - top) / height * rect.height(),
412            ))
413        })
414        .collect()
415}
416
417fn alpha(colour: Color32, opacity: Option<f32>) -> Color32 {
418    match opacity {
419        Some(opacity) if opacity < 1.0 => colour.gamma_multiply(opacity),
420        _ => colour,
421    }
422}
423
424fn blend(from: Color, to: Color, t: f32) -> Color32 {
425    let mix = |a: u8, b: u8| {
426        let a = f32::from(a);
427        let b = f32::from(b);
428        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
429        {
430            (a + (b - a) * t).round().clamp(0.0, 255.0) as u8
431        }
432    };
433    Color32::from_rgb(mix(from.r, to.r), mix(from.g, to.g), mix(from.b, to.b))
434}
435
436/// The colour a gradient has at one point of the rectangle it fills.
437///
438/// **Linear and axial run in the direction the document gives; the four that
439/// radiate from a point do not.** A radial gradient's colour depends on the
440/// distance from a centre, which this could compute — and no fixture uses one,
441/// so it would be a direction invented rather than measured. Those get the flat
442/// average of the two colours, which is visibly an approximation.
443fn gradient_colour(point: Pos2, rect: Rect, gradient: &Gradient) -> Color32 {
444    match gradient.style {
445        GradientStyle::Linear | GradientStyle::Axial => {}
446        _ => return blend(gradient.start, gradient.end, 0.5),
447    }
448
449    // ODF measures the angle counter-clockwise from the direction that runs
450    // bottom to top, and the screen's y grows downward, so the axis is the unit
451    // vector below. A point's place along the gradient is its projection onto
452    // it, rescaled so that the rectangle's own extent is nought to one.
453    let radians = gradient.angle.to_radians();
454    let axis = vec2(radians.sin(), -radians.cos());
455    let corners = [
456        rect.left_top(),
457        rect.right_top(),
458        rect.right_bottom(),
459        rect.left_bottom(),
460    ];
461    let projections = corners.map(|corner| (corner - rect.center()).dot(axis));
462    let low = projections.iter().copied().fold(f32::MAX, f32::min);
463    let high = projections.iter().copied().fold(f32::MIN, f32::max);
464    let span = (high - low).max(f32::EPSILON);
465
466    let mut t = ((point - rect.center()).dot(axis) - low) / span;
467    // The border is the fraction of the run that stays the start colour before
468    // the blend begins.
469    let border = gradient.border.clamp(0.0, 0.99);
470    t = ((t - border) / (1.0 - border)).clamp(0.0, 1.0);
471    // An axial gradient runs out from the middle to both edges, so each half of
472    // the rectangle takes the whole blend.
473    if gradient.style == GradientStyle::Axial {
474        t = (t - 0.5).abs() * 2.0;
475    }
476    blend(gradient.start, gradient.end, t)
477}
478
479/// Cut a closed outline into triangles, by clipping ears.
480///
481/// The standard method, and the reason for it is above [`Canvas::fill`]: the
482/// cheap alternative is right only for convex outlines and ODF's shapes are
483/// routinely not. An outline it cannot cut — one that crosses itself, which a
484/// hand-edited document can hold — falls back to the fan, which is wrong in the
485/// way the fan is always wrong rather than in a new way.
486fn triangulate(points: &[Pos2]) -> Vec<[u32; 3]> {
487    let count = points.len();
488    if count < 3 {
489        return Vec::new();
490    }
491    let fan = || -> Vec<[u32; 3]> {
492        (1..count - 1)
493            .map(|i| {
494                [
495                    0,
496                    u32::try_from(i).unwrap_or(0),
497                    u32::try_from(i + 1).unwrap_or(0),
498                ]
499            })
500            .collect()
501    };
502
503    // Twice the signed area, whose sign is which way round the outline goes.
504    let area: f32 = (0..count)
505        .map(|i| {
506            let (a, b) = (points[i], points[(i + 1) % count]);
507            a.x * b.y - b.x * a.y
508        })
509        .sum();
510    let winding = if area >= 0.0 { 1.0 } else { -1.0 };
511
512    let cross = |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
513    let inside = |a: Pos2, b: Pos2, c: Pos2, p: Pos2| {
514        cross(a, b, p) * winding >= 0.0
515            && cross(b, c, p) * winding >= 0.0
516            && cross(c, a, p) * winding >= 0.0
517    };
518
519    let mut remaining: Vec<usize> = (0..count).collect();
520    let mut triangles = Vec::with_capacity(count);
521    let mut stuck = 0;
522    while remaining.len() > 3 {
523        if stuck > remaining.len() {
524            return fan();
525        }
526        let mut clipped = false;
527        for position in 0..remaining.len() {
528            let corner = [
529                remaining[(position + remaining.len() - 1) % remaining.len()],
530                remaining[position],
531                remaining[(position + 1) % remaining.len()],
532            ];
533            let ear = corner.map(|index| points[index]);
534            // A reflex corner is not an ear, and neither is one whose triangle
535            // has another corner of the outline inside it.
536            if cross(ear[0], ear[1], ear[2]) * winding <= 0.0 {
537                continue;
538            }
539            if remaining
540                .iter()
541                .filter(|other| !corner.contains(other))
542                .any(|other| inside(ear[0], ear[1], ear[2], points[*other]))
543            {
544                continue;
545            }
546            triangles.push(corner.map(|index| u32::try_from(index).unwrap_or(0)));
547            remaining.remove(position);
548            clipped = true;
549            stuck = 0;
550            break;
551        }
552        if !clipped {
553            stuck += 1;
554        }
555    }
556    if remaining.len() == 3 {
557        triangles.push([
558            u32::try_from(remaining[0]).unwrap_or(0),
559            u32::try_from(remaining[1]).unwrap_or(0),
560            u32::try_from(remaining[2]).unwrap_or(0),
561        ]);
562    }
563    triangles
564}
565
566#[cfg(test)]
567mod tests {
568    use super::triangulate;
569    use eframe::egui::{Pos2, pos2};
570
571    /// Twice the area a run of triangles covers, and twice the area the outline
572    /// encloses. Equal means the triangles cover the shape and nothing else.
573    fn areas(points: &[Pos2]) -> (f32, f32) {
574        let cross =
575            |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
576        let triangles: f32 = triangulate(points)
577            .iter()
578            .map(|[a, b, c]| {
579                cross(
580                    points[*a as usize],
581                    points[*b as usize],
582                    points[*c as usize],
583                )
584                .abs()
585            })
586            .sum();
587        let outline: f32 = (0..points.len())
588            .map(|i| {
589                let (a, b) = (points[i], points[(i + 1) % points.len()]);
590                a.x * b.y - b.x * a.y
591            })
592            .sum::<f32>()
593            .abs();
594        (triangles, outline)
595    }
596
597    /// Would catch the fan: an L covers three quarters of its bounding box, and
598    /// a fan from the first corner covers the whole of it.
599    #[test]
600    fn a_concave_outline_is_cut_into_the_shape_and_not_its_hull() {
601        let l = [
602            pos2(0.0, 0.0),
603            pos2(2.0, 0.0),
604            pos2(2.0, 1.0),
605            pos2(1.0, 1.0),
606            pos2(1.0, 2.0),
607            pos2(0.0, 2.0),
608        ];
609        let (triangles, outline) = areas(&l);
610        assert!(
611            (triangles - outline).abs() < 1e-3,
612            "{triangles} against {outline}"
613        );
614        // Three of the four unit squares, twice over.
615        assert!((outline - 6.0).abs() < 1e-3, "{outline}");
616    }
617
618    /// A cross has four reflex corners and is where a careless ear test fails.
619    #[test]
620    fn a_cross_is_cut_correctly_too() {
621        let cross = [
622            pos2(1.0, 0.0),
623            pos2(2.0, 0.0),
624            pos2(2.0, 1.0),
625            pos2(3.0, 1.0),
626            pos2(3.0, 2.0),
627            pos2(2.0, 2.0),
628            pos2(2.0, 3.0),
629            pos2(1.0, 3.0),
630            pos2(1.0, 2.0),
631            pos2(0.0, 2.0),
632            pos2(0.0, 1.0),
633            pos2(1.0, 1.0),
634        ];
635        let (triangles, outline) = areas(&cross);
636        assert!(
637            (triangles - outline).abs() < 1e-3,
638            "{triangles} against {outline}"
639        );
640    }
641
642    /// The same outline the other way round: the winding must not decide whether
643    /// it works, because a mirrored shape arrives reversed.
644    #[test]
645    fn winding_does_not_matter() {
646        let mut l = vec![
647            pos2(0.0, 0.0),
648            pos2(2.0, 0.0),
649            pos2(2.0, 1.0),
650            pos2(1.0, 1.0),
651            pos2(1.0, 2.0),
652            pos2(0.0, 2.0),
653        ];
654        l.reverse();
655        let (triangles, outline) = areas(&l);
656        assert!(
657            (triangles - outline).abs() < 1e-3,
658            "{triangles} against {outline}"
659        );
660    }
661
662    /// A convex outline is the ordinary case and must still come out whole.
663    #[test]
664    fn a_square_is_two_triangles() {
665        let square = [
666            pos2(0.0, 0.0),
667            pos2(1.0, 0.0),
668            pos2(1.0, 1.0),
669            pos2(0.0, 1.0),
670        ];
671        assert_eq!(triangulate(&square).len(), 2);
672        let (triangles, outline) = areas(&square);
673        assert!((triangles - outline).abs() < 1e-4);
674    }
675}