Merge pull request #386 from hannobraun/triangulation

Consolidate triangulation code in `fj_kernel::algorithms::triangulation`
This commit is contained in:
Hanno Braun 2022-03-21 17:45:52 +01:00 committed by GitHub
commit 2ef0550feb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 176 additions and 181 deletions

1
Cargo.lock generated
View File

@ -633,6 +633,7 @@ dependencies = [
"fj", "fj",
"fj-debug", "fj-debug",
"fj-host", "fj-host",
"fj-kernel",
"fj-math", "fj-math",
"fj-operations", "fj-operations",
"futures", "futures",

View File

@ -44,6 +44,10 @@ path = "../fj-debug"
version = "0.5.0" version = "0.5.0"
path = "../fj-host" path = "../fj-host"
[dependencies.fj-kernel]
version = "0.5.0"
path = "../fj-kernel"
[dependencies.fj-math] [dependencies.fj-math]
version = "0.5.0" version = "0.5.0"
path = "../fj-math" path = "../fj-math"

View File

@ -11,6 +11,7 @@ use std::{collections::HashMap, time::Instant};
use fj_debug::DebugInfo; use fj_debug::DebugInfo;
use fj_host::Model; use fj_host::Model;
use fj_kernel::algorithms::triangulate;
use fj_math::{Aabb, Scalar, Triangle}; use fj_math::{Aabb, Scalar, Triangle};
use fj_operations::ToShape as _; use fj_operations::ToShape as _;
use futures::executor::block_on; use futures::executor::block_on;
@ -296,10 +297,12 @@ impl ShapeProcessor {
let mut debug_info = DebugInfo::new(); let mut debug_info = DebugInfo::new();
let mut triangles = Vec::new(); let mut triangles = Vec::new();
shape triangulate(
.to_shape(tolerance, &mut debug_info) shape.to_shape(tolerance, &mut debug_info),
.topology() tolerance,
.triangles(tolerance, &mut triangles, &mut debug_info); &mut triangles,
&mut debug_info,
);
ProcessedShape { ProcessedShape {
aabb, aabb,

View File

@ -1,13 +1,166 @@
use fj_math::Scalar; use std::collections::BTreeSet;
use parry2d_f64::utils::point_in_triangle::{corner_direction, Orientation};
use fj_debug::{DebugInfo, TriangleEdgeCheck};
use fj_math::{Aabb, Scalar, Segment, Triangle};
use parry2d_f64::{
query::{Ray as Ray2, RayCast as _},
utils::point_in_triangle::{corner_direction, Orientation},
};
use parry3d_f64::query::Ray as Ray3;
use spade::HasPosition; use spade::HasPosition;
use crate::geometry; use crate::{geometry, shape::Shape, topology::Face};
use super::Approximation;
/// Triangulate a shape
pub fn triangulate(
mut shape: Shape,
tolerance: Scalar,
out: &mut Vec<Triangle<3>>,
debug_info: &mut DebugInfo,
) {
for face in shape.topology().faces() {
let face = face.get();
match &*face {
Face::Face { surface, color, .. } => {
let surface = surface.get();
let approx = Approximation::for_face(&face, tolerance);
let points: Vec<_> = approx
.points
.into_iter()
.map(|vertex| {
// Can't panic, unless the approximation wrongfully
// generates points that are not in the surface.
surface.point_model_to_surface(vertex)
})
.collect();
let segments: Vec<_> = approx
.segments
.into_iter()
.map(|segment| {
let [a, b] = segment.points();
// Can't panic, unless the approximation wrongfully
// generates points that are not in the surface.
let a = surface.point_model_to_surface(a);
let b = surface.point_model_to_surface(b);
[a, b]
})
.collect();
// We're also going to need a point outside of the polygon, for
// the point-in-polygon tests.
let aabb = Aabb::<2>::from_points(
points.iter().map(|vertex| vertex.native()),
);
let outside = aabb.max * 2.;
let mut triangles = delaunay(points);
let face_as_polygon = segments;
triangles.retain(|t| {
for segment in [t[0], t[1], t[2], t[0]].windows(2) {
// This can't panic, as we passed `2` to `windows`. It
// can be cleaned up a bit, once `array_windows` is
// stable.
let segment = [segment[0], segment[1]];
let inverted_segment = [segment[1], segment[0]];
// If the segment is an edge of the face, we don't need
// to take a closer look.
if face_as_polygon.contains(&segment) {
continue;
}
if face_as_polygon.contains(&inverted_segment) {
continue;
}
// To determine if the edge is within the polygon, we
// determine if its center point is in the polygon.
let center = segment[0]
+ (segment[1] - segment[0]) / Scalar::TWO;
let origin = center;
let dir = outside - center;
let ray = Ray2 {
origin: origin.to_na(),
dir: dir.to_na(),
};
let mut check = TriangleEdgeCheck::new(Ray3 {
origin: surface
.point_surface_to_model(&origin)
.to_na(),
dir: surface.vector_surface_to_model(&dir).to_na(),
});
// We need to keep track of where our ray hits the
// edges. Otherwise, if the ray hits a vertex, we might
// count that hit twice, as every vertex is attached to
// two edges.
let mut hits = BTreeSet::new();
// Use ray-casting to determine if `center` is within
// the face-polygon.
for edge in &face_as_polygon {
// Please note that we if we get to this point, then
// the point is not on a polygon edge, due to the
// check above. We don't need to handle any edge
// cases that would arise from that case.
let edge =
Segment::from(edge.map(|point| point.native()));
let intersection = edge
.to_parry()
.cast_local_ray(&ray, f64::INFINITY, true)
.map(Scalar::from_f64);
if let Some(t) = intersection {
// Due to slight inaccuracies, we might get
// different values for the same intersections.
// Let's round `t` before using it.
let eps = 1_000_000.0;
let t = (t * eps).round() / eps;
if hits.insert(t) {
check.hits.push(t.into_f64());
}
}
}
debug_info.triangle_edge_checks.push(check);
if hits.len() % 2 == 0 {
// The segment is outside of the face. This means we
// can throw away the whole triangle.
return false;
}
}
// If we didn't throw away the triangle up till now, this
// means all its edges are within the face.
true
});
out.extend(triangles.into_iter().map(|triangle| {
let [a, b, c] = triangle.map(|point| point.canonical());
let mut t = Triangle::from([a, b, c]);
t.set_color(*color);
t
}));
}
Face::Triangles(triangles) => out.extend(triangles),
}
}
}
/// Create a Delaunay triangulation of all points /// Create a Delaunay triangulation of all points
pub fn triangulate( fn delaunay(points: Vec<geometry::Point<2>>) -> Vec<[geometry::Point<2>; 3]> {
points: Vec<geometry::Point<2>>,
) -> Vec<[geometry::Point<2>; 3]> {
use spade::Triangulation as _; use spade::Triangulation as _;
let triangulation = spade::DelaunayTriangulation::<_>::bulk_load(points) let triangulation = spade::DelaunayTriangulation::<_>::bulk_load(points)
@ -39,7 +192,7 @@ pub fn triangulate(
triangles triangles
} }
// Enables the use of `SurfacePoint` in the triangulation. // Enables the use of `geometry::Point` in the triangulation.
impl HasPosition for geometry::Point<2> { impl HasPosition for geometry::Point<2> {
type Scalar = Scalar; type Scalar = Scalar;

View File

@ -1,7 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use fj_debug::DebugInfo; use fj_math::{Point, Scalar, Vector};
use fj_math::{Point, Scalar, Triangle, Vector};
use crate::{ use crate::{
geometry::{Circle, Curve, Line}, geometry::{Circle, Curve, Line},
@ -238,18 +237,6 @@ impl Topology<'_> {
pub fn faces(&self) -> Iter<Face> { pub fn faces(&self) -> Iter<Face> {
Iter::new(self.geometry.faces) Iter::new(self.geometry.faces)
} }
/// Triangulate the shape
pub fn triangles(
&self,
tolerance: Scalar,
out: &mut Vec<Triangle<3>>,
debug_info: &mut DebugInfo,
) {
for face in &*self.geometry.faces {
face.get().triangles(tolerance, out, debug_info);
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,18 +1,8 @@
use std::{ use std::hash::{Hash, Hasher};
collections::BTreeSet,
hash::{Hash, Hasher},
};
use fj_debug::{DebugInfo, TriangleEdgeCheck}; use fj_math::Triangle;
use fj_math::{Aabb, Scalar, Segment, Triangle};
use parry2d_f64::query::{Ray as Ray2, RayCast as _};
use parry3d_f64::query::Ray as Ray3;
use crate::{ use crate::{geometry::Surface, shape::Handle};
algorithms::{triangulate, Approximation},
geometry::Surface,
shape::Handle,
};
use super::edges::Cycle; use super::edges::Cycle;
@ -89,149 +79,6 @@ impl Face {
} }
} }
} }
/// Triangulate the face
pub fn triangles(
&self,
tolerance: Scalar,
out: &mut Vec<Triangle<3>>,
debug_info: &mut DebugInfo,
) {
match self {
Self::Face { surface, color, .. } => {
let surface = surface.get();
let approx = Approximation::for_face(self, tolerance);
let points: Vec<_> = approx
.points
.into_iter()
.map(|vertex| {
// Can't panic, unless the approximation wrongfully
// generates points that are not in the surface.
surface.point_model_to_surface(vertex)
})
.collect();
let segments: Vec<_> = approx
.segments
.into_iter()
.map(|segment| {
let [a, b] = segment.points();
// Can't panic, unless the approximation wrongfully
// generates points that are not in the surface.
let a = surface.point_model_to_surface(a);
let b = surface.point_model_to_surface(b);
[a, b]
})
.collect();
// We're also going to need a point outside of the polygon, for
// the point-in-polygon tests.
let aabb = Aabb::<2>::from_points(
points.iter().map(|vertex| vertex.native()),
);
let outside = aabb.max * 2.;
let mut triangles = triangulate(points);
let face_as_polygon = segments;
triangles.retain(|t| {
for segment in [t[0], t[1], t[2], t[0]].windows(2) {
// This can't panic, as we passed `2` to `windows`. It
// can be cleaned up a bit, once `array_windows` is
// stable.
let segment = [segment[0], segment[1]];
let inverted_segment = [segment[1], segment[0]];
// If the segment is an edge of the face, we don't need
// to take a closer look.
if face_as_polygon.contains(&segment) {
continue;
}
if face_as_polygon.contains(&inverted_segment) {
continue;
}
// To determine if the edge is within the polygon, we
// determine if its center point is in the polygon.
let center = segment[0]
+ (segment[1] - segment[0]) / Scalar::TWO;
let origin = center;
let dir = outside - center;
let ray = Ray2 {
origin: origin.to_na(),
dir: dir.to_na(),
};
let mut check = TriangleEdgeCheck::new(Ray3 {
origin: surface
.point_surface_to_model(&origin)
.to_na(),
dir: surface.vector_surface_to_model(&dir).to_na(),
});
// We need to keep track of where our ray hits the
// edges. Otherwise, if the ray hits a vertex, we might
// count that hit twice, as every vertex is attached to
// two edges.
let mut hits = BTreeSet::new();
// Use ray-casting to determine if `center` is within
// the face-polygon.
for edge in &face_as_polygon {
// Please note that we if we get to this point, then
// the point is not on a polygon edge, due to the
// check above. We don't need to handle any edge
// cases that would arise from that case.
let edge =
Segment::from(edge.map(|point| point.native()));
let intersection = edge
.to_parry()
.cast_local_ray(&ray, f64::INFINITY, true)
.map(Scalar::from_f64);
if let Some(t) = intersection {
// Due to slight inaccuracies, we might get
// different values for the same intersections.
// Let's round `t` before using it.
let eps = 1_000_000.0;
let t = (t * eps).round() / eps;
if hits.insert(t) {
check.hits.push(t.into_f64());
}
}
}
debug_info.triangle_edge_checks.push(check);
if hits.len() % 2 == 0 {
// The segment is outside of the face. This means we
// can throw away the whole triangle.
return false;
}
}
// If we didn't throw away the triangle up till now, this
// means all its edges are within the face.
true
});
out.extend(triangles.into_iter().map(|triangle| {
let [a, b, c] = triangle.map(|point| point.canonical());
let mut t = Triangle::from([a, b, c]);
t.set_color(*color);
t
}));
}
Self::Triangles(triangles) => out.extend(triangles),
}
}
} }
impl PartialEq for Face { impl PartialEq for Face {