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::find::Highlights;
29use crate::flow::{Flow, Pictures};
30use crate::flow_model::PageEditor;
31use crate::format::{self, Palette};
32
33pub struct Canvas<'a> {
35 pub document: &'a Document,
37 pub pictures: &'a mut Pictures,
39 pub page: Rect,
41 pub scale: f32,
43 pub palette: Palette,
45 pub page_editor: Option<&'a mut PageEditor>,
49 pub find: Option<Highlights<'a>>,
51 pub followed: Option<String>,
54 at: Option<usize>,
58}
59
60#[derive(Clone, Copy)]
68struct Placement {
69 origin: Pos2,
71 x: Vec2,
73 y: Vec2,
75}
76
77impl Placement {
78 fn across(self, u: f32, v: f32) -> Pos2 {
80 self.origin + self.x * u + self.y * v
81 }
82
83 fn corners(self) -> [Pos2; 4] {
85 [
86 self.across(0.0, 0.0),
87 self.across(1.0, 0.0),
88 self.across(1.0, 1.0),
89 self.across(0.0, 1.0),
90 ]
91 }
92
93 fn bounds(self) -> Rect {
96 Rect::from_points(&self.corners())
97 }
98
99 fn is_upright(self) -> bool {
102 self.x.y.abs() < 0.01 && self.y.x.abs() < 0.01 && self.x.x >= 0.0 && self.y.y >= 0.0
103 }
104}
105
106#[derive(Clone, Copy, PartialEq, Eq)]
109enum Filled {
110 Yes,
111 No,
112}
113
114impl<'a> Canvas<'a> {
115 pub fn new(
117 document: &'a Document,
118 pictures: &'a mut Pictures,
119 page: Rect,
120 scale: f32,
121 palette: Palette,
122 ) -> Self {
123 Self {
124 document,
125 pictures,
126 page,
127 scale,
128 palette,
129 page_editor: None,
130 find: None,
131 followed: 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.page_editor.is_some() && prefix.is_some();
528 let page_editor = self
529 .page_editor
530 .as_deref_mut()
531 .filter(|_| edit_mode && !measuring);
532 let mut builder = UiBuilder::new().max_rect(rect);
533 if measuring {
534 builder = builder.sizing_pass().invisible();
535 }
536 ui.scope_builder(builder, |ui| {
537 ui.set_clip_rect(rect.intersect(page));
538 let mut flow = Flow::new(document, pictures, scale);
539 flow.palette = palette;
540 flow.selectable = !edit_mode;
541 flow.page = page_editor;
542 flow.find = self.find.clone().filter(|_| prefix.is_some() && !measuring);
543 if let Some(prefix) = prefix {
544 flow.start_at(prefix.clone());
545 }
546 if picture {
547 flow.frame(ui, content, rect.width());
548 } else {
549 flow.blocks(ui, content, rect.width());
550 }
551 if flow.followed.is_some() {
552 self.followed = flow.followed.take();
553 }
554 })
555 .response
556 .rect
557 .height()
558 }
559
560 fn style_of(&self, shape: &Element) -> std::rc::Rc<Properties> {
567 if let Some(name) = shape.attr(&Ns::Presentation, "style-name") {
568 return self.document.styles.resolve(&Family::Presentation, name);
569 }
570 let name = shape.attr(&Ns::Draw, "style-name").unwrap_or_default();
571 self.document.styles.resolve(&Family::Graphic, name)
572 }
573
574 fn painter(&self, ui: &Ui) -> eframe::egui::Painter {
577 ui.painter()
578 .with_clip_rect(self.page.intersect(ui.clip_rect()))
579 }
580
581 fn placement(&self, shape: &Element) -> Option<Placement> {
589 let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
590 let width = at("width").map_or(0.0, Length::points);
591 let height = at("height").map_or(0.0, Length::points);
592 let on_page = |x: f32, y: f32| {
593 pos2(
594 self.page.left() + x * self.scale,
595 self.page.top() + y * self.scale,
596 )
597 };
598
599 if let Some(transform) = shape
600 .attr(&Ns::Draw, "transform")
601 .and_then(Transform::parse)
602 {
603 let left = at("x").map_or(0.0, Length::points);
606 let top = at("y").map_or(0.0, Length::points);
607 let corner = |u: f32, v: f32| {
608 let (x, y) = transform.apply((width.mul_add(u, left), height.mul_add(v, top)));
609 on_page(x, y)
610 };
611 let origin = corner(0.0, 0.0);
612 return Some(Placement {
613 origin,
614 x: corner(1.0, 0.0) - origin,
615 y: corner(0.0, 1.0) - origin,
616 });
617 }
618
619 Some(Placement {
620 origin: on_page(at("x")?.points(), at("y")?.points()),
621 x: vec2(width * self.scale, 0.0),
622 y: vec2(0.0, height * self.scale),
623 })
624 }
625
626 fn point(&self, shape: &Element, x: &str, y: &str) -> Option<Pos2> {
627 let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
628 Some(pos2(
629 self.page.left() + at(x)?.points() * self.scale,
630 self.page.top() + at(y)?.points() * self.scale,
631 ))
632 }
633
634 fn stroke(&self, properties: &Properties) -> Option<Stroke> {
635 let colour = properties.graphic.stroke?;
636 let width = properties
637 .graphic
638 .stroke_width
639 .map_or(1.0, |w| w.points() * self.scale)
640 .max(1.0);
641 Some(Stroke::new(width, format::color32(colour)))
642 }
643
644 fn fill(&mut self, ui: &Ui, rect: Rect, fill: &Fill, opacity: Option<f32>, points: &[Pos2]) {
654 let document = self.document;
657
658 if let Fill::Image(name) = fill {
659 let Some(href) = document.styles.fill_image(name) else {
660 return;
661 };
662 let Some(texture) = self
663 .pictures
664 .get(ui.ctx(), document, href)
665 .map(eframe::egui::TextureHandle::id)
666 else {
667 return;
668 };
669 let tint = alpha(Color32::WHITE, opacity);
672 let mut mesh = Mesh::with_texture(texture);
673 for point in points {
674 mesh.vertices.push(Vertex {
675 pos: *point,
676 uv: pos2(
677 (point.x - rect.left()) / rect.width().max(f32::EPSILON),
678 (point.y - rect.top()) / rect.height().max(f32::EPSILON),
679 ),
680 color: tint,
681 });
682 }
683 for [a, b, c] in triangulate(points) {
684 mesh.add_triangle(a, b, c);
685 }
686 self.painter(ui).add(Shape::mesh(mesh));
687 return;
688 }
689
690 let gradient = match fill {
691 Fill::None | Fill::Image(_) => return,
692 Fill::Solid(_) => None,
693 Fill::Gradient(name) => match document.styles.gradient(name) {
694 Some(gradient) => Some(gradient),
695 None => return,
696 },
697 };
698 let flat = match fill {
699 Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
700 _ => None,
701 };
702
703 let mut mesh = Mesh::default();
704 for point in points {
705 let colour = flat.unwrap_or_else(|| {
706 gradient.map_or(Color32::TRANSPARENT, |gradient| {
707 alpha(gradient_colour(*point, rect, gradient), opacity)
708 })
709 });
710 mesh.colored_vertex(*point, colour);
711 }
712 for [a, b, c] in triangulate(points) {
713 mesh.add_triangle(a, b, c);
714 }
715 self.painter(ui).add(Shape::mesh(mesh));
716 }
717
718 fn flat(&self, fill: &Fill, opacity: Option<f32>) -> Option<Color32> {
720 match fill {
721 Fill::None | Fill::Image(_) => None,
724 Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
725 Fill::Gradient(name) => {
726 let gradient = self.document.styles.gradient(name)?;
727 Some(alpha(blend(gradient.start, gradient.end, 0.5), opacity))
728 }
729 }
730 }
731}
732
733fn points(shape: &Element, placement: Placement) -> Vec<Pos2> {
741 let view: Vec<f32> = shape
742 .attr(&Ns::Svg, "viewBox")
743 .unwrap_or_default()
744 .split_whitespace()
745 .filter_map(|n| n.parse().ok())
746 .collect();
747 let [left, top, width, height] = view[..] else {
748 return Vec::new();
749 };
750 if width <= 0.0 || height <= 0.0 {
751 return Vec::new();
752 }
753 shape
754 .attr(&Ns::Draw, "points")
755 .unwrap_or_default()
756 .split_whitespace()
757 .filter_map(|pair| {
758 let (x, y) = pair.split_once(',')?;
759 let x: f32 = x.trim().parse().ok()?;
760 let y: f32 = y.trim().parse().ok()?;
761 Some(placement.across((x - left) / width, (y - top) / height))
762 })
763 .collect()
764}
765
766fn ellipse(placement: Placement) -> Vec<Pos2> {
771 const SIDES: usize = 64;
772 (0..SIDES)
773 .map(|i| {
774 #[allow(clippy::cast_precision_loss)]
775 let angle = std::f32::consts::TAU * i as f32 / SIDES as f32;
776 placement.across(
777 0.5f32.mul_add(angle.cos(), 0.5),
778 0.5f32.mul_add(angle.sin(), 0.5),
779 )
780 })
781 .collect()
782}
783
784fn alpha(colour: Color32, opacity: Option<f32>) -> Color32 {
785 match opacity {
786 Some(opacity) if opacity < 1.0 => colour.gamma_multiply(opacity),
787 _ => colour,
788 }
789}
790
791fn blend(from: Color, to: Color, t: f32) -> Color32 {
792 let mix = |a: u8, b: u8| {
793 let a = f32::from(a);
794 let b = f32::from(b);
795 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
796 {
797 (a + (b - a) * t).round().clamp(0.0, 255.0) as u8
798 }
799 };
800 Color32::from_rgb(mix(from.r, to.r), mix(from.g, to.g), mix(from.b, to.b))
801}
802
803fn gradient_colour(point: Pos2, rect: Rect, gradient: &Gradient) -> Color32 {
811 match gradient.style {
812 GradientStyle::Linear | GradientStyle::Axial => {}
813 _ => return blend(gradient.start, gradient.end, 0.5),
814 }
815
816 let radians = gradient.angle.to_radians();
821 let axis = vec2(radians.sin(), -radians.cos());
822 let corners = [
823 rect.left_top(),
824 rect.right_top(),
825 rect.right_bottom(),
826 rect.left_bottom(),
827 ];
828 let projections = corners.map(|corner| (corner - rect.center()).dot(axis));
829 let low = projections.iter().copied().fold(f32::MAX, f32::min);
830 let high = projections.iter().copied().fold(f32::MIN, f32::max);
831 let span = (high - low).max(f32::EPSILON);
832
833 let mut t = ((point - rect.center()).dot(axis) - low) / span;
834 let border = gradient.border.clamp(0.0, 0.99);
837 t = ((t - border) / (1.0 - border)).clamp(0.0, 1.0);
838 if gradient.style == GradientStyle::Axial {
841 t = (t - 0.5).abs() * 2.0;
842 }
843 blend(gradient.start, gradient.end, t)
844}
845
846fn triangulate(points: &[Pos2]) -> Vec<[u32; 3]> {
854 let count = points.len();
855 if count < 3 {
856 return Vec::new();
857 }
858 let fan = || -> Vec<[u32; 3]> {
859 (1..count - 1)
860 .map(|i| {
861 [
862 0,
863 u32::try_from(i).unwrap_or(0),
864 u32::try_from(i + 1).unwrap_or(0),
865 ]
866 })
867 .collect()
868 };
869
870 let area: f32 = (0..count)
872 .map(|i| {
873 let (a, b) = (points[i], points[(i + 1) % count]);
874 a.x * b.y - b.x * a.y
875 })
876 .sum();
877 let winding = if area >= 0.0 { 1.0 } else { -1.0 };
878
879 let cross = |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
880 let inside = |a: Pos2, b: Pos2, c: Pos2, p: Pos2| {
881 cross(a, b, p) * winding >= 0.0
882 && cross(b, c, p) * winding >= 0.0
883 && cross(c, a, p) * winding >= 0.0
884 };
885
886 let mut remaining: Vec<usize> = (0..count).collect();
887 let mut triangles = Vec::with_capacity(count);
888 let mut stuck = 0;
889 while remaining.len() > 3 {
890 if stuck > remaining.len() {
891 return fan();
892 }
893 let mut clipped = false;
894 for position in 0..remaining.len() {
895 let corner = [
896 remaining[(position + remaining.len() - 1) % remaining.len()],
897 remaining[position],
898 remaining[(position + 1) % remaining.len()],
899 ];
900 let ear = corner.map(|index| points[index]);
901 if cross(ear[0], ear[1], ear[2]) * winding <= 0.0 {
904 continue;
905 }
906 if remaining
907 .iter()
908 .filter(|other| !corner.contains(other))
909 .any(|other| inside(ear[0], ear[1], ear[2], points[*other]))
910 {
911 continue;
912 }
913 triangles.push(corner.map(|index| u32::try_from(index).unwrap_or(0)));
914 remaining.remove(position);
915 clipped = true;
916 stuck = 0;
917 break;
918 }
919 if !clipped {
920 stuck += 1;
921 }
922 }
923 if remaining.len() == 3 {
924 triangles.push([
925 u32::try_from(remaining[0]).unwrap_or(0),
926 u32::try_from(remaining[1]).unwrap_or(0),
927 u32::try_from(remaining[2]).unwrap_or(0),
928 ]);
929 }
930 triangles
931}
932
933#[cfg(test)]
934mod tests {
935 use super::triangulate;
936 use eframe::egui::{Pos2, pos2};
937
938 fn areas(points: &[Pos2]) -> (f32, f32) {
941 let cross =
942 |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
943 let triangles: f32 = triangulate(points)
944 .iter()
945 .map(|[a, b, c]| {
946 cross(
947 points[*a as usize],
948 points[*b as usize],
949 points[*c as usize],
950 )
951 .abs()
952 })
953 .sum();
954 let outline: f32 = (0..points.len())
955 .map(|i| {
956 let (a, b) = (points[i], points[(i + 1) % points.len()]);
957 a.x * b.y - b.x * a.y
958 })
959 .sum::<f32>()
960 .abs();
961 (triangles, outline)
962 }
963
964 #[test]
967 fn a_concave_outline_is_cut_into_the_shape_and_not_its_hull() {
968 let l = [
969 pos2(0.0, 0.0),
970 pos2(2.0, 0.0),
971 pos2(2.0, 1.0),
972 pos2(1.0, 1.0),
973 pos2(1.0, 2.0),
974 pos2(0.0, 2.0),
975 ];
976 let (triangles, outline) = areas(&l);
977 assert!(
978 (triangles - outline).abs() < 1e-3,
979 "{triangles} against {outline}"
980 );
981 assert!((outline - 6.0).abs() < 1e-3, "{outline}");
983 }
984
985 #[test]
987 fn a_cross_is_cut_correctly_too() {
988 let cross = [
989 pos2(1.0, 0.0),
990 pos2(2.0, 0.0),
991 pos2(2.0, 1.0),
992 pos2(3.0, 1.0),
993 pos2(3.0, 2.0),
994 pos2(2.0, 2.0),
995 pos2(2.0, 3.0),
996 pos2(1.0, 3.0),
997 pos2(1.0, 2.0),
998 pos2(0.0, 2.0),
999 pos2(0.0, 1.0),
1000 pos2(1.0, 1.0),
1001 ];
1002 let (triangles, outline) = areas(&cross);
1003 assert!(
1004 (triangles - outline).abs() < 1e-3,
1005 "{triangles} against {outline}"
1006 );
1007 }
1008
1009 #[test]
1012 fn winding_does_not_matter() {
1013 let mut l = vec![
1014 pos2(0.0, 0.0),
1015 pos2(2.0, 0.0),
1016 pos2(2.0, 1.0),
1017 pos2(1.0, 1.0),
1018 pos2(1.0, 2.0),
1019 pos2(0.0, 2.0),
1020 ];
1021 l.reverse();
1022 let (triangles, outline) = areas(&l);
1023 assert!(
1024 (triangles - outline).abs() < 1e-3,
1025 "{triangles} against {outline}"
1026 );
1027 }
1028
1029 #[test]
1031 fn a_square_is_two_triangles() {
1032 let square = [
1033 pos2(0.0, 0.0),
1034 pos2(1.0, 0.0),
1035 pos2(1.0, 1.0),
1036 pos2(0.0, 1.0),
1037 ];
1038 assert_eq!(triangulate(&square).len(), 2);
1039 let (triangles, outline) = areas(&square);
1040 assert!((triangles - outline).abs() < 1e-4);
1041 }
1042}