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::flow::{Editor, Flow, Outcome, Pictures};
29use crate::format::{self, Palette};
30
31/// A page, and where on screen it is being drawn.
32pub struct Canvas<'a> {
33    /// The document, for its styles and its pictures.
34    pub document: &'a Document,
35    /// Pictures decoded so far.
36    pub pictures: &'a mut Pictures,
37    /// The rectangle the page occupies on screen.
38    pub page: Rect,
39    /// Screen points per ODF point.
40    pub scale: f32,
41    /// The colours to draw in where the document names none.
42    pub palette: Palette,
43    /// Whether a click on a label's paragraph opens it for editing.
44    pub edit_mode: bool,
45    /// The paragraph being edited, as a path from the page: the shape's index
46    /// among the page's children, then the way down to the paragraph.
47    pub editor: Option<&'a mut Editor>,
48    /// The paragraph a person clicked this frame in edit mode, as a path from
49    /// the page.
50    pub clicked: Option<Vec<usize>>,
51    /// What the editor asked for this frame, if it closed.
52    pub outcome: Option<Outcome>,
53    /// The index among the page's children of the shape being drawn, where it
54    /// is one of the slide's own; a master page's decoration has none and
55    /// nothing in it is edited.
56    at: Option<usize>,
57}
58
59/// Where a shape's own box lands on the screen.
60///
61/// A shape is drawn in a box of its own, and this is the map from that box onto
62/// the window. Ordinarily it is a corner and a size and the box's edges stay
63/// along the page's, but a shape placed by `draw:transform` is turned or leaned
64/// as well, so the map is the general one: a corner and the two vectors along
65/// the edges that meet there.
66#[derive(Clone, Copy)]
67struct Placement {
68    /// Where the box's top left corner lands.
69    origin: Pos2,
70    /// From that corner to the top right one.
71    x: Vec2,
72    /// From that corner to the bottom left one.
73    y: Vec2,
74}
75
76impl Placement {
77    /// A point of the box, in the fractions of it across and down.
78    fn across(self, u: f32, v: f32) -> Pos2 {
79        self.origin + self.x * u + self.y * v
80    }
81
82    /// The four corners, going round.
83    fn corners(self) -> [Pos2; 4] {
84        [
85            self.across(0.0, 0.0),
86            self.across(1.0, 0.0),
87            self.across(1.0, 1.0),
88            self.across(0.0, 1.0),
89        ]
90    }
91
92    /// The smallest upright rectangle the shape fits inside, which is what a
93    /// gradient runs across and where a label is laid out.
94    fn bounds(self) -> Rect {
95        Rect::from_points(&self.corners())
96    }
97
98    /// Whether the box is still square to the page, which is the case a
99    /// rectangle can stand in for.
100    fn is_upright(self) -> bool {
101        self.x.y.abs() < 0.01 && self.y.x.abs() < 0.01 && self.x.x >= 0.0 && self.y.y >= 0.0
102    }
103}
104
105/// Whether a shape has an area, which is a property of the kind of shape it is
106/// rather than of the style it names.
107#[derive(Clone, Copy, PartialEq, Eq)]
108enum Filled {
109    Yes,
110    No,
111}
112
113impl<'a> Canvas<'a> {
114    /// A canvas over a page, drawing nothing editable until told otherwise.
115    pub fn new(
116        document: &'a Document,
117        pictures: &'a mut Pictures,
118        page: Rect,
119        scale: f32,
120        palette: Palette,
121    ) -> Self {
122        Self {
123            document,
124            pictures,
125            page,
126            scale,
127            palette,
128            edit_mode: false,
129            editor: None,
130            clicked: None,
131            outcome: 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.
526        let edit_mode = self.edit_mode && prefix.is_some();
527        let editor = self.editor.as_deref_mut().filter(|_| edit_mode);
528        let mut builder = UiBuilder::new().max_rect(rect);
529        if measuring {
530            builder = builder.sizing_pass().invisible();
531        }
532        let mut clicked = None;
533        let mut outcome = None;
534        let height = ui
535            .scope_builder(builder, |ui| {
536                ui.set_clip_rect(rect.intersect(page));
537                let mut flow = Flow::new(document, pictures, scale);
538                flow.palette = palette;
539                flow.edit_mode = edit_mode;
540                flow.selectable = !edit_mode;
541                flow.editor = editor;
542                if let Some(prefix) = prefix {
543                    flow.start_at(prefix.clone());
544                }
545                if picture {
546                    flow.frame(ui, content, rect.width());
547                } else {
548                    flow.blocks(ui, content, rect.width());
549                }
550                clicked = flow.clicked.take();
551                outcome = flow.outcome.take();
552            })
553            .response
554            .rect
555            .height();
556        if clicked.is_some() {
557            self.clicked = clicked;
558        }
559        if outcome.is_some() {
560            self.outcome = outcome;
561        }
562        height
563    }
564
565    /// The style a shape names, resolved.
566    ///
567    /// A shape on a slide names a `presentation` style where it is one of the
568    /// slide's own frames and a `graphic` style where it is a drawing; a master
569    /// page's decorations are the second kind. Both chains end in the same
570    /// properties.
571    fn style_of(&self, shape: &Element) -> std::rc::Rc<Properties> {
572        if let Some(name) = shape.attr(&Ns::Presentation, "style-name") {
573            return self.document.styles.resolve(&Family::Presentation, name);
574        }
575        let name = shape.attr(&Ns::Draw, "style-name").unwrap_or_default();
576        self.document.styles.resolve(&Family::Graphic, name)
577    }
578
579    /// Everything is clipped to the page: a master page's decorations are
580    /// routinely wider than the slide they decorate.
581    fn painter(&self, ui: &Ui) -> eframe::egui::Painter {
582        ui.painter()
583            .with_clip_rect(self.page.intersect(ui.clip_rect()))
584    }
585
586    /// Where a shape's own box sits on screen.
587    ///
588    /// Ordinarily its corner and its size, which is a rectangle square to the
589    /// page. A shape that is turned or leaned states `draw:transform` instead
590    /// and routinely gives no corner at all, and a reader that insists on one
591    /// drops the shape: it is the templates' own decoration that is placed this
592    /// way. `odox_core::Transform` says how the list is read.
593    fn placement(&self, shape: &Element) -> Option<Placement> {
594        let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
595        let width = at("width").map_or(0.0, Length::points);
596        let height = at("height").map_or(0.0, Length::points);
597        let on_page = |x: f32, y: f32| {
598            pos2(
599                self.page.left() + x * self.scale,
600                self.page.top() + y * self.scale,
601            )
602        };
603
604        if let Some(transform) = shape
605            .attr(&Ns::Draw, "transform")
606            .and_then(Transform::parse)
607        {
608            // The box begins at the shape's corner where it has one, and at the
609            // page's origin where the transform is the whole of its placement.
610            let left = at("x").map_or(0.0, Length::points);
611            let top = at("y").map_or(0.0, Length::points);
612            let corner = |u: f32, v: f32| {
613                let (x, y) = transform.apply((width.mul_add(u, left), height.mul_add(v, top)));
614                on_page(x, y)
615            };
616            let origin = corner(0.0, 0.0);
617            return Some(Placement {
618                origin,
619                x: corner(1.0, 0.0) - origin,
620                y: corner(0.0, 1.0) - origin,
621            });
622        }
623
624        Some(Placement {
625            origin: on_page(at("x")?.points(), at("y")?.points()),
626            x: vec2(width * self.scale, 0.0),
627            y: vec2(0.0, height * self.scale),
628        })
629    }
630
631    fn point(&self, shape: &Element, x: &str, y: &str) -> Option<Pos2> {
632        let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
633        Some(pos2(
634            self.page.left() + at(x)?.points() * self.scale,
635            self.page.top() + at(y)?.points() * self.scale,
636        ))
637    }
638
639    fn stroke(&self, properties: &Properties) -> Option<Stroke> {
640        let colour = properties.graphic.stroke?;
641        let width = properties
642            .graphic
643            .stroke_width
644            .map_or(1.0, |w| w.points() * self.scale)
645            .max(1.0);
646        Some(Stroke::new(width, format::color32(colour)))
647    }
648
649    /// Paint a fill inside an outline.
650    ///
651    /// Always a mesh, and always triangulated. A graphics toolkit fills a closed
652    /// path by cutting it into triangles, and the obvious way — a fan from the
653    /// first point, which is what `Shape::convex_polygon` does — is right only
654    /// for a convex outline. An arrow, a callout and a puzzle piece are none of
655    /// them convex, and a fan across one paints outside it. Colour varies over a
656    /// mesh by varying at its corners, so a gradient costs nothing more than
657    /// asking for the colour at each.
658    fn fill(&mut self, ui: &Ui, rect: Rect, fill: &Fill, opacity: Option<f32>, points: &[Pos2]) {
659        // The reference is copied out so that the picture cache can be filled
660        // while the document is being read from.
661        let document = self.document;
662
663        if let Fill::Image(name) = fill {
664            let Some(href) = document.styles.fill_image(name) else {
665                return;
666            };
667            let Some(texture) = self
668                .pictures
669                .get(ui.ctx(), document, href)
670                .map(eframe::egui::TextureHandle::id)
671            else {
672                return;
673            };
674            // Stretched over the shape's own rectangle: each corner takes the
675            // corner of the picture that the corner of the rectangle is at.
676            let tint = alpha(Color32::WHITE, opacity);
677            let mut mesh = Mesh::with_texture(texture);
678            for point in points {
679                mesh.vertices.push(Vertex {
680                    pos: *point,
681                    uv: pos2(
682                        (point.x - rect.left()) / rect.width().max(f32::EPSILON),
683                        (point.y - rect.top()) / rect.height().max(f32::EPSILON),
684                    ),
685                    color: tint,
686                });
687            }
688            for [a, b, c] in triangulate(points) {
689                mesh.add_triangle(a, b, c);
690            }
691            self.painter(ui).add(Shape::mesh(mesh));
692            return;
693        }
694
695        let gradient = match fill {
696            Fill::None | Fill::Image(_) => return,
697            Fill::Solid(_) => None,
698            Fill::Gradient(name) => match document.styles.gradient(name) {
699                Some(gradient) => Some(gradient),
700                None => return,
701            },
702        };
703        let flat = match fill {
704            Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
705            _ => None,
706        };
707
708        let mut mesh = Mesh::default();
709        for point in points {
710            let colour = flat.unwrap_or_else(|| {
711                gradient.map_or(Color32::TRANSPARENT, |gradient| {
712                    alpha(gradient_colour(*point, rect, gradient), opacity)
713                })
714            });
715            mesh.colored_vertex(*point, colour);
716        }
717        for [a, b, c] in triangulate(points) {
718            mesh.add_triangle(a, b, c);
719        }
720        self.painter(ui).add(Shape::mesh(mesh));
721    }
722
723    /// One colour for a fill, where the shape being drawn cannot carry a mesh.
724    fn flat(&self, fill: &Fill, opacity: Option<f32>) -> Option<Color32> {
725        match fill {
726            // Nothing to draw, and a picture that has no room in an ellipse,
727            // which is drawn as an ellipse rather than as a mesh.
728            Fill::None | Fill::Image(_) => None,
729            Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
730            Fill::Gradient(name) => {
731                let gradient = self.document.styles.gradient(name)?;
732                Some(alpha(blend(gradient.start, gradient.end, 0.5), opacity))
733            }
734        }
735    }
736}
737
738/// A polygon's points, mapped from the coordinate space it declares onto the
739/// rectangle it occupies.
740///
741/// `draw:points` is in the space `svg:viewBox` sets up, which is a shape's
742/// own and has nothing to do with the page's: a polygon 13.5cm wide states
743/// its points out of 13501. Without the mapping every polygon collapses into
744/// the top left corner.
745fn points(shape: &Element, placement: Placement) -> Vec<Pos2> {
746    let view: Vec<f32> = shape
747        .attr(&Ns::Svg, "viewBox")
748        .unwrap_or_default()
749        .split_whitespace()
750        .filter_map(|n| n.parse().ok())
751        .collect();
752    let [left, top, width, height] = view[..] else {
753        return Vec::new();
754    };
755    if width <= 0.0 || height <= 0.0 {
756        return Vec::new();
757    }
758    shape
759        .attr(&Ns::Draw, "points")
760        .unwrap_or_default()
761        .split_whitespace()
762        .filter_map(|pair| {
763            let (x, y) = pair.split_once(',')?;
764            let x: f32 = x.trim().parse().ok()?;
765            let y: f32 = y.trim().parse().ok()?;
766            Some(placement.across((x - left) / width, (y - top) / height))
767        })
768        .collect()
769}
770
771/// An ellipse inscribed in a shape's box, as points.
772///
773/// For the shape whose box is turned: the window draws an ellipse from a centre
774/// and two radii, which can only be square to the screen.
775fn ellipse(placement: Placement) -> Vec<Pos2> {
776    const SIDES: usize = 64;
777    (0..SIDES)
778        .map(|i| {
779            #[allow(clippy::cast_precision_loss)]
780            let angle = std::f32::consts::TAU * i as f32 / SIDES as f32;
781            placement.across(
782                0.5f32.mul_add(angle.cos(), 0.5),
783                0.5f32.mul_add(angle.sin(), 0.5),
784            )
785        })
786        .collect()
787}
788
789fn alpha(colour: Color32, opacity: Option<f32>) -> Color32 {
790    match opacity {
791        Some(opacity) if opacity < 1.0 => colour.gamma_multiply(opacity),
792        _ => colour,
793    }
794}
795
796fn blend(from: Color, to: Color, t: f32) -> Color32 {
797    let mix = |a: u8, b: u8| {
798        let a = f32::from(a);
799        let b = f32::from(b);
800        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
801        {
802            (a + (b - a) * t).round().clamp(0.0, 255.0) as u8
803        }
804    };
805    Color32::from_rgb(mix(from.r, to.r), mix(from.g, to.g), mix(from.b, to.b))
806}
807
808/// The colour a gradient has at one point of the rectangle it fills.
809///
810/// **Linear and axial run in the direction the document gives; the four that
811/// radiate from a point do not.** A radial gradient's colour depends on the
812/// distance from a centre, which this could compute — and no fixture uses one,
813/// so it would be a direction invented rather than measured. Those get the flat
814/// average of the two colours, which is visibly an approximation.
815fn gradient_colour(point: Pos2, rect: Rect, gradient: &Gradient) -> Color32 {
816    match gradient.style {
817        GradientStyle::Linear | GradientStyle::Axial => {}
818        _ => return blend(gradient.start, gradient.end, 0.5),
819    }
820
821    // ODF measures the angle counter-clockwise from the direction that runs
822    // bottom to top, and the screen's y grows downward, so the axis is the unit
823    // vector below. A point's place along the gradient is its projection onto
824    // it, rescaled so that the rectangle's own extent is nought to one.
825    let radians = gradient.angle.to_radians();
826    let axis = vec2(radians.sin(), -radians.cos());
827    let corners = [
828        rect.left_top(),
829        rect.right_top(),
830        rect.right_bottom(),
831        rect.left_bottom(),
832    ];
833    let projections = corners.map(|corner| (corner - rect.center()).dot(axis));
834    let low = projections.iter().copied().fold(f32::MAX, f32::min);
835    let high = projections.iter().copied().fold(f32::MIN, f32::max);
836    let span = (high - low).max(f32::EPSILON);
837
838    let mut t = ((point - rect.center()).dot(axis) - low) / span;
839    // The border is the fraction of the run that stays the start colour before
840    // the blend begins.
841    let border = gradient.border.clamp(0.0, 0.99);
842    t = ((t - border) / (1.0 - border)).clamp(0.0, 1.0);
843    // An axial gradient runs out from the middle to both edges, so each half of
844    // the rectangle takes the whole blend.
845    if gradient.style == GradientStyle::Axial {
846        t = (t - 0.5).abs() * 2.0;
847    }
848    blend(gradient.start, gradient.end, t)
849}
850
851/// Cut a closed outline into triangles, by clipping ears.
852///
853/// The standard method, and the reason for it is above [`Canvas::fill`]: the
854/// cheap alternative is right only for convex outlines and ODF's shapes are
855/// routinely not. An outline it cannot cut — one that crosses itself, which a
856/// hand-edited document can hold — falls back to the fan, which is wrong in the
857/// way the fan is always wrong rather than in a new way.
858fn triangulate(points: &[Pos2]) -> Vec<[u32; 3]> {
859    let count = points.len();
860    if count < 3 {
861        return Vec::new();
862    }
863    let fan = || -> Vec<[u32; 3]> {
864        (1..count - 1)
865            .map(|i| {
866                [
867                    0,
868                    u32::try_from(i).unwrap_or(0),
869                    u32::try_from(i + 1).unwrap_or(0),
870                ]
871            })
872            .collect()
873    };
874
875    // Twice the signed area, whose sign is which way round the outline goes.
876    let area: f32 = (0..count)
877        .map(|i| {
878            let (a, b) = (points[i], points[(i + 1) % count]);
879            a.x * b.y - b.x * a.y
880        })
881        .sum();
882    let winding = if area >= 0.0 { 1.0 } else { -1.0 };
883
884    let cross = |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
885    let inside = |a: Pos2, b: Pos2, c: Pos2, p: Pos2| {
886        cross(a, b, p) * winding >= 0.0
887            && cross(b, c, p) * winding >= 0.0
888            && cross(c, a, p) * winding >= 0.0
889    };
890
891    let mut remaining: Vec<usize> = (0..count).collect();
892    let mut triangles = Vec::with_capacity(count);
893    let mut stuck = 0;
894    while remaining.len() > 3 {
895        if stuck > remaining.len() {
896            return fan();
897        }
898        let mut clipped = false;
899        for position in 0..remaining.len() {
900            let corner = [
901                remaining[(position + remaining.len() - 1) % remaining.len()],
902                remaining[position],
903                remaining[(position + 1) % remaining.len()],
904            ];
905            let ear = corner.map(|index| points[index]);
906            // A reflex corner is not an ear, and neither is one whose triangle
907            // has another corner of the outline inside it.
908            if cross(ear[0], ear[1], ear[2]) * winding <= 0.0 {
909                continue;
910            }
911            if remaining
912                .iter()
913                .filter(|other| !corner.contains(other))
914                .any(|other| inside(ear[0], ear[1], ear[2], points[*other]))
915            {
916                continue;
917            }
918            triangles.push(corner.map(|index| u32::try_from(index).unwrap_or(0)));
919            remaining.remove(position);
920            clipped = true;
921            stuck = 0;
922            break;
923        }
924        if !clipped {
925            stuck += 1;
926        }
927    }
928    if remaining.len() == 3 {
929        triangles.push([
930            u32::try_from(remaining[0]).unwrap_or(0),
931            u32::try_from(remaining[1]).unwrap_or(0),
932            u32::try_from(remaining[2]).unwrap_or(0),
933        ]);
934    }
935    triangles
936}
937
938#[cfg(test)]
939mod tests {
940    use super::triangulate;
941    use eframe::egui::{Pos2, pos2};
942
943    /// Twice the area a run of triangles covers, and twice the area the outline
944    /// encloses. Equal means the triangles cover the shape and nothing else.
945    fn areas(points: &[Pos2]) -> (f32, f32) {
946        let cross =
947            |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
948        let triangles: f32 = triangulate(points)
949            .iter()
950            .map(|[a, b, c]| {
951                cross(
952                    points[*a as usize],
953                    points[*b as usize],
954                    points[*c as usize],
955                )
956                .abs()
957            })
958            .sum();
959        let outline: f32 = (0..points.len())
960            .map(|i| {
961                let (a, b) = (points[i], points[(i + 1) % points.len()]);
962                a.x * b.y - b.x * a.y
963            })
964            .sum::<f32>()
965            .abs();
966        (triangles, outline)
967    }
968
969    /// Would catch the fan: an L covers three quarters of its bounding box, and
970    /// a fan from the first corner covers the whole of it.
971    #[test]
972    fn a_concave_outline_is_cut_into_the_shape_and_not_its_hull() {
973        let l = [
974            pos2(0.0, 0.0),
975            pos2(2.0, 0.0),
976            pos2(2.0, 1.0),
977            pos2(1.0, 1.0),
978            pos2(1.0, 2.0),
979            pos2(0.0, 2.0),
980        ];
981        let (triangles, outline) = areas(&l);
982        assert!(
983            (triangles - outline).abs() < 1e-3,
984            "{triangles} against {outline}"
985        );
986        // Three of the four unit squares, twice over.
987        assert!((outline - 6.0).abs() < 1e-3, "{outline}");
988    }
989
990    /// A cross has four reflex corners and is where a careless ear test fails.
991    #[test]
992    fn a_cross_is_cut_correctly_too() {
993        let cross = [
994            pos2(1.0, 0.0),
995            pos2(2.0, 0.0),
996            pos2(2.0, 1.0),
997            pos2(3.0, 1.0),
998            pos2(3.0, 2.0),
999            pos2(2.0, 2.0),
1000            pos2(2.0, 3.0),
1001            pos2(1.0, 3.0),
1002            pos2(1.0, 2.0),
1003            pos2(0.0, 2.0),
1004            pos2(0.0, 1.0),
1005            pos2(1.0, 1.0),
1006        ];
1007        let (triangles, outline) = areas(&cross);
1008        assert!(
1009            (triangles - outline).abs() < 1e-3,
1010            "{triangles} against {outline}"
1011        );
1012    }
1013
1014    /// The same outline the other way round: the winding must not decide whether
1015    /// it works, because a mirrored shape arrives reversed.
1016    #[test]
1017    fn winding_does_not_matter() {
1018        let mut l = vec![
1019            pos2(0.0, 0.0),
1020            pos2(2.0, 0.0),
1021            pos2(2.0, 1.0),
1022            pos2(1.0, 1.0),
1023            pos2(1.0, 2.0),
1024            pos2(0.0, 2.0),
1025        ];
1026        l.reverse();
1027        let (triangles, outline) = areas(&l);
1028        assert!(
1029            (triangles - outline).abs() < 1e-3,
1030            "{triangles} against {outline}"
1031        );
1032    }
1033
1034    /// A convex outline is the ordinary case and must still come out whole.
1035    #[test]
1036    fn a_square_is_two_triangles() {
1037        let square = [
1038            pos2(0.0, 0.0),
1039            pos2(1.0, 0.0),
1040            pos2(1.0, 1.0),
1041            pos2(0.0, 1.0),
1042        ];
1043        assert_eq!(triangulate(&square).len(), 2);
1044        let (triangles, outline) = areas(&square);
1045        assert!((triangles - outline).abs() < 1e-4);
1046    }
1047}