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::{Editor, Flow, Outcome, 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 pub edit_mode: bool,
45 pub editor: Option<&'a mut Editor>,
48 pub clicked: Option<Vec<usize>>,
51 pub outcome: Option<Outcome>,
53 at: Option<usize>,
57}
58
59#[derive(Clone, Copy)]
67struct Placement {
68 origin: Pos2,
70 x: Vec2,
72 y: Vec2,
74}
75
76impl Placement {
77 fn across(self, u: f32, v: f32) -> Pos2 {
79 self.origin + self.x * u + self.y * v
80 }
81
82 fn corners(self) -> [Pos2; 4] {
84 [
85 self.across(0.0, 0.0),
86 self.across(1.0, 0.0),
87 self.across(1.0, 1.0),
88 self.across(0.0, 1.0),
89 ]
90 }
91
92 fn bounds(self) -> Rect {
95 Rect::from_points(&self.corners())
96 }
97
98 fn is_upright(self) -> bool {
101 self.x.y.abs() < 0.01 && self.y.x.abs() < 0.01 && self.x.x >= 0.0 && self.y.y >= 0.0
102 }
103}
104
105#[derive(Clone, Copy, PartialEq, Eq)]
108enum Filled {
109 Yes,
110 No,
111}
112
113impl<'a> Canvas<'a> {
114 pub fn new(
116 document: &'a Document,
117 pictures: &'a mut Pictures,
118 page: Rect,
119 scale: f32,
120 palette: Palette,
121 ) -> Self {
122 Self {
123 document,
124 pictures,
125 page,
126 scale,
127 palette,
128 edit_mode: false,
129 editor: None,
130 clicked: None,
131 outcome: None,
132 at: None,
133 }
134 }
135
136 pub fn slide_shape(&mut self, ui: &mut Ui, index: usize, shape: &Element) {
139 self.at = Some(index);
140 self.shape(ui, shape);
141 self.at = None;
142 }
143}
144
145impl Canvas<'_> {
146 pub fn background(&mut self, ui: &Ui, fill: &Fill) {
148 let page = self.page;
149 self.fill(
150 ui,
151 page,
152 fill,
153 None,
154 &[
155 page.left_top(),
156 page.right_top(),
157 page.right_bottom(),
158 page.left_bottom(),
159 ],
160 );
161 }
162
163 pub fn shape(&mut self, ui: &mut Ui, shape: &Element) {
173 if shape.is(&Ns::Draw, "g") {
174 for child in shape.elements() {
175 self.shape(ui, child);
176 }
177 return;
178 }
179
180 let properties = self.style_of(shape);
181 let outline = self.stroke(&properties);
182
183 if shape.is(&Ns::Draw, "line") {
186 let ends = [self.point(shape, "x1", "y1"), self.point(shape, "x2", "y2")];
187 if let ([Some(from), Some(to)], Some(stroke)) = ([ends[0], ends[1]], outline) {
188 self.painter(ui)
189 .add(Shape::line_segment([from, to], stroke));
190 }
191 return;
192 }
193
194 if shape.is(&Ns::Draw, "connector") {
195 self.connector(ui, shape, &properties, outline);
196 return;
197 }
198
199 let Some(place) = self.placement(shape) else {
200 return;
201 };
202 let rect = place.bounds();
203
204 if shape.is(&Ns::Draw, "polygon") || shape.is(&Ns::Draw, "polyline") {
205 let points = points(shape, place);
206 if points.len() >= 2 {
207 if shape.is(&Ns::Draw, "polygon") {
208 self.fill(
209 ui,
210 rect,
211 &properties.graphic.fill(),
212 properties.graphic.opacity,
213 &points,
214 );
215 if let Some(stroke) = outline {
216 self.painter(ui).add(Shape::closed_line(points, stroke));
217 }
218 } else if let Some(stroke) = outline {
219 self.painter(ui).add(Shape::line(points, stroke));
220 }
221 }
222 return;
223 }
224
225 if shape.is(&Ns::Draw, "ellipse") || shape.is(&Ns::Draw, "circle") {
226 self.ellipse(ui, place, &properties, outline);
227 self.text(ui, shape, place);
228 return;
229 }
230
231 if shape.is(&Ns::Draw, "path") {
232 if let Some(geometry) = Geometry::read_path(shape) {
235 self.geometry(ui, &geometry, place, &properties, outline, Filled::Yes);
236 }
237 self.text(ui, shape, place);
238 return;
239 }
240
241 if shape.is(&Ns::Draw, "custom-shape") {
242 if let Some(geometry) = shape
245 .child(&Ns::Draw, "enhanced-geometry")
246 .and_then(Geometry::read)
247 {
248 self.geometry(ui, &geometry, place, &properties, outline, Filled::Yes);
249 }
250 self.text(ui, shape, place);
251 return;
252 }
253
254 if !shape.is(&Ns::Draw, "rect") && !shape.is(&Ns::Draw, "frame") {
255 return;
256 }
257
258 let corners = place.corners();
259 self.fill(
260 ui,
261 rect,
262 &properties.graphic.fill(),
263 properties.graphic.opacity,
264 &corners,
265 );
266 if let Some(stroke) = outline {
267 self.painter(ui)
268 .add(Shape::closed_line(corners.to_vec(), stroke));
269 }
270 self.text(ui, shape, place);
271 }
272
273 fn ellipse(
275 &mut self,
276 ui: &Ui,
277 place: Placement,
278 properties: &Properties,
279 outline: Option<Stroke>,
280 ) {
281 let rect = place.bounds();
282 if place.is_upright() {
283 let (centre, radius) = (rect.center(), rect.size() / 2.0);
284 if let Some(colour) = self.flat(&properties.graphic.fill(), properties.graphic.opacity)
285 {
286 self.painter(ui)
287 .add(Shape::ellipse_filled(centre, radius, colour));
288 }
289 if let Some(stroke) = outline {
290 self.painter(ui)
291 .add(Shape::ellipse_stroke(centre, radius, stroke));
292 }
293 } else {
294 let points = ellipse(place);
298 self.fill(
299 ui,
300 rect,
301 &properties.graphic.fill(),
302 properties.graphic.opacity,
303 &points,
304 );
305 if let Some(stroke) = outline {
306 self.painter(ui).add(Shape::closed_line(points, stroke));
307 }
308 }
309 }
310
311 fn connector(
318 &mut self,
319 ui: &mut Ui,
320 shape: &Element,
321 properties: &Properties,
322 outline: Option<Stroke>,
323 ) {
324 let (Some(from), Some(to)) = (self.point(shape, "x1", "y1"), self.point(shape, "x2", "y2"))
325 else {
326 return;
327 };
328 let rect = Rect::from_two_pos(from, to);
329 match Geometry::read_path(shape) {
330 Some(mut geometry) => {
333 geometry.refit();
334 let place = Placement {
335 origin: rect.left_top(),
336 x: vec2(rect.width(), 0.0),
337 y: vec2(0.0, rect.height()),
338 };
339 self.geometry(ui, &geometry, place, properties, outline, Filled::No);
340 }
341 None => {
342 if let Some(stroke) = outline {
343 self.painter(ui)
344 .add(Shape::line_segment([from, to], stroke));
345 }
346 }
347 }
348 self.text(
349 ui,
350 shape,
351 Placement {
352 origin: rect.left_top(),
353 x: vec2(rect.width(), 0.0),
354 y: vec2(0.0, rect.height()),
355 },
356 );
357 }
358
359 fn geometry(
367 &mut self,
368 ui: &Ui,
369 geometry: &Geometry,
370 placement: Placement,
371 properties: &Properties,
372 outline: Option<Stroke>,
373 filled: Filled,
374 ) {
375 let view = geometry.view;
376 let rect = placement.bounds();
377 let place = |(x, y): (f32, f32)| {
378 placement.across((x - view.x) / view.width, (y - view.y) / view.height)
379 };
380 for stroke in &geometry.paths {
381 let points: Vec<Pos2> = stroke.points.iter().copied().map(place).collect();
382 if points.len() < 2 {
383 continue;
384 }
385 if stroke.fill && filled == Filled::Yes {
386 self.fill(
387 ui,
388 rect,
389 &properties.graphic.fill(),
390 properties.graphic.opacity,
391 &points,
392 );
393 }
394 if stroke.stroke
395 && let Some(pen) = outline
396 {
397 let shape = if stroke.closed {
398 Shape::closed_line(points, pen)
399 } else {
400 Shape::line(points, pen)
401 };
402 self.painter(ui).add(shape);
403 }
404 }
405 }
406
407 fn text(&mut self, ui: &mut Ui, shape: &Element, place: Placement) {
415 let picture = shape.child(&Ns::Draw, "image").is_some();
419 if picture && !place.is_upright() {
420 self.turned_picture(ui, shape, place);
421 return;
422 }
423 let text_box = shape
424 .elements_indexed()
425 .find(|(_, e)| e.is(&Ns::Draw, "text-box"));
426 let (content, below) = if picture {
427 (shape.clone(), None)
428 } else if let Some((index, box_)) = text_box {
429 (box_.clone(), Some(index))
430 } else if shape.child(&Ns::Text, "p").is_some() || shape.child(&Ns::Text, "list").is_some()
431 {
432 (shape.clone(), None)
437 } else {
438 return;
439 };
440 let prefix = self.at.map(|at| match below {
442 Some(index) => vec![at, index],
443 None => vec![at],
444 });
445 if !place.is_upright() {
446 return;
447 }
448 let rect = place.bounds();
449
450 let anchor = if picture {
451 Anchor::Top
452 } else {
453 self.style_of(shape)
454 .graphic
455 .text_anchor
456 .unwrap_or(Anchor::Top)
457 };
458 let mut top = rect.top();
459 if anchor != Anchor::Top {
460 let sized = self.lay_out(ui, &content, rect, picture, true, prefix.as_ref());
466 top += (rect.height() - sized.min(rect.height())) * anchor.share();
467 }
468 let placed = Rect::from_min_max(pos2(rect.left(), top), rect.max);
469 self.lay_out(ui, &content, placed, picture, false, prefix.as_ref());
470 }
471
472 fn turned_picture(&mut self, ui: &Ui, shape: &Element, place: Placement) {
479 let document = self.document;
480 let Some(texture) = shape
481 .elements()
482 .filter(|child| child.is(&Ns::Draw, "image"))
483 .find_map(|image| {
484 let href = image.attr(&Ns::Xlink, "href")?;
485 self.pictures
486 .get(ui.ctx(), document, href)
487 .map(eframe::egui::TextureHandle::id)
488 })
489 else {
490 return;
491 };
492 let mut mesh = Mesh::with_texture(texture);
493 for (corner, (u, v)) in
494 place
495 .corners()
496 .into_iter()
497 .zip([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)])
498 {
499 mesh.vertices.push(Vertex {
500 pos: corner,
501 uv: pos2(u, v),
502 color: Color32::WHITE,
503 });
504 }
505 mesh.add_triangle(0, 1, 2);
506 mesh.add_triangle(0, 2, 3);
507 self.painter(ui).add(Shape::mesh(mesh));
508 }
509
510 fn lay_out(
513 &mut self,
514 ui: &mut Ui,
515 content: &Element,
516 rect: Rect,
517 picture: bool,
518 measuring: bool,
519 prefix: Option<&Vec<usize>>,
520 ) -> f32 {
521 let (page, scale, palette) = (self.page, self.scale, self.palette);
522 let document = self.document;
523 let pictures = &mut *self.pictures;
524 let edit_mode = self.edit_mode && prefix.is_some();
527 let editor = self.editor.as_deref_mut().filter(|_| edit_mode);
528 let mut builder = UiBuilder::new().max_rect(rect);
529 if measuring {
530 builder = builder.sizing_pass().invisible();
531 }
532 let mut clicked = None;
533 let mut outcome = None;
534 let height = ui
535 .scope_builder(builder, |ui| {
536 ui.set_clip_rect(rect.intersect(page));
537 let mut flow = Flow::new(document, pictures, scale);
538 flow.palette = palette;
539 flow.edit_mode = edit_mode;
540 flow.selectable = !edit_mode;
541 flow.editor = editor;
542 if let Some(prefix) = prefix {
543 flow.start_at(prefix.clone());
544 }
545 if picture {
546 flow.frame(ui, content, rect.width());
547 } else {
548 flow.blocks(ui, content, rect.width());
549 }
550 clicked = flow.clicked.take();
551 outcome = flow.outcome.take();
552 })
553 .response
554 .rect
555 .height();
556 if clicked.is_some() {
557 self.clicked = clicked;
558 }
559 if outcome.is_some() {
560 self.outcome = outcome;
561 }
562 height
563 }
564
565 fn style_of(&self, shape: &Element) -> std::rc::Rc<Properties> {
572 if let Some(name) = shape.attr(&Ns::Presentation, "style-name") {
573 return self.document.styles.resolve(&Family::Presentation, name);
574 }
575 let name = shape.attr(&Ns::Draw, "style-name").unwrap_or_default();
576 self.document.styles.resolve(&Family::Graphic, name)
577 }
578
579 fn painter(&self, ui: &Ui) -> eframe::egui::Painter {
582 ui.painter()
583 .with_clip_rect(self.page.intersect(ui.clip_rect()))
584 }
585
586 fn placement(&self, shape: &Element) -> Option<Placement> {
594 let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
595 let width = at("width").map_or(0.0, Length::points);
596 let height = at("height").map_or(0.0, Length::points);
597 let on_page = |x: f32, y: f32| {
598 pos2(
599 self.page.left() + x * self.scale,
600 self.page.top() + y * self.scale,
601 )
602 };
603
604 if let Some(transform) = shape
605 .attr(&Ns::Draw, "transform")
606 .and_then(Transform::parse)
607 {
608 let left = at("x").map_or(0.0, Length::points);
611 let top = at("y").map_or(0.0, Length::points);
612 let corner = |u: f32, v: f32| {
613 let (x, y) = transform.apply((width.mul_add(u, left), height.mul_add(v, top)));
614 on_page(x, y)
615 };
616 let origin = corner(0.0, 0.0);
617 return Some(Placement {
618 origin,
619 x: corner(1.0, 0.0) - origin,
620 y: corner(0.0, 1.0) - origin,
621 });
622 }
623
624 Some(Placement {
625 origin: on_page(at("x")?.points(), at("y")?.points()),
626 x: vec2(width * self.scale, 0.0),
627 y: vec2(0.0, height * self.scale),
628 })
629 }
630
631 fn point(&self, shape: &Element, x: &str, y: &str) -> Option<Pos2> {
632 let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
633 Some(pos2(
634 self.page.left() + at(x)?.points() * self.scale,
635 self.page.top() + at(y)?.points() * self.scale,
636 ))
637 }
638
639 fn stroke(&self, properties: &Properties) -> Option<Stroke> {
640 let colour = properties.graphic.stroke?;
641 let width = properties
642 .graphic
643 .stroke_width
644 .map_or(1.0, |w| w.points() * self.scale)
645 .max(1.0);
646 Some(Stroke::new(width, format::color32(colour)))
647 }
648
649 fn fill(&mut self, ui: &Ui, rect: Rect, fill: &Fill, opacity: Option<f32>, points: &[Pos2]) {
659 let document = self.document;
662
663 if let Fill::Image(name) = fill {
664 let Some(href) = document.styles.fill_image(name) else {
665 return;
666 };
667 let Some(texture) = self
668 .pictures
669 .get(ui.ctx(), document, href)
670 .map(eframe::egui::TextureHandle::id)
671 else {
672 return;
673 };
674 let tint = alpha(Color32::WHITE, opacity);
677 let mut mesh = Mesh::with_texture(texture);
678 for point in points {
679 mesh.vertices.push(Vertex {
680 pos: *point,
681 uv: pos2(
682 (point.x - rect.left()) / rect.width().max(f32::EPSILON),
683 (point.y - rect.top()) / rect.height().max(f32::EPSILON),
684 ),
685 color: tint,
686 });
687 }
688 for [a, b, c] in triangulate(points) {
689 mesh.add_triangle(a, b, c);
690 }
691 self.painter(ui).add(Shape::mesh(mesh));
692 return;
693 }
694
695 let gradient = match fill {
696 Fill::None | Fill::Image(_) => return,
697 Fill::Solid(_) => None,
698 Fill::Gradient(name) => match document.styles.gradient(name) {
699 Some(gradient) => Some(gradient),
700 None => return,
701 },
702 };
703 let flat = match fill {
704 Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
705 _ => None,
706 };
707
708 let mut mesh = Mesh::default();
709 for point in points {
710 let colour = flat.unwrap_or_else(|| {
711 gradient.map_or(Color32::TRANSPARENT, |gradient| {
712 alpha(gradient_colour(*point, rect, gradient), opacity)
713 })
714 });
715 mesh.colored_vertex(*point, colour);
716 }
717 for [a, b, c] in triangulate(points) {
718 mesh.add_triangle(a, b, c);
719 }
720 self.painter(ui).add(Shape::mesh(mesh));
721 }
722
723 fn flat(&self, fill: &Fill, opacity: Option<f32>) -> Option<Color32> {
725 match fill {
726 Fill::None | Fill::Image(_) => None,
729 Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
730 Fill::Gradient(name) => {
731 let gradient = self.document.styles.gradient(name)?;
732 Some(alpha(blend(gradient.start, gradient.end, 0.5), opacity))
733 }
734 }
735 }
736}
737
738fn points(shape: &Element, placement: Placement) -> Vec<Pos2> {
746 let view: Vec<f32> = shape
747 .attr(&Ns::Svg, "viewBox")
748 .unwrap_or_default()
749 .split_whitespace()
750 .filter_map(|n| n.parse().ok())
751 .collect();
752 let [left, top, width, height] = view[..] else {
753 return Vec::new();
754 };
755 if width <= 0.0 || height <= 0.0 {
756 return Vec::new();
757 }
758 shape
759 .attr(&Ns::Draw, "points")
760 .unwrap_or_default()
761 .split_whitespace()
762 .filter_map(|pair| {
763 let (x, y) = pair.split_once(',')?;
764 let x: f32 = x.trim().parse().ok()?;
765 let y: f32 = y.trim().parse().ok()?;
766 Some(placement.across((x - left) / width, (y - top) / height))
767 })
768 .collect()
769}
770
771fn ellipse(placement: Placement) -> Vec<Pos2> {
776 const SIDES: usize = 64;
777 (0..SIDES)
778 .map(|i| {
779 #[allow(clippy::cast_precision_loss)]
780 let angle = std::f32::consts::TAU * i as f32 / SIDES as f32;
781 placement.across(
782 0.5f32.mul_add(angle.cos(), 0.5),
783 0.5f32.mul_add(angle.sin(), 0.5),
784 )
785 })
786 .collect()
787}
788
789fn alpha(colour: Color32, opacity: Option<f32>) -> Color32 {
790 match opacity {
791 Some(opacity) if opacity < 1.0 => colour.gamma_multiply(opacity),
792 _ => colour,
793 }
794}
795
796fn blend(from: Color, to: Color, t: f32) -> Color32 {
797 let mix = |a: u8, b: u8| {
798 let a = f32::from(a);
799 let b = f32::from(b);
800 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
801 {
802 (a + (b - a) * t).round().clamp(0.0, 255.0) as u8
803 }
804 };
805 Color32::from_rgb(mix(from.r, to.r), mix(from.g, to.g), mix(from.b, to.b))
806}
807
808fn gradient_colour(point: Pos2, rect: Rect, gradient: &Gradient) -> Color32 {
816 match gradient.style {
817 GradientStyle::Linear | GradientStyle::Axial => {}
818 _ => return blend(gradient.start, gradient.end, 0.5),
819 }
820
821 let radians = gradient.angle.to_radians();
826 let axis = vec2(radians.sin(), -radians.cos());
827 let corners = [
828 rect.left_top(),
829 rect.right_top(),
830 rect.right_bottom(),
831 rect.left_bottom(),
832 ];
833 let projections = corners.map(|corner| (corner - rect.center()).dot(axis));
834 let low = projections.iter().copied().fold(f32::MAX, f32::min);
835 let high = projections.iter().copied().fold(f32::MIN, f32::max);
836 let span = (high - low).max(f32::EPSILON);
837
838 let mut t = ((point - rect.center()).dot(axis) - low) / span;
839 let border = gradient.border.clamp(0.0, 0.99);
842 t = ((t - border) / (1.0 - border)).clamp(0.0, 1.0);
843 if gradient.style == GradientStyle::Axial {
846 t = (t - 0.5).abs() * 2.0;
847 }
848 blend(gradient.start, gradient.end, t)
849}
850
851fn triangulate(points: &[Pos2]) -> Vec<[u32; 3]> {
859 let count = points.len();
860 if count < 3 {
861 return Vec::new();
862 }
863 let fan = || -> Vec<[u32; 3]> {
864 (1..count - 1)
865 .map(|i| {
866 [
867 0,
868 u32::try_from(i).unwrap_or(0),
869 u32::try_from(i + 1).unwrap_or(0),
870 ]
871 })
872 .collect()
873 };
874
875 let area: f32 = (0..count)
877 .map(|i| {
878 let (a, b) = (points[i], points[(i + 1) % count]);
879 a.x * b.y - b.x * a.y
880 })
881 .sum();
882 let winding = if area >= 0.0 { 1.0 } else { -1.0 };
883
884 let cross = |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
885 let inside = |a: Pos2, b: Pos2, c: Pos2, p: Pos2| {
886 cross(a, b, p) * winding >= 0.0
887 && cross(b, c, p) * winding >= 0.0
888 && cross(c, a, p) * winding >= 0.0
889 };
890
891 let mut remaining: Vec<usize> = (0..count).collect();
892 let mut triangles = Vec::with_capacity(count);
893 let mut stuck = 0;
894 while remaining.len() > 3 {
895 if stuck > remaining.len() {
896 return fan();
897 }
898 let mut clipped = false;
899 for position in 0..remaining.len() {
900 let corner = [
901 remaining[(position + remaining.len() - 1) % remaining.len()],
902 remaining[position],
903 remaining[(position + 1) % remaining.len()],
904 ];
905 let ear = corner.map(|index| points[index]);
906 if cross(ear[0], ear[1], ear[2]) * winding <= 0.0 {
909 continue;
910 }
911 if remaining
912 .iter()
913 .filter(|other| !corner.contains(other))
914 .any(|other| inside(ear[0], ear[1], ear[2], points[*other]))
915 {
916 continue;
917 }
918 triangles.push(corner.map(|index| u32::try_from(index).unwrap_or(0)));
919 remaining.remove(position);
920 clipped = true;
921 stuck = 0;
922 break;
923 }
924 if !clipped {
925 stuck += 1;
926 }
927 }
928 if remaining.len() == 3 {
929 triangles.push([
930 u32::try_from(remaining[0]).unwrap_or(0),
931 u32::try_from(remaining[1]).unwrap_or(0),
932 u32::try_from(remaining[2]).unwrap_or(0),
933 ]);
934 }
935 triangles
936}
937
938#[cfg(test)]
939mod tests {
940 use super::triangulate;
941 use eframe::egui::{Pos2, pos2};
942
943 fn areas(points: &[Pos2]) -> (f32, f32) {
946 let cross =
947 |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
948 let triangles: f32 = triangulate(points)
949 .iter()
950 .map(|[a, b, c]| {
951 cross(
952 points[*a as usize],
953 points[*b as usize],
954 points[*c as usize],
955 )
956 .abs()
957 })
958 .sum();
959 let outline: f32 = (0..points.len())
960 .map(|i| {
961 let (a, b) = (points[i], points[(i + 1) % points.len()]);
962 a.x * b.y - b.x * a.y
963 })
964 .sum::<f32>()
965 .abs();
966 (triangles, outline)
967 }
968
969 #[test]
972 fn a_concave_outline_is_cut_into_the_shape_and_not_its_hull() {
973 let l = [
974 pos2(0.0, 0.0),
975 pos2(2.0, 0.0),
976 pos2(2.0, 1.0),
977 pos2(1.0, 1.0),
978 pos2(1.0, 2.0),
979 pos2(0.0, 2.0),
980 ];
981 let (triangles, outline) = areas(&l);
982 assert!(
983 (triangles - outline).abs() < 1e-3,
984 "{triangles} against {outline}"
985 );
986 assert!((outline - 6.0).abs() < 1e-3, "{outline}");
988 }
989
990 #[test]
992 fn a_cross_is_cut_correctly_too() {
993 let cross = [
994 pos2(1.0, 0.0),
995 pos2(2.0, 0.0),
996 pos2(2.0, 1.0),
997 pos2(3.0, 1.0),
998 pos2(3.0, 2.0),
999 pos2(2.0, 2.0),
1000 pos2(2.0, 3.0),
1001 pos2(1.0, 3.0),
1002 pos2(1.0, 2.0),
1003 pos2(0.0, 2.0),
1004 pos2(0.0, 1.0),
1005 pos2(1.0, 1.0),
1006 ];
1007 let (triangles, outline) = areas(&cross);
1008 assert!(
1009 (triangles - outline).abs() < 1e-3,
1010 "{triangles} against {outline}"
1011 );
1012 }
1013
1014 #[test]
1017 fn winding_does_not_matter() {
1018 let mut l = vec![
1019 pos2(0.0, 0.0),
1020 pos2(2.0, 0.0),
1021 pos2(2.0, 1.0),
1022 pos2(1.0, 1.0),
1023 pos2(1.0, 2.0),
1024 pos2(0.0, 2.0),
1025 ];
1026 l.reverse();
1027 let (triangles, outline) = areas(&l);
1028 assert!(
1029 (triangles - outline).abs() < 1e-3,
1030 "{triangles} against {outline}"
1031 );
1032 }
1033
1034 #[test]
1036 fn a_square_is_two_triangles() {
1037 let square = [
1038 pos2(0.0, 0.0),
1039 pos2(1.0, 0.0),
1040 pos2(1.0, 1.0),
1041 pos2(0.0, 1.0),
1042 ];
1043 assert_eq!(triangulate(&square).len(), 2);
1044 let (triangles, outline) = areas(&square);
1045 assert!((triangles - outline).abs() < 1e-4);
1046 }
1047}