diff --git a/Cargo.lock b/Cargo.lock index 2c10d6bc0..ecb2e9e5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,6 +633,7 @@ dependencies = [ "fj", "fj-debug", "fj-host", + "fj-kernel", "fj-math", "fj-operations", "futures", diff --git a/fj-app/Cargo.toml b/fj-app/Cargo.toml index 1b9da326b..c117455b3 100644 --- a/fj-app/Cargo.toml +++ b/fj-app/Cargo.toml @@ -44,6 +44,10 @@ path = "../fj-debug" version = "0.5.0" path = "../fj-host" +[dependencies.fj-kernel] +version = "0.5.0" +path = "../fj-kernel" + [dependencies.fj-math] version = "0.5.0" path = "../fj-math" diff --git a/fj-app/src/main.rs b/fj-app/src/main.rs index cfbf2aee7..910805161 100644 --- a/fj-app/src/main.rs +++ b/fj-app/src/main.rs @@ -11,6 +11,7 @@ use std::{collections::HashMap, time::Instant}; use fj_debug::DebugInfo; use fj_host::Model; +use fj_kernel::algorithms::triangulate; use fj_math::{Aabb, Scalar, Triangle}; use fj_operations::ToShape as _; use futures::executor::block_on; @@ -296,10 +297,12 @@ impl ShapeProcessor { let mut debug_info = DebugInfo::new(); let mut triangles = Vec::new(); - shape - .to_shape(tolerance, &mut debug_info) - .topology() - .triangles(tolerance, &mut triangles, &mut debug_info); + triangulate( + shape.to_shape(tolerance, &mut debug_info), + tolerance, + &mut triangles, + &mut debug_info, + ); ProcessedShape { aabb, diff --git a/fj-kernel/src/algorithms/triangulation.rs b/fj-kernel/src/algorithms/triangulation.rs index fcd23dd50..e2daeffa1 100644 --- a/fj-kernel/src/algorithms/triangulation.rs +++ b/fj-kernel/src/algorithms/triangulation.rs @@ -1,13 +1,166 @@ -use fj_math::Scalar; -use parry2d_f64::utils::point_in_triangle::{corner_direction, Orientation}; +use std::collections::BTreeSet; + +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 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>, + 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 -pub fn triangulate( - points: Vec>, -) -> Vec<[geometry::Point<2>; 3]> { +fn delaunay(points: Vec>) -> Vec<[geometry::Point<2>; 3]> { use spade::Triangulation as _; let triangulation = spade::DelaunayTriangulation::<_>::bulk_load(points) @@ -39,7 +192,7 @@ pub fn triangulate( triangles } -// Enables the use of `SurfacePoint` in the triangulation. +// Enables the use of `geometry::Point` in the triangulation. impl HasPosition for geometry::Point<2> { type Scalar = Scalar; diff --git a/fj-kernel/src/shape/topology.rs b/fj-kernel/src/shape/topology.rs index 46cb5cc94..6f1866994 100644 --- a/fj-kernel/src/shape/topology.rs +++ b/fj-kernel/src/shape/topology.rs @@ -1,7 +1,6 @@ use std::collections::HashSet; -use fj_debug::DebugInfo; -use fj_math::{Point, Scalar, Triangle, Vector}; +use fj_math::{Point, Scalar, Vector}; use crate::{ geometry::{Circle, Curve, Line}, @@ -238,18 +237,6 @@ impl Topology<'_> { pub fn faces(&self) -> Iter { Iter::new(self.geometry.faces) } - - /// Triangulate the shape - pub fn triangles( - &self, - tolerance: Scalar, - out: &mut Vec>, - debug_info: &mut DebugInfo, - ) { - for face in &*self.geometry.faces { - face.get().triangles(tolerance, out, debug_info); - } - } } #[cfg(test)] diff --git a/fj-kernel/src/topology/faces.rs b/fj-kernel/src/topology/faces.rs index 30529231a..d5120cbf2 100644 --- a/fj-kernel/src/topology/faces.rs +++ b/fj-kernel/src/topology/faces.rs @@ -1,18 +1,8 @@ -use std::{ - collections::BTreeSet, - hash::{Hash, Hasher}, -}; +use std::hash::{Hash, Hasher}; -use fj_debug::{DebugInfo, TriangleEdgeCheck}; -use fj_math::{Aabb, Scalar, Segment, Triangle}; -use parry2d_f64::query::{Ray as Ray2, RayCast as _}; -use parry3d_f64::query::Ray as Ray3; +use fj_math::Triangle; -use crate::{ - algorithms::{triangulate, Approximation}, - geometry::Surface, - shape::Handle, -}; +use crate::{geometry::Surface, shape::Handle}; use super::edges::Cycle; @@ -89,149 +79,6 @@ impl Face { } } } - - /// Triangulate the face - pub fn triangles( - &self, - tolerance: Scalar, - out: &mut Vec>, - 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 {