1use 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
31pub struct Canvas<'a> {
33 pub document: &'a Document,
35 pub pictures: &'a mut Pictures,
37 pub page: Rect,
39 pub scale: f32,
41 pub palette: Palette,
43}
44
45#[derive(Clone, Copy)]
53struct Placement {
54 origin: Pos2,
56 x: Vec2,
58 y: Vec2,
60}
61
62impl Placement {
63 fn across(self, u: f32, v: f32) -> Pos2 {
65 self.origin + self.x * u + self.y * v
66 }
67
68 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 fn bounds(self) -> Rect {
81 Rect::from_points(&self.corners())
82 }
83
84 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#[derive(Clone, Copy, PartialEq, Eq)]
94enum Filled {
95 Yes,
96 No,
97}
98
99impl Canvas<'_> {
100 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 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 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 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 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 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 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 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 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 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 fn text(&mut self, ui: &mut Ui, shape: &Element, place: Placement) {
369 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 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 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 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 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 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 fn painter(&self, ui: &Ui) -> eframe::egui::Painter {
505 ui.painter()
506 .with_clip_rect(self.page.intersect(ui.clip_rect()))
507 }
508
509 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 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 fn fill(&mut self, ui: &Ui, rect: Rect, fill: &Fill, opacity: Option<f32>, points: &[Pos2]) {
582 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 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 fn flat(&self, fill: &Fill, opacity: Option<f32>) -> Option<Color32> {
648 match fill {
649 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
661fn 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
694fn 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
731fn 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 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 let border = gradient.border.clamp(0.0, 0.99);
765 t = ((t - border) / (1.0 - border)).clamp(0.0, 1.0);
766 if gradient.style == GradientStyle::Axial {
769 t = (t - 0.5).abs() * 2.0;
770 }
771 blend(gradient.start, gradient.end, t)
772}
773
774fn 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 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 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 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 #[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 assert!((outline - 6.0).abs() < 1e-3, "{outline}");
911 }
912
913 #[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 #[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 #[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}