use std::f64::consts::PI;
use smallvec::SmallVec;
use brepkit_math::predicates::point_in_polygon;
use brepkit_math::traits::ParametricSurface;
use brepkit_math::vec::{Point2, Point3, Vec3};
use brepkit_topology::Topology;
use brepkit_topology::face::{FaceId, FaceSurface};
use crate::CheckError;
use crate::classify::ray_surface;
use crate::util::{face_polygon, point_in_polygon_3d};
const RAY_T_MIN: f64 = 1e-12;
const HALF_SPACE_EPS: f64 = 1e-10;
const COINCIDENT_SQ: f64 = 1e-12;
#[inline]
fn unwrap_angle(prev: f64, next: f64) -> f64 {
let tau = std::f64::consts::TAU;
let diff = next - prev;
prev + diff - tau * ((diff + PI) / tau).floor()
}
fn build_uv_boundary<F>(verts: &[Point3], project: &F, v_periodic: bool) -> Vec<(f64, f64)>
where
F: Fn(Point3) -> (f64, f64),
{
let mut uv: Vec<(f64, f64)> = verts.iter().map(|&p| project(p)).collect();
for i in 1..uv.len() {
uv[i].0 = unwrap_angle(uv[i - 1].0, uv[i].0);
if v_periodic {
uv[i].1 = unwrap_angle(uv[i - 1].1, uv[i].1);
}
}
uv
}
fn point_in_uv_boundary(
hit_u: f64,
hit_v: f64,
uv_boundary: &[(f64, f64)],
v_periodic: bool,
) -> bool {
let u_min = uv_boundary
.iter()
.map(|(u, _)| *u)
.fold(f64::INFINITY, f64::min);
let u_max = uv_boundary
.iter()
.map(|(u, _)| *u)
.fold(f64::NEG_INFINITY, f64::max);
let u_center = (u_min + u_max) * 0.5;
let hu = unwrap_angle(u_center, hit_u);
let hv = if v_periodic {
let v_min = uv_boundary
.iter()
.map(|(_, v)| *v)
.fold(f64::INFINITY, f64::min);
let v_max = uv_boundary
.iter()
.map(|(_, v)| *v)
.fold(f64::NEG_INFINITY, f64::max);
let v_center = (v_min + v_max) * 0.5;
unwrap_angle(v_center, hit_v)
} else {
hit_v
};
let poly: Vec<Point2> = uv_boundary
.iter()
.map(|(u, v)| Point2::new(*u, *v))
.collect();
let test = Point2::new(hu, hv);
point_in_polygon(test, &poly)
}
pub fn polygon_normal(verts: &[Point3]) -> Vec3 {
crate::util::polygon_normal(verts)
}
fn hit_in_inner_wire_3d(
topo: &Topology,
face_id: FaceId,
hit: Point3,
normal: &Vec3,
) -> Result<bool, CheckError> {
for &iw in topo.face(face_id)?.inner_wires() {
let hole = crate::util::wire_polygon(topo, iw)?;
if hole.len() >= 3 && point_in_polygon_3d(&hit, &hole, normal) {
return Ok(true);
}
}
Ok(false)
}
fn hit_in_inner_wire_uv<F>(
topo: &Topology,
face_id: FaceId,
hit_u: f64,
hit_v: f64,
project: &F,
v_periodic: bool,
) -> Result<bool, CheckError>
where
F: Fn(Point3) -> (f64, f64),
{
for &iw in topo.face(face_id)?.inner_wires() {
let hole = crate::util::wire_polygon(topo, iw)?;
if hole.len() < 3 {
continue;
}
let uv_hole = build_uv_boundary(&hole, project, v_periodic);
if point_in_uv_boundary(hit_u, hit_v, &uv_hole, v_periodic) {
return Ok(true);
}
}
Ok(false)
}
fn count_analytic_crossings<F>(
topo: &Topology,
face_id: FaceId,
origin: Point3,
direction: Vec3,
roots: &SmallVec<[f64; 4]>,
project: F,
v_periodic: bool,
) -> Result<u32, CheckError>
where
F: Fn(Point3) -> (f64, f64),
{
if roots.is_empty() {
return Ok(0);
}
let verts = face_polygon(topo, face_id)?;
let is_full_surface = verts.len() < 3 || {
let ref_pt = verts[0];
verts
.iter()
.all(|v| (*v - ref_pt).length_squared() < COINCIDENT_SQ)
};
if is_full_surface {
#[allow(clippy::cast_possible_truncation)]
return Ok(roots.iter().filter(|&&t| t > RAY_T_MIN).count() as u32);
}
let uv_boundary = build_uv_boundary(&verts, &project, v_periodic);
let mut crossings = 0u32;
for &t in roots {
if t <= RAY_T_MIN {
continue;
}
let hit = origin + direction * t;
let (hit_u, hit_v) = project(hit);
if point_in_uv_boundary(hit_u, hit_v, &uv_boundary, v_periodic)
&& !hit_in_inner_wire_uv(topo, face_id, hit_u, hit_v, &project, v_periodic)?
{
crossings += 1;
}
}
Ok(crossings)
}
fn count_3d_polygon_crossings(
topo: &Topology,
face_id: FaceId,
origin: Point3,
direction: Vec3,
roots: &SmallVec<[f64; 4]>,
) -> Result<u32, CheckError> {
if roots.is_empty() {
return Ok(0);
}
let verts = face_polygon(topo, face_id)?;
if verts.len() < 3 {
return Ok(0);
}
let mut normal = polygon_normal(&verts);
let face = topo.face(face_id)?;
if face.is_reversed() {
normal = -normal;
}
let ref_pt = verts[0];
let mut crossings = 0u32;
for &t in roots {
if t <= RAY_T_MIN {
continue;
}
let hit = origin + direction * t;
let side = (hit - ref_pt).dot(normal);
if side < -HALF_SPACE_EPS {
continue;
}
if point_in_polygon_3d(&hit, &verts, &normal)
&& !hit_in_inner_wire_3d(topo, face_id, hit, &normal)?
{
crossings += 1;
}
}
Ok(crossings)
}
#[allow(clippy::too_many_lines)]
pub fn count_face_ray_crossings(
topo: &Topology,
face_id: FaceId,
origin: Point3,
direction: Vec3,
) -> Result<u32, CheckError> {
let face = topo.face(face_id)?;
match face.surface() {
FaceSurface::Plane { normal, d } => {
ray_plane_crossings(topo, face_id, origin, direction, *normal, *d)
}
FaceSurface::Cylinder(cyl) => {
let cyl = cyl.clone();
let roots = ray_surface::ray_cylinder(origin, direction, &cyl);
count_analytic_crossings(
topo,
face_id,
origin,
direction,
&roots,
|p| cyl.project_point(p),
false,
)
}
FaceSurface::Cone(cone) => {
let cone = cone.clone();
let roots = ray_surface::ray_cone(origin, direction, &cone);
count_analytic_crossings(
topo,
face_id,
origin,
direction,
&roots,
|p| cone.project_point(p),
false,
)
}
FaceSurface::Sphere(sph) => {
let sph = sph.clone();
let roots = ray_surface::ray_sphere(origin, direction, &sph);
count_3d_polygon_crossings(topo, face_id, origin, direction, &roots)
}
FaceSurface::Torus(tor) => {
let tor = tor.clone();
let roots = ray_surface::ray_torus(origin, direction, &tor);
count_analytic_crossings(
topo,
face_id,
origin,
direction,
&roots,
|p| tor.project_point(p),
true,
)
}
FaceSurface::Nurbs(surface) => {
ray_crossings_nurbs(topo, face_id, origin, direction, surface)
}
}
}
fn ray_plane_crossings(
topo: &Topology,
face_id: FaceId,
origin: Point3,
direction: Vec3,
normal: Vec3,
d: f64,
) -> Result<u32, CheckError> {
let t = match ray_surface::ray_plane(origin, direction, normal, d) {
Some(t) => t,
None => return Ok(0),
};
let hit = origin + direction * t;
let verts = face_polygon(topo, face_id)?;
if verts.len() < 3 {
return Ok(0);
}
if point_in_polygon_3d(&hit, &verts, &normal)
&& !hit_in_inner_wire_3d(topo, face_id, hit, &normal)?
{
Ok(1)
} else {
Ok(0)
}
}
fn ray_crossings_nurbs(
topo: &Topology,
face_id: FaceId,
origin: Point3,
direction: Vec3,
surface: &brepkit_math::nurbs::surface::NurbsSurface,
) -> Result<u32, CheckError> {
let hits = ray_surface::ray_nurbs(origin, direction, surface, 20)?;
if hits.is_empty() {
return Ok(0);
}
let verts = face_polygon(topo, face_id)?;
if verts.len() < 3 {
#[allow(clippy::cast_possible_truncation)]
return Ok(hits.len() as u32);
}
let project = |p: Point3| -> (f64, f64) { surface.project_point(p) };
let uv_boundary = build_uv_boundary(&verts, &project, false);
let mut crossings = 0u32;
for (_, hit_u, hit_v) in &hits {
if point_in_uv_boundary(*hit_u, *hit_v, &uv_boundary, false)
&& !hit_in_inner_wire_uv(topo, face_id, *hit_u, *hit_v, &project, false)?
{
crossings += 1;
}
}
Ok(crossings)
}