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, Vec2, epaint::Vertex, pos2, vec2,
20};
21use odox_core::draw::Geometry;
22
23use odox_core::{
24    Anchor, Color, Document, Element, Family, Fill, Gradient, GradientStyle, Length, Ns,
25    Properties, Transform,
26};
27
28use crate::find::Highlights;
29use crate::flow::{Flow, Pictures};
30use crate::flow_model::PageEditor;
31use crate::format::{self, Palette};
32
33/// A page, and where on screen it is being drawn.
34pub struct Canvas<'a> {
35    /// The document, for its styles and its pictures.
36    pub document: &'a Document,
37    /// Pictures decoded so far.
38    pub pictures: &'a mut Pictures,
39    /// The rectangle the page occupies on screen.
40    pub page: Rect,
41    /// Screen points per ODF point.
42    pub scale: f32,
43    /// The colours to draw in where the document names none.
44    pub palette: Palette,
45    /// The page editor, in edit mode: the slide's own labels are typed into
46    /// through it, each paragraph named by its path from the page, which is
47    /// the shape's index among the page's children and then the way down.
48    pub page_editor: Option<&'a mut PageEditor>,
49    /// The matches of a search, drawn in the slide's own labels.
50    pub find: Option<Highlights<'a>>,
51    /// A place in the document that a link in a label just followed, for the
52    /// view to go to.
53    pub followed: Option<String>,
54    /// The index among the page's children of the shape being drawn, where it
55    /// is one of the slide's own; a master page's decoration has none and
56    /// nothing in it is edited.
57    at: Option<usize>,
58}
59
60/// Where a shape's own box lands on the screen.
61///
62/// A shape is drawn in a box of its own, and this is the map from that box onto
63/// the window. Ordinarily it is a corner and a size and the box's edges stay
64/// along the page's, but a shape placed by `draw:transform` is turned or leaned
65/// as well, so the map is the general one: a corner and the two vectors along
66/// the edges that meet there.
67#[derive(Clone, Copy)]
68struct Placement {
69    /// Where the box's top left corner lands.
70    origin: Pos2,
71    /// From that corner to the top right one.
72    x: Vec2,
73    /// From that corner to the bottom left one.
74    y: Vec2,
75}
76
77impl Placement {
78    /// A point of the box, in the fractions of it across and down.
79    fn across(self, u: f32, v: f32) -> Pos2 {
80        self.origin + self.x * u + self.y * v
81    }
82
83    /// The four corners, going round.
84    fn corners(self) -> [Pos2; 4] {
85        [
86            self.across(0.0, 0.0),
87            self.across(1.0, 0.0),
88            self.across(1.0, 1.0),
89            self.across(0.0, 1.0),
90        ]
91    }
92
93    /// The smallest upright rectangle the shape fits inside, which is what a
94    /// gradient runs across and where a label is laid out.
95    fn bounds(self) -> Rect {
96        Rect::from_points(&self.corners())
97    }
98
99    /// Whether the box is still square to the page, which is the case a
100    /// rectangle can stand in for.
101    fn is_upright(self) -> bool {
102        self.x.y.abs() < 0.01 && self.y.x.abs() < 0.01 && self.x.x >= 0.0 && self.y.y >= 0.0
103    }
104}
105
106/// Whether a shape has an area, which is a property of the kind of shape it is
107/// rather than of the style it names.
108#[derive(Clone, Copy, PartialEq, Eq)]
109enum Filled {
110    Yes,
111    No,
112}
113
114impl<'a> Canvas<'a> {
115    /// A canvas over a page, drawing nothing editable until told otherwise.
116    pub fn new(
117        document: &'a Document,
118        pictures: &'a mut Pictures,
119        page: Rect,
120        scale: f32,
121        palette: Palette,
122    ) -> Self {
123        Self {
124            document,
125            pictures,
126            page,
127            scale,
128            palette,
129            page_editor: None,
130            find: None,
131            followed: None,
132            at: None,
133        }
134    }
135
136    /// One of the slide's own shapes, by its index among the page's children,
137    /// which is what a label inside it is edited under.
138    pub fn slide_shape(&mut self, ui: &mut Ui, index: usize, shape: &Element) {
139        self.at = Some(index);
140        self.shape(ui, shape);
141        self.at = None;
142    }
143}
144
145impl Canvas<'_> {
146    /// Fill the page.
147    pub fn background(&mut self, ui: &Ui, fill: &Fill) {
148        let page = self.page;
149        self.fill(
150            ui,
151            page,
152            fill,
153            None,
154            &[
155                page.left_top(),
156                page.right_top(),
157                page.right_bottom(),
158                page.left_bottom(),
159            ],
160        );
161    }
162
163    /// One shape, at the place on the page the document puts it.
164    ///
165    /// Drawn: `draw:rect`, `draw:ellipse`, `draw:circle`, `draw:polygon`,
166    /// `draw:polyline`, `draw:line`, `draw:custom-shape` and `draw:path` — the
167    /// last two have their outlines worked out by [`Geometry`], from ODF's own
168    /// command language and from SVG's respectively — a `draw:frame` holding a
169    /// picture or a text box, `draw:connector`, whose route between the two
170    /// shapes it joins the producer has already worked out, and `draw:g`, which
171    /// is a group and is descended into. Left undrawn: `draw:measure`.
172    pub fn shape(&mut self, ui: &mut Ui, shape: &Element) {
173        if shape.is(&Ns::Draw, "g") {
174            for child in shape.elements() {
175                self.shape(ui, child);
176            }
177            return;
178        }
179
180        let properties = self.style_of(shape);
181        let outline = self.stroke(&properties);
182
183        // A line is the one shape positioned by its two ends rather than by a
184        // corner and a size.
185        if shape.is(&Ns::Draw, "line") {
186            let ends = [self.point(shape, "x1", "y1"), self.point(shape, "x2", "y2")];
187            if let ([Some(from), Some(to)], Some(stroke)) = ([ends[0], ends[1]], outline) {
188                self.painter(ui)
189                    .add(Shape::line_segment([from, to], stroke));
190            }
191            return;
192        }
193
194        if shape.is(&Ns::Draw, "connector") {
195            self.connector(ui, shape, &properties, outline);
196            return;
197        }
198
199        let Some(place) = self.placement(shape) else {
200            return;
201        };
202        let rect = place.bounds();
203
204        if shape.is(&Ns::Draw, "polygon") || shape.is(&Ns::Draw, "polyline") {
205            let points = points(shape, place);
206            if points.len() >= 2 {
207                if shape.is(&Ns::Draw, "polygon") {
208                    self.fill(
209                        ui,
210                        rect,
211                        &properties.graphic.fill(),
212                        properties.graphic.opacity,
213                        &points,
214                    );
215                    if let Some(stroke) = outline {
216                        self.painter(ui).add(Shape::closed_line(points, stroke));
217                    }
218                } else if let Some(stroke) = outline {
219                    self.painter(ui).add(Shape::line(points, stroke));
220                }
221            }
222            return;
223        }
224
225        if shape.is(&Ns::Draw, "ellipse") || shape.is(&Ns::Draw, "circle") {
226            self.ellipse(ui, place, &properties, outline);
227            self.text(ui, shape, place);
228            return;
229        }
230
231        if shape.is(&Ns::Draw, "path") {
232            // A different notation for the same thing: SVG path data rather than
233            // ODF's own commands, and the same polylines out of it.
234            if let Some(geometry) = Geometry::read_path(shape) {
235                self.geometry(ui, &geometry, place, &properties, outline, Filled::Yes);
236            }
237            self.text(ui, shape, place);
238            return;
239        }
240
241        if shape.is(&Ns::Draw, "custom-shape") {
242            // The outline is a path in a space of the shape's own, and the
243            // formulas in it have to be evaluated before there are any points.
244            if let Some(geometry) = shape
245                .child(&Ns::Draw, "enhanced-geometry")
246                .and_then(Geometry::read)
247            {
248                self.geometry(ui, &geometry, place, &properties, outline, Filled::Yes);
249            }
250            self.text(ui, shape, place);
251            return;
252        }
253
254        if !shape.is(&Ns::Draw, "rect") && !shape.is(&Ns::Draw, "frame") {
255            return;
256        }
257
258        let corners = place.corners();
259        self.fill(
260            ui,
261            rect,
262            &properties.graphic.fill(),
263            properties.graphic.opacity,
264            &corners,
265        );
266        if let Some(stroke) = outline {
267            self.painter(ui)
268                .add(Shape::closed_line(corners.to_vec(), stroke));
269        }
270        self.text(ui, shape, place);
271    }
272
273    /// An ellipse, however its box is placed.
274    fn ellipse(
275        &mut self,
276        ui: &Ui,
277        place: Placement,
278        properties: &Properties,
279        outline: Option<Stroke>,
280    ) {
281        let rect = place.bounds();
282        if place.is_upright() {
283            let (centre, radius) = (rect.center(), rect.size() / 2.0);
284            if let Some(colour) = self.flat(&properties.graphic.fill(), properties.graphic.opacity)
285            {
286                self.painter(ui)
287                    .add(Shape::ellipse_filled(centre, radius, colour));
288            }
289            if let Some(stroke) = outline {
290                self.painter(ui)
291                    .add(Shape::ellipse_stroke(centre, radius, stroke));
292            }
293        } else {
294            // A turned ellipse is no longer an ellipse of the window's, so
295            // it is drawn as the polygon it is: the same curve, in the
296            // shape's own box, mapped through the placement like any other.
297            let points = ellipse(place);
298            self.fill(
299                ui,
300                rect,
301                &properties.graphic.fill(),
302                properties.graphic.opacity,
303                &points,
304            );
305            if let Some(stroke) = outline {
306                self.painter(ui).add(Shape::closed_line(points, stroke));
307            }
308        }
309    }
310
311    /// A connector, and the label it may carry.
312    ///
313    /// It is positioned by the two ends it joins and has no corner and no size
314    /// of its own. The route between them is the producer's to work out, and it
315    /// writes the result as SVG path data; where it wrote none, the straight
316    /// line between the ends is the whole shape.
317    fn connector(
318        &mut self,
319        ui: &mut Ui,
320        shape: &Element,
321        properties: &Properties,
322        outline: Option<Stroke>,
323    ) {
324        let (Some(from), Some(to)) = (self.point(shape, "x1", "y1"), self.point(shape, "x2", "y2"))
325        else {
326            return;
327        };
328        let rect = Rect::from_two_pos(from, to);
329        match Geometry::read_path(shape) {
330            // The route's coordinates are the page's and the view box beside
331            // them says nothing useful, so the outline states its own space.
332            Some(mut geometry) => {
333                geometry.refit();
334                let place = Placement {
335                    origin: rect.left_top(),
336                    x: vec2(rect.width(), 0.0),
337                    y: vec2(0.0, rect.height()),
338                };
339                self.geometry(ui, &geometry, place, properties, outline, Filled::No);
340            }
341            None => {
342                if let Some(stroke) = outline {
343                    self.painter(ui)
344                        .add(Shape::line_segment([from, to], stroke));
345                }
346            }
347        }
348        self.text(
349            ui,
350            shape,
351            Placement {
352                origin: rect.left_top(),
353                x: vec2(rect.width(), 0.0),
354                y: vec2(0.0, rect.height()),
355            },
356        );
357    }
358
359    /// A custom shape's outline, mapped from its own coordinate space onto the
360    /// rectangle it occupies.
361    ///
362    /// `filled` is the shape kind's answer and not the style's. A connector's
363    /// style routinely says `draw:fill="solid"` — `LibreOffice` writes it on
364    /// every one — and a connector has no area for a fill to go in, so the
365    /// route would be painted as a ribbon of whatever colour the style named.
366    fn geometry(
367        &mut self,
368        ui: &Ui,
369        geometry: &Geometry,
370        placement: Placement,
371        properties: &Properties,
372        outline: Option<Stroke>,
373        filled: Filled,
374    ) {
375        let view = geometry.view;
376        let rect = placement.bounds();
377        let place = |(x, y): (f32, f32)| {
378            placement.across((x - view.x) / view.width, (y - view.y) / view.height)
379        };
380        for stroke in &geometry.paths {
381            let points: Vec<Pos2> = stroke.points.iter().copied().map(place).collect();
382            if points.len() < 2 {
383                continue;
384            }
385            if stroke.fill && filled == Filled::Yes {
386                self.fill(
387                    ui,
388                    rect,
389                    &properties.graphic.fill(),
390                    properties.graphic.opacity,
391                    &points,
392                );
393            }
394            if stroke.stroke
395                && let Some(pen) = outline
396            {
397                let shape = if stroke.closed {
398                    Shape::closed_line(points, pen)
399                } else {
400                    Shape::line(points, pen)
401                };
402                self.painter(ui).add(shape);
403            }
404        }
405    }
406
407    /// The paragraphs a shape holds, or the picture it frames.
408    ///
409    /// **A turned shape keeps its picture and loses its label.** A picture is
410    /// four corners and a texture and turns exactly; a paragraph is a line
411    /// breaker, a font and a selection, and drawing one upright inside a box
412    /// that is not upright says something the document does not. Across the
413    /// templates that is twenty-two text boxes, and they stay undrawn.
414    fn text(&mut self, ui: &mut Ui, shape: &Element, place: Placement) {
415        // A frame around a picture is handed over whole, because the renderer
416        // finds a frame among a parent's children and here the shape is the
417        // frame itself.
418        let picture = shape.child(&Ns::Draw, "image").is_some();
419        if picture && !place.is_upright() {
420            self.turned_picture(ui, shape, place);
421            return;
422        }
423        let text_box = shape
424            .elements_indexed()
425            .find(|(_, e)| e.is(&Ns::Draw, "text-box"));
426        let (content, below) = if picture {
427            (shape.clone(), None)
428        } else if let Some((index, box_)) = text_box {
429            (box_.clone(), Some(index))
430        } else if shape.child(&Ns::Text, "p").is_some() || shape.child(&Ns::Text, "list").is_some()
431        {
432            // A drawing shape keeps its label as paragraphs of its own. The box
433            // is a frame's way of saying the same thing, and across the
434            // presentation templates it is the shapes that use it, not the
435            // frames.
436            (shape.clone(), None)
437        } else {
438            return;
439        };
440        // The way from the page to the content, for a label that is edited.
441        let prefix = self.at.map(|at| match below {
442            Some(index) => vec![at, index],
443            None => vec![at],
444        });
445        if !place.is_upright() {
446            return;
447        }
448        let rect = place.bounds();
449
450        let anchor = if picture {
451            Anchor::Top
452        } else {
453            self.style_of(shape)
454                .graphic
455                .text_anchor
456                .unwrap_or(Anchor::Top)
457        };
458        let mut top = rect.top();
459        if anchor != Anchor::Top {
460            // Where the label goes depends on how tall it turns out to be, and
461            // how tall it turns out to be depends on the fonts and the wrapping,
462            // so it is laid out twice: once into a ui that draws nothing, to
463            // measure, and then once for real at the offset that measurement
464            // gives.
465            let sized = self.lay_out(ui, &content, rect, picture, true, prefix.as_ref());
466            top += (rect.height() - sized.min(rect.height())) * anchor.share();
467        }
468        let placed = Rect::from_min_max(pos2(rect.left(), top), rect.max);
469        self.lay_out(ui, &content, placed, picture, false, prefix.as_ref());
470    }
471
472    /// A picture in a frame that is turned, drawn as its own four corners.
473    ///
474    /// The window's own image widget draws into an upright rectangle, so this
475    /// builds the quadrilateral instead: the same texture, its corners at the
476    /// frame's. The alternatives are tried in the order the producer wrote them,
477    /// as they are for an upright frame.
478    fn turned_picture(&mut self, ui: &Ui, shape: &Element, place: Placement) {
479        let document = self.document;
480        let Some(texture) = shape
481            .elements()
482            .filter(|child| child.is(&Ns::Draw, "image"))
483            .find_map(|image| {
484                let href = image.attr(&Ns::Xlink, "href")?;
485                self.pictures
486                    .get(ui.ctx(), document, href)
487                    .map(eframe::egui::TextureHandle::id)
488            })
489        else {
490            return;
491        };
492        let mut mesh = Mesh::with_texture(texture);
493        for (corner, (u, v)) in
494            place
495                .corners()
496                .into_iter()
497                .zip([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)])
498        {
499            mesh.vertices.push(Vertex {
500                pos: corner,
501                uv: pos2(u, v),
502                color: Color32::WHITE,
503            });
504        }
505        mesh.add_triangle(0, 1, 2);
506        mesh.add_triangle(0, 2, 3);
507        self.painter(ui).add(Shape::mesh(mesh));
508    }
509
510    /// Draw a shape's content into a rectangle, or measure how tall it is
511    /// without drawing it. Returns the height it took.
512    fn lay_out(
513        &mut self,
514        ui: &mut Ui,
515        content: &Element,
516        rect: Rect,
517        picture: bool,
518        measuring: bool,
519        prefix: Option<&Vec<usize>>,
520    ) -> f32 {
521        let (page, scale, palette) = (self.page, self.scale, self.palette);
522        let document = self.document;
523        let pictures = &mut *self.pictures;
524        // Editable where the label belongs to one of the slide's own shapes,
525        // which is what a prefix says, and in the pass that draws it: the
526        // measuring pass draws nothing, and the editor would take its clicks.
527        let edit_mode = self.page_editor.is_some() && prefix.is_some();
528        let page_editor = self
529            .page_editor
530            .as_deref_mut()
531            .filter(|_| edit_mode && !measuring);
532        let mut builder = UiBuilder::new().max_rect(rect);
533        if measuring {
534            builder = builder.sizing_pass().invisible();
535        }
536        ui.scope_builder(builder, |ui| {
537            ui.set_clip_rect(rect.intersect(page));
538            let mut flow = Flow::new(document, pictures, scale);
539            flow.palette = palette;
540            flow.selectable = !edit_mode;
541            flow.page = page_editor;
542            flow.find = self.find.clone().filter(|_| prefix.is_some() && !measuring);
543            if let Some(prefix) = prefix {
544                flow.start_at(prefix.clone());
545            }
546            if picture {
547                flow.frame(ui, content, rect.width());
548            } else {
549                flow.blocks(ui, content, rect.width());
550            }
551            if flow.followed.is_some() {
552                self.followed = flow.followed.take();
553            }
554        })
555        .response
556        .rect
557        .height()
558    }
559
560    /// The style a shape names, resolved.
561    ///
562    /// A shape on a slide names a `presentation` style where it is one of the
563    /// slide's own frames and a `graphic` style where it is a drawing; a master
564    /// page's decorations are the second kind. Both chains end in the same
565    /// properties.
566    fn style_of(&self, shape: &Element) -> std::rc::Rc<Properties> {
567        if let Some(name) = shape.attr(&Ns::Presentation, "style-name") {
568            return self.document.styles.resolve(&Family::Presentation, name);
569        }
570        let name = shape.attr(&Ns::Draw, "style-name").unwrap_or_default();
571        self.document.styles.resolve(&Family::Graphic, name)
572    }
573
574    /// Everything is clipped to the page: a master page's decorations are
575    /// routinely wider than the slide they decorate.
576    fn painter(&self, ui: &Ui) -> eframe::egui::Painter {
577        ui.painter()
578            .with_clip_rect(self.page.intersect(ui.clip_rect()))
579    }
580
581    /// Where a shape's own box sits on screen.
582    ///
583    /// Ordinarily its corner and its size, which is a rectangle square to the
584    /// page. A shape that is turned or leaned states `draw:transform` instead
585    /// and routinely gives no corner at all, and a reader that insists on one
586    /// drops the shape: it is the templates' own decoration that is placed this
587    /// way. `odox_core::Transform` says how the list is read.
588    fn placement(&self, shape: &Element) -> Option<Placement> {
589        let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
590        let width = at("width").map_or(0.0, Length::points);
591        let height = at("height").map_or(0.0, Length::points);
592        let on_page = |x: f32, y: f32| {
593            pos2(
594                self.page.left() + x * self.scale,
595                self.page.top() + y * self.scale,
596            )
597        };
598
599        if let Some(transform) = shape
600            .attr(&Ns::Draw, "transform")
601            .and_then(Transform::parse)
602        {
603            // The box begins at the shape's corner where it has one, and at the
604            // page's origin where the transform is the whole of its placement.
605            let left = at("x").map_or(0.0, Length::points);
606            let top = at("y").map_or(0.0, Length::points);
607            let corner = |u: f32, v: f32| {
608                let (x, y) = transform.apply((width.mul_add(u, left), height.mul_add(v, top)));
609                on_page(x, y)
610            };
611            let origin = corner(0.0, 0.0);
612            return Some(Placement {
613                origin,
614                x: corner(1.0, 0.0) - origin,
615                y: corner(0.0, 1.0) - origin,
616            });
617        }
618
619        Some(Placement {
620            origin: on_page(at("x")?.points(), at("y")?.points()),
621            x: vec2(width * self.scale, 0.0),
622            y: vec2(0.0, height * self.scale),
623        })
624    }
625
626    fn point(&self, shape: &Element, x: &str, y: &str) -> Option<Pos2> {
627        let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
628        Some(pos2(
629            self.page.left() + at(x)?.points() * self.scale,
630            self.page.top() + at(y)?.points() * self.scale,
631        ))
632    }
633
634    fn stroke(&self, properties: &Properties) -> Option<Stroke> {
635        let colour = properties.graphic.stroke?;
636        let width = properties
637            .graphic
638            .stroke_width
639            .map_or(1.0, |w| w.points() * self.scale)
640            .max(1.0);
641        Some(Stroke::new(width, format::color32(colour)))
642    }
643
644    /// Paint a fill inside an outline.
645    ///
646    /// Always a mesh, and always triangulated. A graphics toolkit fills a closed
647    /// path by cutting it into triangles, and the obvious way — a fan from the
648    /// first point, which is what `Shape::convex_polygon` does — is right only
649    /// for a convex outline. An arrow, a callout and a puzzle piece are none of
650    /// them convex, and a fan across one paints outside it. Colour varies over a
651    /// mesh by varying at its corners, so a gradient costs nothing more than
652    /// asking for the colour at each.
653    fn fill(&mut self, ui: &Ui, rect: Rect, fill: &Fill, opacity: Option<f32>, points: &[Pos2]) {
654        // The reference is copied out so that the picture cache can be filled
655        // while the document is being read from.
656        let document = self.document;
657
658        if let Fill::Image(name) = fill {
659            let Some(href) = document.styles.fill_image(name) else {
660                return;
661            };
662            let Some(texture) = self
663                .pictures
664                .get(ui.ctx(), document, href)
665                .map(eframe::egui::TextureHandle::id)
666            else {
667                return;
668            };
669            // Stretched over the shape's own rectangle: each corner takes the
670            // corner of the picture that the corner of the rectangle is at.
671            let tint = alpha(Color32::WHITE, opacity);
672            let mut mesh = Mesh::with_texture(texture);
673            for point in points {
674                mesh.vertices.push(Vertex {
675                    pos: *point,
676                    uv: pos2(
677                        (point.x - rect.left()) / rect.width().max(f32::EPSILON),
678                        (point.y - rect.top()) / rect.height().max(f32::EPSILON),
679                    ),
680                    color: tint,
681                });
682            }
683            for [a, b, c] in triangulate(points) {
684                mesh.add_triangle(a, b, c);
685            }
686            self.painter(ui).add(Shape::mesh(mesh));
687            return;
688        }
689
690        let gradient = match fill {
691            Fill::None | Fill::Image(_) => return,
692            Fill::Solid(_) => None,
693            Fill::Gradient(name) => match document.styles.gradient(name) {
694                Some(gradient) => Some(gradient),
695                None => return,
696            },
697        };
698        let flat = match fill {
699            Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
700            _ => None,
701        };
702
703        let mut mesh = Mesh::default();
704        for point in points {
705            let colour = flat.unwrap_or_else(|| {
706                gradient.map_or(Color32::TRANSPARENT, |gradient| {
707                    alpha(gradient_colour(*point, rect, gradient), opacity)
708                })
709            });
710            mesh.colored_vertex(*point, colour);
711        }
712        for [a, b, c] in triangulate(points) {
713            mesh.add_triangle(a, b, c);
714        }
715        self.painter(ui).add(Shape::mesh(mesh));
716    }
717
718    /// One colour for a fill, where the shape being drawn cannot carry a mesh.
719    fn flat(&self, fill: &Fill, opacity: Option<f32>) -> Option<Color32> {
720        match fill {
721            // Nothing to draw, and a picture that has no room in an ellipse,
722            // which is drawn as an ellipse rather than as a mesh.
723            Fill::None | Fill::Image(_) => None,
724            Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
725            Fill::Gradient(name) => {
726                let gradient = self.document.styles.gradient(name)?;
727                Some(alpha(blend(gradient.start, gradient.end, 0.5), opacity))
728            }
729        }
730    }
731}
732
733/// A polygon's points, mapped from the coordinate space it declares onto the
734/// rectangle it occupies.
735///
736/// `draw:points` is in the space `svg:viewBox` sets up, which is a shape's
737/// own and has nothing to do with the page's: a polygon 13.5cm wide states
738/// its points out of 13501. Without the mapping every polygon collapses into
739/// the top left corner.
740fn points(shape: &Element, placement: Placement) -> Vec<Pos2> {
741    let view: Vec<f32> = shape
742        .attr(&Ns::Svg, "viewBox")
743        .unwrap_or_default()
744        .split_whitespace()
745        .filter_map(|n| n.parse().ok())
746        .collect();
747    let [left, top, width, height] = view[..] else {
748        return Vec::new();
749    };
750    if width <= 0.0 || height <= 0.0 {
751        return Vec::new();
752    }
753    shape
754        .attr(&Ns::Draw, "points")
755        .unwrap_or_default()
756        .split_whitespace()
757        .filter_map(|pair| {
758            let (x, y) = pair.split_once(',')?;
759            let x: f32 = x.trim().parse().ok()?;
760            let y: f32 = y.trim().parse().ok()?;
761            Some(placement.across((x - left) / width, (y - top) / height))
762        })
763        .collect()
764}
765
766/// An ellipse inscribed in a shape's box, as points.
767///
768/// For the shape whose box is turned: the window draws an ellipse from a centre
769/// and two radii, which can only be square to the screen.
770fn ellipse(placement: Placement) -> Vec<Pos2> {
771    const SIDES: usize = 64;
772    (0..SIDES)
773        .map(|i| {
774            #[allow(clippy::cast_precision_loss)]
775            let angle = std::f32::consts::TAU * i as f32 / SIDES as f32;
776            placement.across(
777                0.5f32.mul_add(angle.cos(), 0.5),
778                0.5f32.mul_add(angle.sin(), 0.5),
779            )
780        })
781        .collect()
782}
783
784fn alpha(colour: Color32, opacity: Option<f32>) -> Color32 {
785    match opacity {
786        Some(opacity) if opacity < 1.0 => colour.gamma_multiply(opacity),
787        _ => colour,
788    }
789}
790
791fn blend(from: Color, to: Color, t: f32) -> Color32 {
792    let mix = |a: u8, b: u8| {
793        let a = f32::from(a);
794        let b = f32::from(b);
795        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
796        {
797            (a + (b - a) * t).round().clamp(0.0, 255.0) as u8
798        }
799    };
800    Color32::from_rgb(mix(from.r, to.r), mix(from.g, to.g), mix(from.b, to.b))
801}
802
803/// The colour a gradient has at one point of the rectangle it fills.
804///
805/// **Linear and axial run in the direction the document gives; the four that
806/// radiate from a point do not.** A radial gradient's colour depends on the
807/// distance from a centre, which this could compute — and no fixture uses one,
808/// so it would be a direction invented rather than measured. Those get the flat
809/// average of the two colours, which is visibly an approximation.
810fn gradient_colour(point: Pos2, rect: Rect, gradient: &Gradient) -> Color32 {
811    match gradient.style {
812        GradientStyle::Linear | GradientStyle::Axial => {}
813        _ => return blend(gradient.start, gradient.end, 0.5),
814    }
815
816    // ODF measures the angle counter-clockwise from the direction that runs
817    // bottom to top, and the screen's y grows downward, so the axis is the unit
818    // vector below. A point's place along the gradient is its projection onto
819    // it, rescaled so that the rectangle's own extent is nought to one.
820    let radians = gradient.angle.to_radians();
821    let axis = vec2(radians.sin(), -radians.cos());
822    let corners = [
823        rect.left_top(),
824        rect.right_top(),
825        rect.right_bottom(),
826        rect.left_bottom(),
827    ];
828    let projections = corners.map(|corner| (corner - rect.center()).dot(axis));
829    let low = projections.iter().copied().fold(f32::MAX, f32::min);
830    let high = projections.iter().copied().fold(f32::MIN, f32::max);
831    let span = (high - low).max(f32::EPSILON);
832
833    let mut t = ((point - rect.center()).dot(axis) - low) / span;
834    // The border is the fraction of the run that stays the start colour before
835    // the blend begins.
836    let border = gradient.border.clamp(0.0, 0.99);
837    t = ((t - border) / (1.0 - border)).clamp(0.0, 1.0);
838    // An axial gradient runs out from the middle to both edges, so each half of
839    // the rectangle takes the whole blend.
840    if gradient.style == GradientStyle::Axial {
841        t = (t - 0.5).abs() * 2.0;
842    }
843    blend(gradient.start, gradient.end, t)
844}
845
846/// Cut a closed outline into triangles, by clipping ears.
847///
848/// The standard method, and the reason for it is above [`Canvas::fill`]: the
849/// cheap alternative is right only for convex outlines and ODF's shapes are
850/// routinely not. An outline it cannot cut — one that crosses itself, which a
851/// hand-edited document can hold — falls back to the fan, which is wrong in the
852/// way the fan is always wrong rather than in a new way.
853fn triangulate(points: &[Pos2]) -> Vec<[u32; 3]> {
854    let count = points.len();
855    if count < 3 {
856        return Vec::new();
857    }
858    let fan = || -> Vec<[u32; 3]> {
859        (1..count - 1)
860            .map(|i| {
861                [
862                    0,
863                    u32::try_from(i).unwrap_or(0),
864                    u32::try_from(i + 1).unwrap_or(0),
865                ]
866            })
867            .collect()
868    };
869
870    // Twice the signed area, whose sign is which way round the outline goes.
871    let area: f32 = (0..count)
872        .map(|i| {
873            let (a, b) = (points[i], points[(i + 1) % count]);
874            a.x * b.y - b.x * a.y
875        })
876        .sum();
877    let winding = if area >= 0.0 { 1.0 } else { -1.0 };
878
879    let cross = |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
880    let inside = |a: Pos2, b: Pos2, c: Pos2, p: Pos2| {
881        cross(a, b, p) * winding >= 0.0
882            && cross(b, c, p) * winding >= 0.0
883            && cross(c, a, p) * winding >= 0.0
884    };
885
886    let mut remaining: Vec<usize> = (0..count).collect();
887    let mut triangles = Vec::with_capacity(count);
888    let mut stuck = 0;
889    while remaining.len() > 3 {
890        if stuck > remaining.len() {
891            return fan();
892        }
893        let mut clipped = false;
894        for position in 0..remaining.len() {
895            let corner = [
896                remaining[(position + remaining.len() - 1) % remaining.len()],
897                remaining[position],
898                remaining[(position + 1) % remaining.len()],
899            ];
900            let ear = corner.map(|index| points[index]);
901            // A reflex corner is not an ear, and neither is one whose triangle
902            // has another corner of the outline inside it.
903            if cross(ear[0], ear[1], ear[2]) * winding <= 0.0 {
904                continue;
905            }
906            if remaining
907                .iter()
908                .filter(|other| !corner.contains(other))
909                .any(|other| inside(ear[0], ear[1], ear[2], points[*other]))
910            {
911                continue;
912            }
913            triangles.push(corner.map(|index| u32::try_from(index).unwrap_or(0)));
914            remaining.remove(position);
915            clipped = true;
916            stuck = 0;
917            break;
918        }
919        if !clipped {
920            stuck += 1;
921        }
922    }
923    if remaining.len() == 3 {
924        triangles.push([
925            u32::try_from(remaining[0]).unwrap_or(0),
926            u32::try_from(remaining[1]).unwrap_or(0),
927            u32::try_from(remaining[2]).unwrap_or(0),
928        ]);
929    }
930    triangles
931}
932
933#[cfg(test)]
934mod tests {
935    use super::triangulate;
936    use eframe::egui::{Pos2, pos2};
937
938    /// Twice the area a run of triangles covers, and twice the area the outline
939    /// encloses. Equal means the triangles cover the shape and nothing else.
940    fn areas(points: &[Pos2]) -> (f32, f32) {
941        let cross =
942            |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
943        let triangles: f32 = triangulate(points)
944            .iter()
945            .map(|[a, b, c]| {
946                cross(
947                    points[*a as usize],
948                    points[*b as usize],
949                    points[*c as usize],
950                )
951                .abs()
952            })
953            .sum();
954        let outline: f32 = (0..points.len())
955            .map(|i| {
956                let (a, b) = (points[i], points[(i + 1) % points.len()]);
957                a.x * b.y - b.x * a.y
958            })
959            .sum::<f32>()
960            .abs();
961        (triangles, outline)
962    }
963
964    /// Would catch the fan: an L covers three quarters of its bounding box, and
965    /// a fan from the first corner covers the whole of it.
966    #[test]
967    fn a_concave_outline_is_cut_into_the_shape_and_not_its_hull() {
968        let l = [
969            pos2(0.0, 0.0),
970            pos2(2.0, 0.0),
971            pos2(2.0, 1.0),
972            pos2(1.0, 1.0),
973            pos2(1.0, 2.0),
974            pos2(0.0, 2.0),
975        ];
976        let (triangles, outline) = areas(&l);
977        assert!(
978            (triangles - outline).abs() < 1e-3,
979            "{triangles} against {outline}"
980        );
981        // Three of the four unit squares, twice over.
982        assert!((outline - 6.0).abs() < 1e-3, "{outline}");
983    }
984
985    /// A cross has four reflex corners and is where a careless ear test fails.
986    #[test]
987    fn a_cross_is_cut_correctly_too() {
988        let cross = [
989            pos2(1.0, 0.0),
990            pos2(2.0, 0.0),
991            pos2(2.0, 1.0),
992            pos2(3.0, 1.0),
993            pos2(3.0, 2.0),
994            pos2(2.0, 2.0),
995            pos2(2.0, 3.0),
996            pos2(1.0, 3.0),
997            pos2(1.0, 2.0),
998            pos2(0.0, 2.0),
999            pos2(0.0, 1.0),
1000            pos2(1.0, 1.0),
1001        ];
1002        let (triangles, outline) = areas(&cross);
1003        assert!(
1004            (triangles - outline).abs() < 1e-3,
1005            "{triangles} against {outline}"
1006        );
1007    }
1008
1009    /// The same outline the other way round: the winding must not decide whether
1010    /// it works, because a mirrored shape arrives reversed.
1011    #[test]
1012    fn winding_does_not_matter() {
1013        let mut l = vec![
1014            pos2(0.0, 0.0),
1015            pos2(2.0, 0.0),
1016            pos2(2.0, 1.0),
1017            pos2(1.0, 1.0),
1018            pos2(1.0, 2.0),
1019            pos2(0.0, 2.0),
1020        ];
1021        l.reverse();
1022        let (triangles, outline) = areas(&l);
1023        assert!(
1024            (triangles - outline).abs() < 1e-3,
1025            "{triangles} against {outline}"
1026        );
1027    }
1028
1029    /// A convex outline is the ordinary case and must still come out whole.
1030    #[test]
1031    fn a_square_is_two_triangles() {
1032        let square = [
1033            pos2(0.0, 0.0),
1034            pos2(1.0, 0.0),
1035            pos2(1.0, 1.0),
1036            pos2(0.0, 1.0),
1037        ];
1038        assert_eq!(triangulate(&square).len(), 2);
1039        let (triangles, outline) = areas(&square);
1040        assert!((triangles - outline).abs() < 1e-4);
1041    }
1042}