1use ogeom_core::collections::FastMap;
28use ogeom_core::parallel::map_ordered;
29use ogeom_core::{Exact, OgeomResult, Predicates, Sign, ogeom_bail};
30use ogeom_math::{Plane, Point, Vector};
31use ogeom_topo::Triangulation;
32
33#[derive(Debug, Clone, PartialEq, Default)]
35pub struct Chain {
36 pub points: Vec<Point>,
39 pub outward: Vec<Vector>,
43 pub closed: bool,
47}
48
49#[derive(Debug, Clone, PartialEq, Default)]
51pub struct Section {
52 pub chains: Vec<Chain>,
54 pub coplanar: Vec<u32>,
57 pub unlinked: Vec<[u32; 2]>,
62}
63
64pub fn section(mesh: &Triangulation, plane: &Plane) -> OgeomResult<Section> {
78 checked(mesh, plane)?;
79 Ok(cut(mesh, &Cutter::new(plane, 0.0)))
80}
81
82pub fn sections(mesh: &Triangulation, plane: &Plane, offsets: &[f64]) -> OgeomResult<Vec<Section>> {
93 checked(mesh, plane)?;
94 if let Some(d) = offsets.iter().find(|d| !d.is_finite()) {
95 ogeom_bail!(Numeric, "a section offset of {d} is not a distance");
96 }
97 Ok(map_ordered(offsets, |_, &d| {
98 cut(mesh, &Cutter::new(plane, d))
99 }))
100}
101
102fn checked(mesh: &Triangulation, plane: &Plane) -> OgeomResult<()> {
103 if !plane.origin().is_finite() {
104 ogeom_bail!(Numeric, "the section plane's origin is not finite");
105 }
106 if let Some(k) = mesh.positions.iter().position(|p| !p.is_finite()) {
107 ogeom_bail!(Numeric, "mesh vertex {k} is not finite");
108 }
109 let count = mesh.positions.len();
110 if let Some(&k) = mesh
111 .triangles
112 .iter()
113 .flatten()
114 .find(|&&k| k as usize >= count)
115 {
116 ogeom_bail!(Range, "a triangle names vertex {k} of {count}");
117 }
118 Ok(())
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub(crate) enum Place {
124 Below,
125 On,
126 Above,
127}
128
129impl Place {
130 pub(crate) const fn up(self) -> bool {
132 !matches!(self, Self::Below)
133 }
134}
135
136#[derive(Debug, Clone, Copy)]
138pub(crate) struct Cutter {
139 origin: Point,
140 normal: Vector,
141 axes: [Vector; 2],
144 span: [[f64; 3]; 3],
146}
147
148impl Cutter {
149 pub(crate) fn new(plane: &Plane, offset: f64) -> Self {
150 let z = plane.normal();
151 let normal = z.vector();
152 let origin = plane.origin() + normal * offset;
153 let u = z.any_perpendicular().vector();
154 let v = normal.cross(u);
155 Self {
156 origin,
157 normal,
158 axes: [u, v],
159 span: [
160 origin.to_array(),
161 (origin + u).to_array(),
162 (origin + v).to_array(),
163 ],
164 }
165 }
166
167 pub(crate) fn side(&self, p: Point) -> Place {
168 let [a, b, c] = self.span;
169 match Exact::orient3d(a, b, c, p.to_array()) {
172 Sign::Positive => Place::Below,
173 Sign::Zero => Place::On,
174 Sign::Negative => Place::Above,
175 }
176 }
177
178 fn height(&self, p: Point) -> f64 {
179 (p - self.origin).dot(self.normal)
180 }
181
182 pub(crate) fn blur(&self, p: Point) -> f64 {
189 let w = p - self.origin;
190 5.0 * f64::EPSILON * (w.x.abs() + w.y.abs() + w.z.abs())
191 }
192
193 pub(crate) fn near(&self, p: Point) -> bool {
198 self.height(p).abs() <= self.blur(p)
199 }
200
201 pub(crate) const fn normal(&self) -> Vector {
203 self.normal
204 }
205
206 pub(crate) fn along(&self, p: Point) -> [f64; 2] {
208 let w = p - self.origin;
209 [w.dot(self.axes[0]), w.dot(self.axes[1])]
210 }
211
212 pub(crate) fn crossing(&self, low: Point, high: Point) -> Point {
216 let (hp, hq) = (self.height(low), self.height(high));
217 let t = if hp == hq {
222 0.5
223 } else {
224 (hp / (hp - hq)).clamp(0.0, 1.0)
225 };
226 low.lerp(high, t)
227 }
228
229 pub(crate) fn crossing_blur(low: Point, high: Point, x: Point) -> f64 {
238 let d = high - low;
239 let l1 = x.x.abs() + x.y.abs() + x.z.abs() + d.x.abs() + d.y.abs() + d.z.abs();
240 f64::EPSILON * l1
241 }
242}
243
244#[derive(Debug, Clone, Copy)]
247struct Segment {
248 from: Edge,
249 to: Edge,
250 triangle: u32,
251}
252
253#[derive(Debug, Clone, Copy)]
255struct Slot {
256 starts: u32,
257 ends: u32,
258 start: u32,
259 end: u32,
260}
261
262const NONE: u32 = u32::MAX;
263
264type Edge = [u32; 2];
266
267fn key(a: u32, b: u32) -> Edge {
268 [a.min(b), a.max(b)]
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273enum Node {
274 Vertex(u32),
275 Edge(Edge),
276}
277
278fn cut(mesh: &Triangulation, cutter: &Cutter) -> Section {
279 let sides: Vec<Place> = mesh.positions.iter().map(|&p| cutter.side(p)).collect();
280
281 let mut segments = Vec::new();
282 let mut coplanar = Vec::new();
283 for (index, t) in mesh.triangles.iter().enumerate() {
284 let triangle = u32::try_from(index).unwrap_or(NONE);
285 if t[0] == t[1] || t[1] == t[2] || t[2] == t[0] {
286 continue;
287 }
288 let s = t.map(|k| sides[k as usize]);
289 let ups = s.iter().filter(|s| s.up()).count();
290 if ups == 0 || ups == 3 {
291 if s.iter().all(|&s| s == Place::On) {
292 coplanar.push(triangle);
293 }
294 continue;
295 }
296 let (mut from, mut to) = ([0, 0], [0, 0]);
297 for k in 0..3 {
298 let (p, q) = (k, (k + 1) % 3);
299 match (s[p].up(), s[q].up()) {
300 (true, false) => from = key(t[p], t[q]),
301 (false, true) => to = key(t[p], t[q]),
302 _ => {}
303 }
304 }
305 segments.push(Segment { from, to, triangle });
306 }
307
308 let mut slots: FastMap<Edge, Slot> = FastMap::default();
309 let empty = Slot {
310 starts: 0,
311 ends: 0,
312 start: NONE,
313 end: NONE,
314 };
315 for (i, s) in segments.iter().enumerate() {
316 let i = u32::try_from(i).unwrap_or(NONE);
317 let slot = slots.entry(s.from).or_insert(empty);
318 slot.starts += 1;
319 slot.start = i;
320 let slot = slots.entry(s.to).or_insert(empty);
321 slot.ends += 1;
322 slot.end = i;
323 }
324 let linked = |slot: &Slot| slot.starts <= 1 && slot.ends <= 1;
325 let mut unlinked: Vec<[u32; 2]> = slots
326 .iter()
327 .filter(|(_, slot)| !linked(slot))
328 .map(|(&k, _)| k)
329 .collect();
330 unlinked.sort_unstable();
331
332 let next: Vec<u32> = segments
333 .iter()
334 .map(|s| {
335 slots
336 .get(&s.to)
337 .filter(|x| linked(x))
338 .map_or(NONE, |x| x.start)
339 })
340 .collect();
341 let prev: Vec<u32> = segments
342 .iter()
343 .map(|s| {
344 slots
345 .get(&s.from)
346 .filter(|x| linked(x))
347 .map_or(NONE, |x| x.end)
348 })
349 .collect();
350
351 let mut seen = vec![false; segments.len()];
352 let mut chains = Vec::new();
353 for first in 0..segments.len() {
354 if seen[first] {
355 continue;
356 }
357 let mut head = first;
358 let mut closed = false;
359 loop {
360 let p = prev[head];
361 if p == NONE {
362 break;
363 }
364 if p as usize == first {
365 closed = true;
366 head = first;
367 break;
368 }
369 head = p as usize;
370 }
371 let mut run = vec![head];
372 seen[head] = true;
373 let mut at = head;
374 loop {
375 let n = next[at];
376 if n == NONE || n as usize == head {
377 break;
378 }
379 at = n as usize;
380 seen[at] = true;
381 run.push(at);
382 }
383 if let Some(chain) = chain(mesh, cutter, &sides, &segments, &run, closed) {
384 chains.push(chain);
385 }
386 }
387
388 Section {
389 chains,
390 coplanar,
391 unlinked,
392 }
393}
394
395fn chain(
398 mesh: &Triangulation,
399 cutter: &Cutter,
400 sides: &[Place],
401 segments: &[Segment],
402 run: &[usize],
403 closed: bool,
404) -> Option<Chain> {
405 let node = |edge: Edge| {
406 let [a, b] = edge;
407 let up = if sides[a as usize].up() { a } else { b };
408 if sides[up as usize] == Place::On {
409 Node::Vertex(up)
410 } else {
411 Node::Edge(edge)
412 }
413 };
414 let lean = |s: &Segment| {
420 if node(s.from) == node(s.to) {
421 Vector::ZERO
422 } else {
423 in_plane(mesh, cutter, s.triangle)
424 }
425 };
426 let leans: Vec<Vector> = run.iter().map(|&s| lean(&segments[s])).collect();
427 let mut nodes: Vec<(Node, Vector)> = Vec::with_capacity(run.len() + 1);
428 for (i, &s) in run.iter().enumerate() {
429 let before = if i > 0 {
430 leans[i - 1]
431 } else if closed && let Some(&last) = leans.last() {
432 last
433 } else {
434 Vector::ZERO
435 };
436 push(&mut nodes, node(segments[s].from), leans[i] + before);
437 }
438 if !closed && let (Some(&last), Some(&w)) = (run.last(), leans.last()) {
439 push(&mut nodes, node(segments[last].to), w);
440 }
441 if closed
442 && nodes.len() > 1
443 && let (Some(&(first, _)), Some(&(last, w))) = (nodes.first(), nodes.last())
444 && first == last
445 {
446 nodes.pop();
447 nodes[0].1 += w;
448 }
449 if nodes.len() < 2 {
450 return None;
451 }
452
453 let points: Vec<Point> = nodes
454 .iter()
455 .map(|&(n, _)| point(mesh, cutter, sides, n))
456 .collect();
457 let count = points.len();
458 let outward = (0..count)
459 .map(|i| {
460 let w = nodes[i].1;
461 let length = w.magnitude();
462 if length > 0.0 && length.is_finite() {
463 return w / length;
464 }
465 let before = if i > 0 || closed {
469 (i + count - 1) % count
470 } else {
471 i
472 };
473 let after = if i + 1 < count || closed {
474 (i + 1) % count
475 } else {
476 i
477 };
478 let c = (points[after] - points[before]).cross(cutter.normal);
479 let length = c.magnitude();
480 if length > 0.0 && length.is_finite() {
481 c / length
482 } else {
483 Vector::ZERO
484 }
485 })
486 .collect();
487 Some(Chain {
488 points,
489 outward,
490 closed,
491 })
492}
493
494fn push(nodes: &mut Vec<(Node, Vector)>, node: Node, w: Vector) {
496 if let Some(last) = nodes.last_mut()
497 && last.0 == node
498 {
499 last.1 += w;
500 return;
501 }
502 nodes.push((node, w));
503}
504
505fn in_plane(mesh: &Triangulation, cutter: &Cutter, triangle: u32) -> Vector {
510 let [a, b, c] = mesh.triangles[triangle as usize].map(|k| mesh.positions[k as usize]);
511 let n = (b - a).cross(c - a);
512 let length = n.magnitude();
513 if !(length > 0.0 && length.is_finite()) {
514 return Vector::ZERO;
515 }
516 let n = n / length;
517 n - cutter.normal * n.dot(cutter.normal)
518}
519
520fn point(mesh: &Triangulation, cutter: &Cutter, sides: &[Place], node: Node) -> Point {
522 match node {
523 Node::Vertex(v) => mesh.positions[v as usize],
524 Node::Edge(edge) => {
525 let [a, b] = edge;
526 let (low, high) = if sides[a as usize].up() {
527 (b, a)
528 } else {
529 (a, b)
530 };
531 cutter.crossing(mesh.positions[low as usize], mesh.positions[high as usize])
532 }
533 }
534}
535
536#[cfg(test)]
537#[allow(clippy::unwrap_used, clippy::cast_precision_loss, reason = "test code")]
538mod tests {
539 use super::*;
540 use core::f64::consts::TAU;
541 use ogeom_core::Tolerances;
542 use ogeom_math::Direction;
543 use proptest::prelude::*;
544
545 const T: Tolerances = Tolerances::millimetres();
546
547 fn mesh(positions: Vec<Point>, triangles: Vec<[u32; 3]>) -> Triangulation {
548 Triangulation {
549 positions,
550 triangles,
551 ..Triangulation::new()
552 }
553 }
554
555 fn index(i: usize) -> u32 {
556 u32::try_from(i).unwrap()
557 }
558
559 fn wall(
563 points: &mut Vec<Point>,
564 triangles: &mut Vec<[u32; 3]>,
565 radius: f64,
566 height: f64,
567 sides: usize,
568 rings: usize,
569 outward: bool,
570 ) -> usize {
571 let base = points.len();
572 for j in 0..rings {
573 let z = height * j as f64 / (rings - 1) as f64;
574 for i in 0..sides {
575 let a = TAU * i as f64 / sides as f64;
576 points.push(Point::new(radius * a.cos(), radius * a.sin(), z));
577 }
578 }
579 let at = |i: usize, j: usize| index(base + j * sides + i % sides);
580 for j in 0..rings - 1 {
581 for i in 0..sides {
582 let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
583 if outward {
584 triangles.extend([[a, b, c], [a, c, d]]);
585 } else {
586 triangles.extend([[a, c, b], [a, d, c]]);
587 }
588 }
589 }
590 base
591 }
592
593 fn cylinder(radius: f64, height: f64, sides: usize, rings: usize) -> Triangulation {
595 let (mut points, mut triangles) = (Vec::new(), Vec::new());
596 let base = wall(
597 &mut points,
598 &mut triangles,
599 radius,
600 height,
601 sides,
602 rings,
603 true,
604 );
605 let top = base + (rings - 1) * sides;
606 let (low, high) = (index(points.len()), index(points.len() + 1));
607 points.extend([Point::new(0.0, 0.0, 0.0), Point::new(0.0, 0.0, height)]);
608 for i in 0..sides {
609 let j = (i + 1) % sides;
610 triangles.push([low, index(base + j), index(base + i)]);
611 triangles.push([high, index(top + i), index(top + j)]);
612 }
613 mesh(points, triangles)
614 }
615
616 fn tube(inner: f64, outer: f64, height: f64, sides: usize) -> Triangulation {
618 let (mut points, mut triangles) = (Vec::new(), Vec::new());
619 let o = wall(&mut points, &mut triangles, outer, height, sides, 2, true);
620 let i = wall(&mut points, &mut triangles, inner, height, sides, 2, false);
621 for k in 0..sides {
622 let m = (k + 1) % sides;
623 let (ok, om, ik, im) = (index(o + k), index(o + m), index(i + k), index(i + m));
624 triangles.extend([[ok, ik, im], [ok, im, om]]);
625 let s = index(sides);
626 triangles.extend([[ok + s, om + s, im + s], [ok + s, im + s, ik + s]]);
627 }
628 mesh(points, triangles)
629 }
630
631 fn cube(n: u32) -> Triangulation {
634 let mut ids: FastMap<[u32; 3], u32> = FastMap::default();
635 let (mut points, mut triangles) = (Vec::new(), Vec::new());
636 let scale = f64::from(n);
637 let mut vertex = |c: [u32; 3], points: &mut Vec<Point>| {
638 *ids.entry(c).or_insert_with(|| {
639 points.push(Point::new(
640 f64::from(c[0]) / scale,
641 f64::from(c[1]) / scale,
642 f64::from(c[2]) / scale,
643 ));
644 index(points.len() - 1)
645 })
646 };
647 for axis in 0..3 {
648 for side in [0, n] {
649 let (u, v) = ((axis + 1) % 3, (axis + 2) % 3);
650 let at = |a: u32, b: u32| {
651 let mut c = [0; 3];
652 c[axis] = side;
653 c[u] = a;
654 c[v] = b;
655 c
656 };
657 for a in 0..n {
658 for b in 0..n {
659 let q = [at(a, b), at(a + 1, b), at(a + 1, b + 1), at(a, b + 1)]
660 .map(|c| vertex(c, &mut points));
661 if side == n {
663 triangles.extend([[q[0], q[1], q[2]], [q[0], q[2], q[3]]]);
664 } else {
665 triangles.extend([[q[0], q[2], q[1]], [q[0], q[3], q[2]]]);
666 }
667 }
668 }
669 }
670 }
671 mesh(points, triangles)
672 }
673
674 fn plane(origin: Point, normal: Vector) -> Plane {
675 Plane::through(origin, Direction::new(normal, T).unwrap())
676 }
677
678 fn horizontal(z: f64) -> Plane {
679 plane(Point::new(0.0, 0.0, z), Vector::Z)
680 }
681
682 fn area(chain: &Chain, normal: Vector) -> f64 {
684 let p0 = chain.points[0];
685 let mut sum = 0.0;
686 for w in chain.points.windows(2).skip(1) {
687 sum += (w[0] - p0).cross(w[1] - p0).dot(normal);
688 }
689 sum * 0.5
690 }
691
692 fn radial(p: Point) -> Vector {
693 Vector::new(p.x, p.y, 0.0) / p.x.hypot(p.y)
694 }
695
696 #[test]
697 fn a_closed_cylinder_cut_across_is_one_circle_facing_away_from_its_axis() {
698 let sides = 48;
699 let m = cylinder(2.0, 3.0, sides, 4);
700 let polygon = 0.5 * sides as f64 * 4.0 * (TAU / sides as f64).sin();
703 let half = (TAU / sides as f64 / 2.0).cos();
704 for (z, count) in [(0.7, 2 * sides), (1.0, sides)] {
705 let s = section(&m, &horizontal(z)).unwrap();
706 assert!(s.unlinked.is_empty() && s.coplanar.is_empty());
707 assert_eq!(s.chains.len(), 1);
708 let c = &s.chains[0];
709 assert!(c.closed);
710 assert_eq!(c.points.len(), count);
711 assert_eq!(c.outward.len(), count);
712 let mut on_edges = 0;
713 for (p, w) in c.points.iter().zip(&c.outward) {
714 assert!((p.z - z).abs() < 1e-15, "{p:?}");
715 assert!(w.z.abs() < 1e-15);
716 if (p.x.hypot(p.y) - 2.0).abs() < 1e-14 {
717 assert!(w.dot(radial(*p)) > 1.0 - 1e-12, "{w:?} at {p:?}");
719 on_edges += 1;
720 } else {
721 assert!(w.dot(radial(*p)) > half - 1e-12, "{w:?} at {p:?}");
723 }
724 }
725 assert_eq!(on_edges, sides);
726 assert!((area(c, Vector::Z) - polygon).abs() < 1e-12);
728 }
729 }
730
731 #[test]
732 fn a_thin_wall_gives_two_chains_with_the_material_between_them() {
733 let sides = 64;
734 let (inner, outer) = (0.95, 1.0);
735 let s = section(&tube(inner, outer, 1.0, sides), &horizontal(0.5)).unwrap();
736 assert!(s.unlinked.is_empty());
737 assert_eq!(s.chains.len(), 2);
738 let polygon = |r: f64| 0.5 * sides as f64 * r * r * (TAU / sides as f64).sin();
739 let half = (TAU / sides as f64 / 2.0).cos();
742 let mut faces = Vec::new();
743 for c in &s.chains {
744 assert!(c.closed);
745 assert_eq!(c.points.len(), 2 * sides);
746 let r = c.points.iter().map(|p| p.x.hypot(p.y)).fold(0.0, f64::max);
747 let facing: Vec<f64> = c
748 .points
749 .iter()
750 .zip(&c.outward)
751 .map(|(p, w)| w.dot(radial(*p)))
752 .collect();
753 if (r - outer).abs() < 1e-12 {
754 assert!(facing.iter().all(|&f| f > half - 1e-12));
755 assert!((area(c, Vector::Z) - polygon(outer)).abs() < 1e-12);
756 faces.push(1);
757 } else {
758 assert!((r - inner).abs() < 1e-12);
759 assert!(facing.iter().all(|&f| f < -half + 1e-12));
761 assert!((area(c, Vector::Z) + polygon(inner)).abs() < 1e-12);
762 faces.push(-1);
763 }
764 }
765 faces.sort_unstable();
766 assert_eq!(faces, [-1, 1]);
767 }
768
769 #[test]
770 fn an_open_sheet_gives_an_open_chain_from_edge_to_edge() {
771 let n = 40;
773 let mut points = Vec::new();
774 for j in 0..=n {
775 for i in 0..=n {
776 let (x, y) = (4.0 * i as f64 / n as f64, 4.0 * j as f64 / n as f64);
777 points.push(Point::new(x, y, 0.1 * x.sin() * y.cos()));
778 }
779 }
780 let at = |i: usize, j: usize| index(j * (n + 1) + i);
781 let mut triangles = Vec::new();
782 for j in 0..n {
783 for i in 0..n {
784 let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
785 triangles.extend([[a, b, c], [a, c, d]]);
786 }
787 }
788 let m = mesh(points, triangles);
789 let s = section(&m, &plane(Point::new(2.05, 0.0, 0.0), Vector::X)).unwrap();
790 assert!(s.unlinked.is_empty());
791 assert_eq!(s.chains.len(), 1);
792 let c = &s.chains[0];
793 assert!(!c.closed);
794 assert_eq!(c.points.len(), 2 * n + 1);
796 assert_eq!(c.points[0].y, 4.0);
798 assert_eq!(c.points[2 * n].y, 0.0);
799 for (p, w) in c.points.iter().zip(&c.outward) {
800 assert!((p.x - 2.05).abs() < 1e-15);
801 assert!((p.z - 0.1 * p.x.sin() * p.y.cos()).abs() < 1e-3);
802 assert!(w.x.abs() < 1e-15 && w.z > 0.99, "{w:?}");
803 }
804 }
805
806 #[test]
807 fn a_plane_through_vertices_and_edges_passes_them_once() {
808 let m = cube(2);
809 let s = section(&m, &horizontal(0.5)).unwrap();
812 assert!(s.unlinked.is_empty() && s.coplanar.is_empty());
813 assert_eq!(s.chains.len(), 1);
814 let c = &s.chains[0];
815 assert!(c.closed);
816 assert_eq!(c.points.len(), 8);
817 assert!(c.points.iter().all(|p| m.positions.contains(p)));
818 assert!((area(c, Vector::Z) - 1.0).abs() < 1e-15);
819 for (p, w) in c.points.iter().zip(&c.outward) {
820 let away = Vector::new(p.x - 0.5, p.y - 0.5, 0.0);
821 assert!(w.dot(away) > 0.0, "{w:?} at {p:?}");
822 assert!(w.z == 0.0);
823 }
824
825 let s = section(&m, &horizontal(1.0)).unwrap();
828 assert_eq!(s.coplanar.len(), 8);
829 for &t in &s.coplanar {
830 assert!(
831 m.triangles[t as usize]
832 .iter()
833 .all(|&k| m.positions[k as usize].z == 1.0)
834 );
835 }
836 assert_eq!(s.chains.len(), 1);
837 assert_eq!(s.chains[0].points.len(), 8);
838 assert!((area(&s.chains[0], Vector::Z) - 1.0).abs() < 1e-15);
839
840 let s = section(&m, &horizontal(0.0)).unwrap();
843 assert_eq!(s.coplanar.len(), 8);
844 assert!(s.chains.is_empty());
845 let s = section(&m, &plane(Point::ORIGIN, -Vector::Z)).unwrap();
846 assert_eq!(s.chains.len(), 1);
847 let c = &s.chains[0];
848 assert!((area(c, -Vector::Z) - 1.0).abs() < 1e-15);
849 for (p, w) in c.points.iter().zip(&c.outward) {
850 assert!(w.dot(Vector::new(p.x - 0.5, p.y - 0.5, 0.0)) > 0.0);
851 }
852
853 let s = section(&m, &plane(Point::ORIGIN, Vector::new(1.0, -1.0, 0.0))).unwrap();
856 assert!(s.unlinked.is_empty());
857 assert_eq!(s.chains.len(), 1);
858 assert!(s.chains[0].closed);
859 assert!(
860 (area(&s.chains[0], Vector::new(1.0, -1.0, 0.0) / 2f64.sqrt()) - 2f64.sqrt()).abs()
861 < 1e-14
862 );
863 }
864
865 #[test]
866 fn every_lattice_plane_of_a_cube_cuts_it_once_or_not_at_all() {
867 let m = cube(4);
871 for axis in 0..3 {
872 for sign in [1.0, -1.0] {
873 for layer in 0..=4 {
874 let mut normal = [0.0; 3];
875 normal[axis] = sign;
876 let normal = Vector::from_array(normal);
877 let mut origin = [0.5; 3];
878 origin[axis] = f64::from(layer) / 4.0;
879 let s = section(&m, &plane(Point::from_array(origin), normal)).unwrap();
880 let inside = if sign > 0.0 { layer > 0 } else { layer < 4 };
881 let face = layer == 0 || layer == 4;
882 assert_eq!(s.coplanar.len(), if face { 32 } else { 0 });
883 assert!(s.unlinked.is_empty());
884 assert_eq!(s.chains.len(), usize::from(inside), "{axis} {sign} {layer}");
885 if let Some(c) = s.chains.first() {
886 assert!(c.closed);
887 assert_eq!(c.points.len(), 16);
888 assert!((area(c, normal) - 1.0).abs() < 1e-15);
889 }
890 }
891 }
892 }
893 }
894
895 proptest! {
896 #[test]
900 fn a_plane_through_a_cube_cuts_one_convex_loop_facing_out(
901 (a, b) in (-3.2..3.2f64, 0.0..3.2f64),
902 at in (0.3..0.7f64, 0.3..0.7f64, 0.3..0.7f64),
903 ) {
904 let m = cube(3);
905 let normal = Vector::new(a.cos() * b.sin(), a.sin() * b.sin(), b.cos());
906 let s = section(&m, &plane(Point::new(at.0, at.1, at.2), normal)).unwrap();
907 prop_assert!(s.unlinked.is_empty() && s.coplanar.is_empty());
908 prop_assert_eq!(s.chains.len(), 1);
909 let c = &s.chains[0];
910 prop_assert!(c.closed);
911 prop_assert!(area(c, normal) > 0.0);
912 let centre = Point::centroid(&c.points).unwrap();
913 for (p, w) in c.points.iter().zip(&c.outward) {
914 prop_assert!(w.dot(*p - centre) > 0.0);
915 prop_assert!((w.magnitude() - 1.0).abs() < 1e-15);
916 prop_assert!(w.dot(normal).abs() < 1e-15);
917 }
918 }
919 }
920
921 #[test]
922 fn a_vertex_touching_the_plane_from_below_adds_no_chain() {
923 let points = vec![
925 Point::new(-1.0, -1.0, 0.0),
926 Point::new(1.0, -1.0, 0.0),
927 Point::new(1.0, 1.0, 0.0),
928 Point::new(-1.0, 1.0, 0.0),
929 Point::new(0.0, 0.0, 1.0),
930 ];
931 let triangles = vec![
932 [0, 1, 4],
933 [1, 2, 4],
934 [2, 3, 4],
935 [3, 0, 4],
936 [0, 2, 1],
937 [0, 3, 2],
938 ];
939 let s = section(&mesh(points, triangles), &horizontal(1.0)).unwrap();
940 assert_eq!(s, Section::default());
941 }
942
943 #[test]
944 fn an_edge_of_three_sheets_is_reported_and_every_chain_ends_on_it() {
945 let mut points = vec![Point::new(0.0, 0.0, -1.0), Point::new(0.0, 0.0, 1.0)];
947 let mut triangles = Vec::new();
948 for k in 0..3 {
949 let a = TAU * f64::from(k) / 3.0;
950 let (b, t) = (index(points.len()), index(points.len() + 1));
951 points.extend([
952 Point::new(a.cos(), a.sin(), -1.0),
953 Point::new(a.cos(), a.sin(), 1.0),
954 ]);
955 triangles.extend([[0, b, t], [0, t, 1]]);
956 }
957 let s = section(&mesh(points, triangles), &horizontal(0.0)).unwrap();
958 assert_eq!(s.unlinked, [[0, 1]]);
959 assert_eq!(s.chains.len(), 3);
960 for c in &s.chains {
961 assert!(!c.closed);
962 assert_eq!(c.points[0], Point::ORIGIN);
963 assert!((c.points.last().unwrap().to_vector().magnitude() - 1.0).abs() < 1e-15);
964 }
965
966 let points = vec![
968 Point::new(0.0, 0.0, -1.0),
969 Point::new(0.0, 0.0, 1.0),
970 Point::new(1.0, 0.0, 0.0),
971 Point::new(-1.0, 0.0, 0.0),
972 ];
973 let s = section(&mesh(points, vec![[0, 2, 1], [0, 3, 1]]), &horizontal(0.0)).unwrap();
974 assert_eq!(s.unlinked, [[0, 1]]);
975 assert_eq!(s.chains.len(), 2);
976 }
977
978 #[test]
979 fn a_single_precision_mesh_is_cut_through_its_rings_exactly() {
980 #[allow(clippy::cast_possible_truncation)]
982 let single = |v: f64| f64::from(v as f32);
983 let mut m = cylinder(1.3, 1.0, 40, 11);
984 for p in &mut m.positions {
985 *p = Point::new(single(p.x), single(p.y), single(p.z));
986 }
987 let ring = single(0.3);
988 let s = section(&m, &horizontal(ring)).unwrap();
989 assert_eq!(s.chains.len(), 1);
990 let c = &s.chains[0];
991 assert!(c.closed && c.points.len() == 40);
992 assert!(
993 c.points
994 .iter()
995 .all(|p| p.z == ring && m.positions.contains(p))
996 );
997 let s = section(&m, &horizontal(0.3)).unwrap();
999 assert_eq!(s.chains.len(), 1);
1000 assert!(s.chains[0].closed && s.chains[0].points.len() == 80);
1001 }
1002
1003 #[test]
1004 fn a_stack_is_the_sections_of_the_moved_planes_in_order() {
1005 let m = cylinder(1.0, 2.0, 32, 5);
1006 let base = plane(Point::new(0.1, 0.2, 0.0), Vector::new(0.1, 0.2, 1.0));
1007 let offsets = [1.5, 0.25, 0.5, 1.0, -3.0];
1008 let stack = sections(&m, &base, &offsets).unwrap();
1009 assert_eq!(stack.len(), offsets.len());
1010 for (s, &d) in stack.iter().zip(&offsets) {
1011 let moved = Plane::through(base.origin() + base.normal().vector() * d, base.normal());
1012 assert_eq!(*s, section(&m, &moved).unwrap());
1013 }
1014 assert_eq!(stack[1].chains.len(), 1);
1015 assert!(stack[4].chains.is_empty());
1016 assert!(sections(&m, &base, &[f64::NAN]).is_err());
1017 }
1018
1019 #[test]
1020 fn a_bad_index_or_position_is_refused() {
1021 let mut m = cube(1);
1022 m.triangles.push([0, 1, 99]);
1023 assert!(section(&m, &horizontal(0.5)).is_err());
1024 let mut m = cube(1);
1025 m.positions[0].x = f64::NAN;
1026 assert!(section(&m, &horizontal(0.5)).is_err());
1027 }
1028}