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::flow_model::PageEditor;
30use crate::format::{self, Palette};
31
32pub struct Canvas<'a> {
34 pub document: &'a Document,
36 pub pictures: &'a mut Pictures,
38 pub page: Rect,
40 pub scale: f32,
42 pub palette: Palette,
44 pub page_editor: Option<&'a mut PageEditor>,
48 at: Option<usize>,
52}
53
54#[derive(Clone, Copy)]
62struct Placement {
63 origin: Pos2,
65 x: Vec2,
67 y: Vec2,
69}
70
71impl Placement {
72 fn across(self, u: f32, v: f32) -> Pos2 {
74 self.origin + self.x * u + self.y * v
75 }
76
77 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 fn bounds(self) -> Rect {
90 Rect::from_points(&self.corners())
91 }
92
93 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#[derive(Clone, Copy, PartialEq, Eq)]
103enum Filled {
104 Yes,
105 No,
106}
107
108impl<'a> Canvas<'a> {
109 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 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 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 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 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 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 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 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 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 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 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 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 fn text(&mut self, ui: &mut Ui, shape: &Element, place: Placement) {
407 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 (shape.clone(), None)
429 } else {
430 return;
431 };
432 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 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 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 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 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 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 fn painter(&self, ui: &Ui) -> eframe::egui::Painter {
565 ui.painter()
566 .with_clip_rect(self.page.intersect(ui.clip_rect()))
567 }
568
569 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 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 fn fill(&mut self, ui: &Ui, rect: Rect, fill: &Fill, opacity: Option<f32>, points: &[Pos2]) {
642 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 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 fn flat(&self, fill: &Fill, opacity: Option<f32>) -> Option<Color32> {
708 match fill {
709 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
721fn 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
754fn 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
791fn 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 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 let border = gradient.border.clamp(0.0, 0.99);
825 t = ((t - border) / (1.0 - border)).clamp(0.0, 1.0);
826 if gradient.style == GradientStyle::Axial {
829 t = (t - 0.5).abs() * 2.0;
830 }
831 blend(gradient.start, gradient.end, t)
832}
833
834fn 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 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 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 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 #[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 assert!((outline - 6.0).abs() < 1e-3, "{outline}");
971 }
972
973 #[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 #[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 #[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}