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