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