#![allow(dead_code)]
use brepkit_math::nurbs::curve::NurbsCurve;
use brepkit_math::nurbs::surface::NurbsSurface;
use brepkit_math::vec::{Point3, Vec3};
use brepkit_topology::face::FaceSurface;
use brepkit_topology::vertex::VertexId;
use crate::BlendError;
const TOL: f64 = 1e-6;
pub struct VertexContactData {
pub vertex_pos: Point3,
pub contact_points: Vec<Point3>,
pub face_normals: Vec<Vec3>,
pub radius: f64,
pub is_convex: bool,
pub vertex_id: VertexId,
}
pub struct SphericalCornerResult {
pub surface: FaceSurface,
pub boundary_curves: Vec<NurbsCurve>,
}
fn compute_sphere_center(data: &VertexContactData) -> Result<(Point3, f64), BlendError> {
let mut normal_sum = Vec3::new(0.0, 0.0, 0.0);
for n in &data.face_normals {
normal_sum += *n;
}
let len = normal_sum.length();
if len < TOL {
return Err(BlendError::CornerFailure {
vertex: data.vertex_id,
});
}
let offset = normal_sum * data.radius;
let center = if data.is_convex {
data.vertex_pos + offset
} else {
data.vertex_pos - offset
};
if data.contact_points.is_empty() {
return Err(BlendError::CornerFailure {
vertex: data.vertex_id,
});
}
let sphere_radius = (data.contact_points[0] - center).length();
if sphere_radius < TOL {
return Err(BlendError::CornerFailure {
vertex: data.vertex_id,
});
}
for cp in &data.contact_points {
let dist = (*cp - center).length();
let err = (dist - sphere_radius).abs();
if err > TOL * 100.0 {
return Err(BlendError::CornerFailure {
vertex: data.vertex_id,
});
}
}
Ok((center, sphere_radius))
}
fn build_great_circle_arc(
center: Point3,
radius: f64,
q_start: Point3,
q_end: Point3,
vertex_id: VertexId,
) -> Result<NurbsCurve, BlendError> {
let dir_i = (q_start - center) * (1.0 / radius);
let dir_j = (q_end - center) * (1.0 / radius);
let bisector_raw = dir_i + dir_j;
let bisector_len = bisector_raw.length();
if bisector_len < TOL {
return Err(BlendError::CornerFailure { vertex: vertex_id });
}
let bisector = bisector_raw * (1.0 / bisector_len);
let cos_half = dir_i.dot(bisector);
if cos_half.abs() < TOL {
return Err(BlendError::CornerFailure { vertex: vertex_id });
}
let mid_cp = center + bisector * (radius / cos_half);
let control_points = vec![q_start, mid_cp, q_end];
let weights = vec![1.0, cos_half, 1.0];
let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
Ok(NurbsCurve::new(2, knots, control_points, weights)?)
}
#[allow(clippy::too_many_lines)]
pub fn build_spherical_corner(
data: &VertexContactData,
) -> Result<SphericalCornerResult, BlendError> {
if data.contact_points.len() < 3 {
return Err(BlendError::CornerFailure {
vertex: data.vertex_id,
});
}
let (center, r) = compute_sphere_center(data)?;
let vid = data.vertex_id;
let q1 = data.contact_points[0];
let q2 = data.contact_points[1];
let q3 = data.contact_points[2];
let dir1 = (q1 - center) * (1.0 / r);
let dir2 = (q2 - center) * (1.0 / r);
let dir3 = (q3 - center) * (1.0 / r);
let mid_q1q2 = edge_mid_cp(center, r, dir1, dir2, vid)?;
let mid_q2q3 = edge_mid_cp(center, r, dir2, dir3, vid)?;
let mid_q3q1 = edge_mid_cp(center, r, dir3, dir1, vid)?;
let w_q1q2 = cos_half_angle(dir1, dir2);
let w_q2q3 = cos_half_angle(dir2, dir3);
let w_q3q1 = cos_half_angle(dir3, dir1);
let apex_dir_raw = dir1 + dir2 + dir3;
let apex_dir_len = apex_dir_raw.length();
if apex_dir_len < TOL {
return Err(BlendError::CornerFailure { vertex: vid });
}
let apex_dir = apex_dir_raw * (1.0 / apex_dir_len);
let apex = center + apex_dir * r;
let w_apex = w_q1q2 * w_q2q3 * w_q3q1;
let control_points = vec![
vec![q1, mid_q1q2, q2],
vec![mid_q3q1, apex, mid_q2q3],
vec![q3, q3, q3],
];
let weights = vec![
vec![1.0, w_q1q2, 1.0],
vec![w_q3q1, w_apex, w_q2q3],
vec![1.0, 1.0, 1.0],
];
let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let surface = NurbsSurface::new(2, 2, knots.clone(), knots, control_points, weights)?;
let arc_q1q2 = build_great_circle_arc(center, r, q1, q2, vid)?;
let arc_q2q3 = build_great_circle_arc(center, r, q2, q3, vid)?;
let arc_q3q1 = build_great_circle_arc(center, r, q3, q1, vid)?;
Ok(SphericalCornerResult {
surface: FaceSurface::Nurbs(surface),
boundary_curves: vec![arc_q1q2, arc_q2q3, arc_q3q1],
})
}
pub fn build_n_edge_corner(
data: &VertexContactData,
) -> Result<Vec<SphericalCornerResult>, BlendError> {
let n = data.contact_points.len();
if n < 3 {
return Err(BlendError::CornerFailure {
vertex: data.vertex_id,
});
}
let (center, r) = compute_sphere_center(data)?;
let mut centroid_raw = Vec3::new(0.0, 0.0, 0.0);
for p in &data.contact_points {
centroid_raw += *p - center;
}
centroid_raw = centroid_raw * (1.0 / n as f64);
let centroid_len = centroid_raw.length();
if centroid_len < TOL {
return Err(BlendError::CornerFailure {
vertex: data.vertex_id,
});
}
let centroid = center + centroid_raw * (r / centroid_len);
let mut results = Vec::with_capacity(n);
for i in 0..n {
let j = (i + 1) % n;
let qi = data.contact_points[i];
let qj = data.contact_points[j];
let result = build_triangle_on_sphere(center, r, qi, qj, centroid, data.vertex_id)?;
results.push(result);
}
Ok(results)
}
fn build_triangle_on_sphere(
center: Point3,
radius: f64,
q1: Point3,
q2: Point3,
q3: Point3,
vertex_id: VertexId,
) -> Result<SphericalCornerResult, BlendError> {
let r = radius;
let dir1 = (q1 - center) * (1.0 / r);
let dir2 = (q2 - center) * (1.0 / r);
let dir3 = (q3 - center) * (1.0 / r);
let mid_q1q2 = edge_mid_cp(center, r, dir1, dir2, vertex_id)?;
let mid_q2q3 = edge_mid_cp(center, r, dir2, dir3, vertex_id)?;
let mid_q3q1 = edge_mid_cp(center, r, dir3, dir1, vertex_id)?;
let w_q1q2 = cos_half_angle(dir1, dir2);
let w_q2q3 = cos_half_angle(dir2, dir3);
let w_q3q1 = cos_half_angle(dir3, dir1);
let apex_dir_raw = dir1 + dir2 + dir3;
let apex_dir_len = apex_dir_raw.length();
if apex_dir_len < TOL {
return Err(BlendError::CornerFailure { vertex: vertex_id });
}
let apex = center + apex_dir_raw * (r / apex_dir_len);
let w_apex = w_q1q2 * w_q2q3 * w_q3q1;
let control_points = vec![
vec![q1, mid_q1q2, q2],
vec![mid_q3q1, apex, mid_q2q3],
vec![q3, q3, q3],
];
let weights = vec![
vec![1.0, w_q1q2, 1.0],
vec![w_q3q1, w_apex, w_q2q3],
vec![1.0, 1.0, 1.0],
];
let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let surface = NurbsSurface::new(2, 2, knots.clone(), knots, control_points, weights)?;
let arc_q1q2 = build_great_circle_arc(center, r, q1, q2, vertex_id)?;
let arc_q2q3 = build_great_circle_arc(center, r, q2, q3, vertex_id)?;
let arc_q3q1 = build_great_circle_arc(center, r, q3, q1, vertex_id)?;
Ok(SphericalCornerResult {
surface: FaceSurface::Nurbs(surface),
boundary_curves: vec![arc_q1q2, arc_q2q3, arc_q3q1],
})
}
fn edge_mid_cp(
center: Point3,
radius: f64,
dir_a: Vec3,
dir_b: Vec3,
vertex_id: VertexId,
) -> Result<Point3, BlendError> {
let bisector_raw = dir_a + dir_b;
let bisector_len = bisector_raw.length();
if bisector_len < TOL {
return Err(BlendError::CornerFailure { vertex: vertex_id });
}
let bisector = bisector_raw * (1.0 / bisector_len);
let cos_half = dir_a.dot(bisector);
if cos_half.abs() < TOL {
return Err(BlendError::CornerFailure { vertex: vertex_id });
}
Ok(center + bisector * (radius / cos_half))
}
#[must_use]
fn cos_half_angle(dir_a: Vec3, dir_b: Vec3) -> f64 {
let bisector_raw = dir_a + dir_b;
let bisector_len = bisector_raw.length();
if bisector_len < f64::EPSILON {
return 0.0;
}
let bisector = bisector_raw * (1.0 / bisector_len);
dir_a.dot(bisector)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use brepkit_topology::topology::Topology;
use brepkit_topology::vertex::Vertex;
fn make_vertex_id() -> (Topology, VertexId) {
let mut topo = Topology::new();
let vid = topo.add_vertex(Vertex::new(Point3::new(0.0, 0.0, 0.0), 1e-7));
(topo, vid)
}
fn unit_cube_corner_data(vertex_id: VertexId) -> VertexContactData {
let r = 0.2;
let origin = Point3::new(0.0, 0.0, 0.0);
let nx = Vec3::new(1.0, 0.0, 0.0);
let ny = Vec3::new(0.0, 1.0, 0.0);
let nz = Vec3::new(0.0, 0.0, 1.0);
let normal_sum = nx + ny + nz;
let normal_dir = normal_sum * (1.0 / normal_sum.length());
let center = origin + normal_dir * r;
let q1 = center - nx * r; let q2 = center - ny * r; let q3 = center - nz * r;
VertexContactData {
vertex_pos: origin,
contact_points: vec![q1, q2, q3],
face_normals: vec![nx, ny, nz],
radius: r,
is_convex: true,
vertex_id,
}
}
#[test]
fn test_sphere_center_convex() {
let (_topo, vid) = make_vertex_id();
let data = unit_cube_corner_data(vid);
let (center, sphere_r) = compute_sphere_center(&data).expect("should compute center");
let r = data.radius;
let expected = data.vertex_pos + Vec3::new(1.0, 1.0, 1.0) * r;
let err = (center - expected).length();
assert!(err < 1e-10, "center offset: {err}");
for (i, cp) in data.contact_points.iter().enumerate() {
let dist = (*cp - center).length();
let diff = (dist - sphere_r).abs();
assert!(diff < 1e-10, "contact point {i} distance error: {diff}");
}
}
#[test]
fn test_spherical_triangle_points_on_sphere() {
let (_topo, vid) = make_vertex_id();
let data = unit_cube_corner_data(vid);
let (center, r) = compute_sphere_center(&data).expect("should compute center");
let result = build_spherical_corner(&data).expect("should build corner");
let nurbs = match &result.surface {
FaceSurface::Nurbs(s) => s,
_ => panic!("expected Nurbs surface"),
};
let n_samples = 5;
for i in 0..=n_samples {
for j in 0..=n_samples {
let u = i as f64 / n_samples as f64;
let v = j as f64 / n_samples as f64;
let pt = nurbs.evaluate(u, v);
let dist = (pt - center).length();
let err = (dist - r).abs();
assert!(
err < r * 0.15,
"point at ({u},{v}) dist error {err} (dist={dist}, r={r})"
);
}
}
}
#[test]
fn test_boundary_arc_is_circular() {
let (_topo, vid) = make_vertex_id();
let data = unit_cube_corner_data(vid);
let (center, r) = compute_sphere_center(&data).expect("should compute center");
let result = build_spherical_corner(&data).expect("should build corner");
for (arc_idx, arc) in result.boundary_curves.iter().enumerate() {
let n_samples = 20;
for i in 0..=n_samples {
let t = i as f64 / n_samples as f64;
let pt = arc.evaluate(t);
let dist = (pt - center).length();
let err = (dist - r).abs();
assert!(
err < 1e-10,
"arc {arc_idx} at t={t}: dist error {err} (dist={dist}, r={r})"
);
}
}
}
}