Skip to content

Commit

Permalink
Merge pull request #149 from hannobraun/approximation
Browse files Browse the repository at this point in the history
Prepare for adding approximation test suite
  • Loading branch information
hannobraun authored Feb 8, 2022
2 parents d86964a + d378ce8 commit 8c4dfe7
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 28 deletions.
158 changes: 158 additions & 0 deletions src/kernel/approximation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use std::collections::HashSet;

use decorum::R64;
use parry3d_f64::shape::Segment;

use crate::math::Point;

/// An approximation of an edge, multiple edges, or a face
pub struct Approximation {
/// All points that make up the approximation
///
/// These could be actual vertices from the model, points that approximate
/// an edge, or points that approximate a face.
pub points: Vec<Point<3>>,

/// Segments that approximate edges
///
/// Every approximation will involve edges, typically, and these are
/// approximated by these segments.
///
/// All the points of these segments will also be available in the `points`
/// field of this struct. This can be verified by calling
/// [`Approximation::validate`].
pub segments: Vec<Segment>,
}

impl Approximation {
/// Validate the approximation
///
/// Returns an `Err(ValidationError)`, if the validation is not valid. See
/// [`ValidationError`] for the ways that the approximation can be invalid.
pub fn validate(&self) -> Result<(), ValidationError> {
let mut duplicate_points = Vec::new();
let mut duplicate_segments = Vec::new();
let mut invalid_segments = Vec::new();
let mut segments_with_invalid_points = Vec::new();

// Verify that there are no duplicate points
let mut points = HashSet::new();
for &point in &self.points {
let point_r64 = point_to_r64(point);

if points.contains(&point_r64) {
duplicate_points.push(point);
}

points.insert(point_r64);
}

let mut segments = HashSet::new();
for &segment @ Segment { a, b } in &self.segments {
// Verify that there are no duplicate segments
let ab = [point_to_r64(a), point_to_r64(b)];
let ba = [point_to_r64(b), point_to_r64(a)];
if segments.contains(&ab) {
duplicate_segments.push(segment);
}
segments.insert(ab);
segments.insert(ba);

// Verify that segments are actually segments
if a == b {
invalid_segments.push(segment);
}

// Verify that segments refer to valid points
if !(self.points.contains(&a) && self.points.contains(&b)) {
segments_with_invalid_points.push(segment);
}
}

if !(duplicate_points.is_empty()
&& duplicate_segments.is_empty()
&& invalid_segments.is_empty()
&& segments_with_invalid_points.is_empty())
{
return Err(ValidationError {
duplicate_points,
duplicate_segments,
invalid_segments,
segments_with_invalid_points,
});
}

Ok(())
}
}

/// Error returned by [`Approximation::validate`]
#[derive(Debug)]
pub struct ValidationError {
/// Points that are duplicated
pub duplicate_points: Vec<Point<3>>,

/// Segments that are duplicated
pub duplicate_segments: Vec<Segment>,

/// Segments that have two equal points
pub invalid_segments: Vec<Segment>,

/// Segments that do not refer to points from the approximation
pub segments_with_invalid_points: Vec<Segment>,
}

fn point_to_r64(point: Point<3>) -> [R64; 3] {
[point.x.into(), point.y.into(), point.z.into()]
}

#[cfg(test)]
mod tests {
use nalgebra::point;
use parry3d_f64::shape::Segment;

use super::Approximation;

#[test]
fn test_validate() {
let a = point![0., 1., 2.];
let b = point![1., 2., 3.];
let c = point![3., 5., 8.];

let valid = Approximation {
points: vec![a, b, c],
segments: vec![Segment { a, b }],
};
assert!(valid.validate().is_ok());

let duplicate_points = Approximation {
points: vec![a, b, c, b],
segments: vec![Segment { a, b }],
};
assert!(duplicate_points.validate().is_err());

let duplicate_segments = Approximation {
points: vec![a, b, c],
segments: vec![Segment { a, b }, Segment { a, b }],
};
assert!(duplicate_segments.validate().is_err());

let duplicate_segments_inverted = Approximation {
points: vec![a, b, c],
segments: vec![Segment { a, b }, Segment { a: b, b: a }],
};
assert!(duplicate_segments_inverted.validate().is_err());

let invalid_segment = Approximation {
points: vec![a, b, c],
segments: vec![Segment { a, b: a }],
};
assert!(invalid_segment.validate().is_err());

let segment_with_invalid_point = Approximation {
points: vec![a, c],
segments: vec![Segment { a, b }],
};
assert!(segment_with_invalid_point.validate().is_err());
}
}
1 change: 1 addition & 0 deletions src/kernel/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod approximation;
pub mod geometry;
pub mod shapes;
pub mod topology;
Expand Down
45 changes: 21 additions & 24 deletions src/kernel/topology/edges.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use nalgebra::vector;
use parry3d_f64::{math::Isometry, shape::Segment};
use parry3d_f64::math::Isometry;

use crate::{
kernel::geometry::{Circle, Curve},
kernel::{
approximation::Approximation,
geometry::{Circle, Curve},
},
math::Point,
};

Expand Down Expand Up @@ -62,18 +65,18 @@ impl Edges {
/// Only approximating an edge once, and then referring to that
/// approximation from then on where needed, would take care of these two
/// problems.
pub fn approx(&self, tolerance: f64) -> Approx {
let mut vertices = Vec::new();
pub fn approx(&self, tolerance: f64) -> Approximation {
let mut points = Vec::new();
let mut segments = Vec::new();

for cycle in &self.cycles {
let approx = cycle.approx(tolerance);

vertices.extend(approx.vertices);
points.extend(approx.points);
segments.extend(approx.segments);
}

Approx { vertices, segments }
Approximation { points, segments }
}
}

Expand All @@ -92,22 +95,22 @@ impl Cycle {
///
/// `tolerance` defines how far the approximation is allowed to deviate from
/// the actual cycle.
pub fn approx(&self, tolerance: f64) -> Approx {
let mut vertices = Vec::new();
pub fn approx(&self, tolerance: f64) -> Approximation {
let mut points = Vec::new();
let mut segments = Vec::new();

for edge in &self.edges {
let approx = edge.approx(tolerance);

vertices.extend(approx.vertices);
points.extend(approx.points);
segments.extend(approx.segments);
}

// As this is a cycle, the last vertex of an edge could be identical to
// the first vertex of the next. Let's remove those duplicates.
vertices.dedup();
points.dedup();

Approx { vertices, segments }
Approximation { points, segments }
}
}

Expand Down Expand Up @@ -172,20 +175,20 @@ impl Edge {
///
/// `tolerance` defines how far the approximation is allowed to deviate from
/// the actual edge.
pub fn approx(&self, tolerance: f64) -> Approx {
let mut vertices = Vec::new();
self.curve.approx(tolerance, &mut vertices);
pub fn approx(&self, tolerance: f64) -> Approximation {
let mut points = Vec::new();
self.curve.approx(tolerance, &mut points);

if self.reverse {
vertices.reverse()
points.reverse()
}

let mut segment_vertices = vertices.clone();
let mut segment_vertices = points.clone();
if self.vertices.is_none() {
// The edge has no vertices, which means it connects to itself. We
// need to reflect that in the approximation.

if let Some(&vertex) = vertices.first() {
if let Some(&vertex) = points.first() {
segment_vertices.push(vertex);
}
}
Expand All @@ -198,12 +201,6 @@ impl Edge {
segments.push([v0, v1].into());
}

Approx { vertices, segments }
Approximation { points, segments }
}
}

/// An approximation of one or more edges
pub struct Approx {
pub vertices: Vec<Point<3>>,
pub segments: Vec<Segment>,
}
15 changes: 11 additions & 4 deletions src/kernel/topology/faces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use parry3d_f64::{
query::Ray as Ray3,
shape::{Segment as Segment3, Triangle},
};
use tracing::warn;

use crate::{
debug::{DebugInfo, TriangleEdgeCheck},
Expand Down Expand Up @@ -110,8 +111,14 @@ impl Face {
Self::Face { edges, surface } => {
let approx = edges.approx(tolerance);

let vertices: Vec<_> = approx
.vertices
// Can't make this a panic, as the current approximation code
// actually produces invalid approximations.
if let Err(err) = approx.validate() {
warn!("Invalid approximation: {:?}", err);
}

let points: Vec<_> = approx
.points
.into_iter()
.map(|vertex| {
// Can't panic, unless the approximation wrongfully
Expand All @@ -136,11 +143,11 @@ impl Face {
// We're also going to need a point outside of the polygon, for
// the point-in-polygon tests.
let aabb = AABB::from_points(
vertices.iter().map(|vertex| &vertex.value),
points.iter().map(|vertex| &vertex.value),
);
let outside = aabb.maxs * 2.;

let mut triangles = triangulate(vertices);
let mut triangles = triangulate(points);
let face_as_polygon = segments;

triangles.retain(|t| {
Expand Down

0 comments on commit 8c4dfe7

Please sign in to comment.