use crate::numerical;
use crate::{AnalyticSurface, Mesh, RecognitionError, SourceMetadata, SurfaceHint, Vec3};
#[derive(Clone, Debug, PartialEq)]
pub struct KernelFaceTruth {
pub mesh_face_id: u32,
pub source_face_id: u64,
pub source_face_name: Option<String>,
pub orientation: i8,
pub surface: AnalyticSurface,
}
fn kernel_carrier_orientation_gauge(source: &brep_kernel::AnalyticSurface) -> i8 {
match source {
brep_kernel::AnalyticSurface::RuledRevolution { height, .. } if *height < 0.0 => -1,
_ => 1,
}
}
fn kernel_face_orientation(source: &brep_kernel::AnalyticSurface, same_sense: bool) -> i8 {
(if same_sense { 1 } else { -1 }) * kernel_carrier_orientation_gauge(source)
}
fn invalid_kernel_analytic(reason: impl Into<String>) -> RecognitionError {
RecognitionError::FitFailed {
surface: None,
reason: reason.into(),
}
}
pub fn surface_from_kernel_analytic(
source: &brep_kernel::AnalyticSurface,
) -> Result<Option<AnalyticSurface>, RecognitionError> {
use brep_kernel::AnalyticSurface as KernelSurface;
let converted = match source {
KernelSurface::Plane {
origin,
u_dir,
v_dir,
..
} => {
let normal = vec3_from_kernel(*u_dir)
.cross(vec3_from_kernel(*v_dir))
.normalized()
.ok_or_else(|| invalid_kernel_analytic("kernel plane has degenerate directions"))?;
AnalyticSurface::Plane(crate::PlaneSurface {
origin: vec3_from_kernel(*origin),
normal,
})
}
KernelSurface::RuledRevolution {
frame,
rho0,
rho1,
height,
} => {
if !rho0.is_finite()
|| !rho1.is_finite()
|| !height.is_finite()
|| *rho0 < 0.0
|| *rho1 < 0.0
{
return Err(invalid_kernel_analytic(
"kernel ruled revolution has invalid radius or height",
));
}
let axis = vec3_from_kernel(frame.axis)
.normalized()
.ok_or_else(|| invalid_kernel_analytic("kernel revolution axis is degenerate"))?;
let origin = vec3_from_kernel(frame.origin);
let radius_scale = rho0.abs().max(rho1.abs()).max(1.0);
if (*rho1 - *rho0).abs()
<= numerical::brep::REVOLUTION_EQUAL_RADIUS_RELATIVE * radius_scale
{
if *rho0 <= 0.0 || height.abs() <= numerical::brep::REVOLUTION_MIN_ABSOLUTE_HEIGHT {
return Err(invalid_kernel_analytic(
"kernel cylinder has non-positive radius or zero height",
));
}
AnalyticSurface::Cylinder(crate::CylinderSurface {
axis_origin: origin,
axis,
radius: *rho0,
})
} else {
if height.abs() <= numerical::brep::REVOLUTION_MIN_ABSOLUTE_HEIGHT {
return Err(invalid_kernel_analytic("kernel cone has zero height"));
}
let slope = (*rho1 - *rho0) / *height;
if !slope.is_finite() || slope == 0.0 {
return Err(invalid_kernel_analytic("kernel cone has invalid slope"));
}
let apex_z = -*rho0 / slope;
AnalyticSurface::Cone(crate::ConeSurface {
apex: origin + axis * apex_z,
axis: axis * slope.signum(),
half_angle: slope.abs().atan(),
})
}
}
KernelSurface::Sphere { frame, radius } => {
let _axis = vec3_from_kernel(frame.axis)
.normalized()
.ok_or_else(|| invalid_kernel_analytic("kernel sphere frame is degenerate"))?;
AnalyticSurface::Sphere(crate::SphereSurface {
center: vec3_from_kernel(frame.origin),
radius: *radius,
})
}
KernelSurface::Torus {
frame,
major_radius,
minor_radius,
} => AnalyticSurface::Torus(crate::TorusSurface {
center: vec3_from_kernel(frame.origin),
axis: vec3_from_kernel(frame.axis)
.normalized()
.ok_or_else(|| invalid_kernel_analytic("kernel torus axis is degenerate"))?,
major_radius: *major_radius,
minor_radius: *minor_radius,
}),
KernelSurface::Revolution { .. } => return Ok(None),
};
if !converted.is_valid() {
return Err(invalid_kernel_analytic(
"kernel analytic carrier has invalid primitive parameters",
));
}
Ok(Some(converted))
}
pub fn analytic_truth_from_face(
face: &brep_kernel::FaceRecord,
mesh_face_id: u32,
) -> Result<Option<KernelFaceTruth>, RecognitionError> {
let Some(source) = face.surface.analytic() else {
return Ok(None);
};
let Some(surface) = surface_from_kernel_analytic(source)? else {
return Ok(None);
};
Ok(Some(KernelFaceTruth {
mesh_face_id,
source_face_id: face.id,
source_face_name: face.name.clone(),
orientation: kernel_face_orientation(source, face.same_sense),
surface,
}))
}
pub fn analytic_truths_from_solid(
solid: &brep_kernel::BrepSolid,
) -> Result<Vec<KernelFaceTruth>, RecognitionError> {
let mut truths = Vec::new();
let mut mesh_face_id = 0u32;
for shell in &solid.shells {
for face in &shell.faces {
if let Some(truth) = analytic_truth_from_face(face, mesh_face_id)? {
truths.push(truth);
}
mesh_face_id = mesh_face_id.checked_add(1).ok_or_else(|| {
RecognitionError::InvalidMesh("kernel solid has more than u32::MAX faces".into())
})?;
}
}
Ok(truths)
}
pub fn attach_solid_analytic_metadata(
converted: &mut KernelMeshConversion,
solid: &brep_kernel::BrepSolid,
source_tolerance: Option<f64>,
) -> Result<usize, RecognitionError> {
if source_tolerance.is_some_and(|value| !value.is_finite() || value <= 0.0) {
return Err(RecognitionError::InvalidSelection(
"source tolerance must be finite and positive".into(),
));
}
let start = converted.mesh.source_metadata.len();
for truth in analytic_truths_from_solid(solid)? {
let triangle_indices: Vec<usize> = converted
.triangle_face_ids
.iter()
.enumerate()
.filter_map(|(triangle, &face)| (face == Some(truth.mesh_face_id)).then_some(triangle))
.collect();
if triangle_indices.is_empty() {
continue;
}
converted.mesh.source_metadata.push(SourceMetadata {
version: 1,
triangle_indices,
hint: SurfaceHint::ExactCandidate {
surface: truth.surface,
},
source_face_id: Some(truth.source_face_id),
source_face_name: truth.source_face_name,
source_surface_id: None,
orientation: Some(truth.orientation),
source_tolerance,
});
}
Ok(converted.mesh.source_metadata.len() - start)
}
#[derive(Clone, Debug, PartialEq)]
pub struct KernelMeshConversion {
pub mesh: Mesh,
pub triangle_face_ids: Vec<Option<u32>>,
}
pub fn vec3_from_kernel(value: brep_kernel::Vec3) -> Vec3 {
Vec3::new(value.x, value.y, value.z)
}
pub fn vec3_to_kernel(value: Vec3) -> brep_kernel::Vec3 {
brep_kernel::Vec3::new(value.x, value.y, value.z)
}
pub fn convert_kernel_mesh(
source: &brep_kernel::Mesh,
) -> Result<KernelMeshConversion, RecognitionError> {
if source.positions.is_empty() {
return Err(RecognitionError::InvalidMesh(
"kernel mesh has no positions".into(),
));
}
if !source.positions.len().is_multiple_of(3) {
return Err(RecognitionError::InvalidMesh(
"kernel position buffer must contain xyz triples".into(),
));
}
if source.indices.is_empty() || !source.indices.len().is_multiple_of(3) {
return Err(RecognitionError::InvalidMesh(
"kernel index buffer must contain triangles".into(),
));
}
if source
.positions
.iter()
.any(|coordinate| !coordinate.is_finite())
{
return Err(RecognitionError::InvalidMesh(
"kernel position buffer contains a non-finite coordinate".into(),
));
}
if !source.normals.is_empty() && source.normals.len() != source.positions.len() {
return Err(RecognitionError::InvalidMesh(format!(
"kernel normal buffer has {} coordinates for {} position coordinates",
source.normals.len(),
source.positions.len()
)));
}
if source
.normals
.iter()
.any(|coordinate| !coordinate.is_finite())
{
return Err(RecognitionError::InvalidMesh(
"kernel normal buffer contains a non-finite coordinate".into(),
));
}
let vertex_count = source.positions.len() / 3;
if source
.indices
.iter()
.any(|&index| index as usize >= vertex_count)
{
return Err(RecognitionError::InvalidMesh(
"kernel triangle index is outside the position buffer".into(),
));
}
let triangle_count = source.indices.len() / 3;
if !source.face_ids.is_empty() && source.face_ids.len() != triangle_count {
return Err(RecognitionError::InvalidMesh(format!(
"kernel face-id buffer has {} entries for {triangle_count} triangles",
source.face_ids.len()
)));
}
let vertices = source
.positions
.chunks_exact(3)
.map(|point| Vec3::new(point[0], point[1], point[2]))
.collect();
let vertex_normals = (!source.normals.is_empty()).then(|| {
source
.normals
.chunks_exact(3)
.map(|normal| Vec3::new(normal[0], normal[1], normal[2]))
.collect()
});
let triangles = source
.indices
.chunks_exact(3)
.map(|triangle| [triangle[0], triangle[1], triangle[2]])
.collect();
let triangle_face_ids = if source.face_ids.is_empty() {
vec![None; triangle_count]
} else {
source.face_ids.iter().copied().map(Some).collect()
};
Ok(KernelMeshConversion {
mesh: Mesh {
vertices,
triangles,
vertex_normals,
source_metadata: Vec::new(),
},
triangle_face_ids,
})
}
pub fn mesh_from_kernel(source: &brep_kernel::Mesh) -> Result<Mesh, RecognitionError> {
Ok(convert_kernel_mesh(source)?.mesh)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FiniteInterval {
start: f64,
end: f64,
}
impl FiniteInterval {
pub fn new(start: f64, end: f64) -> Result<Self, RecognitionError> {
let length = end - start;
if !start.is_finite() || !end.is_finite() || !length.is_finite() || length <= 0.0 {
return Err(RecognitionError::InvalidSelection(
"surface interval bounds must be finite and strictly increasing".into(),
));
}
Ok(Self { start, end })
}
pub fn start(self) -> f64 {
self.start
}
pub fn end(self) -> f64 {
self.end
}
pub fn length(self) -> f64 {
self.end - self.start
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum FinitePatchBounds {
Plane {
u: FiniteInterval,
v: FiniteInterval,
},
Axial(FiniteInterval),
}
impl FinitePatchBounds {
pub fn plane(
u_start: f64,
u_end: f64,
v_start: f64,
v_end: f64,
) -> Result<Self, RecognitionError> {
Ok(Self::Plane {
u: FiniteInterval::new(u_start, u_end)?,
v: FiniteInterval::new(v_start, v_end)?,
})
}
pub fn axial(start: f64, end: f64) -> Result<Self, RecognitionError> {
Ok(Self::Axial(FiniteInterval::new(start, end)?))
}
}
#[derive(Clone, Debug)]
pub struct OrientedNurbsSurface {
pub surface: brep_kernel::NurbsSurface,
pub orientation: i8,
}
fn native_surface_error(surface: AnalyticSurface, reason: impl Into<String>) -> RecognitionError {
RecognitionError::FitFailed {
surface: Some(surface.surface_type().name()),
reason: format!("native NURBS construction failed: {}", reason.into()),
}
}
pub fn nurbs_surface_from_analytic(
surface: AnalyticSurface,
orientation: i8,
bounds: Option<FinitePatchBounds>,
) -> Result<OrientedNurbsSurface, RecognitionError> {
if !matches!(orientation, -1 | 1) {
return Err(RecognitionError::InvalidSelection(
"surface orientation must be -1 or +1".into(),
));
}
if !surface.is_valid() {
return Err(native_surface_error(surface, "invalid analytic parameters"));
}
let native = match (surface, bounds) {
(AnalyticSurface::Plane(plane), Some(FinitePatchBounds::Plane { u, v })) => {
let (u_direction, v_direction) = plane
.normal
.orthonormal_basis()
.ok_or_else(|| native_surface_error(surface, "invalid plane basis"))?;
let origin = plane.origin + u_direction * u.start() + v_direction * v.start();
brep_kernel::make_plane(
vec3_to_kernel(origin),
vec3_to_kernel(u_direction),
vec3_to_kernel(v_direction),
u.length(),
v.length(),
)
}
(AnalyticSurface::Cylinder(cylinder), Some(FinitePatchBounds::Axial(axial))) => {
let base = cylinder.axis_origin + cylinder.axis * axial.start();
brep_kernel::make_cylinder_surface(
vec3_to_kernel(base),
vec3_to_kernel(cylinder.axis),
cylinder.radius,
axial.length(),
)
}
(AnalyticSurface::Cone(cone), Some(FinitePatchBounds::Axial(axial))) => {
if axial.start() <= 0.0 {
return Err(RecognitionError::InvalidSelection(
"cone axial bounds must lie strictly on the positive nappe".into(),
));
}
let tangent = cone.half_angle.tan();
let base = cone.apex + cone.axis * axial.start();
brep_kernel::make_cone_surface(
vec3_to_kernel(base),
vec3_to_kernel(cone.axis),
axial.start() * tangent,
axial.end() * tangent,
axial.length(),
)
}
(AnalyticSurface::Sphere(sphere), None) => brep_kernel::make_sphere_surface(
vec3_to_kernel(sphere.center),
sphere.radius,
brep_kernel::Vec3::new(0.0, 0.0, 1.0),
),
(AnalyticSurface::Torus(torus), None) => brep_kernel::make_torus_surface(
vec3_to_kernel(torus.center),
vec3_to_kernel(torus.axis),
torus.major_radius,
torus.minor_radius,
),
(AnalyticSurface::Plane(_), _) => {
return Err(RecognitionError::InvalidSelection(
"plane conversion requires plane u/v bounds".into(),
));
}
(AnalyticSurface::Cylinder(_), _) => {
return Err(RecognitionError::InvalidSelection(
"cylinder conversion requires axial bounds".into(),
));
}
(AnalyticSurface::Cone(_), _) => {
return Err(RecognitionError::InvalidSelection(
"cone conversion requires axial bounds".into(),
));
}
(AnalyticSurface::Sphere(_) | AnalyticSurface::Torus(_), Some(_)) => {
return Err(RecognitionError::InvalidSelection(
"sphere and torus conversion use their complete native domains and take no bounds"
.into(),
));
}
}
.map_err(|reason| native_surface_error(surface, reason))?;
Ok(OrientedNurbsSurface {
surface: native,
orientation,
})
}
pub fn tessellate_kernel_solid_with_metadata(
solid: &brep_kernel::BrepSolid,
chord_tolerance: f64,
source_tolerance: Option<f64>,
) -> Result<KernelMeshConversion, RecognitionError> {
if !chord_tolerance.is_finite() || chord_tolerance <= 0.0 {
return Err(RecognitionError::InvalidSelection(
"chord tolerance must be finite and positive".into(),
));
}
let source =
brep_kernel::tessellate_brep_watertight(solid, chord_tolerance).map_err(|error| {
RecognitionError::InvalidMesh(format!("kernel tessellation failed: {error}"))
})?;
let mut converted = convert_kernel_mesh(&source)?;
attach_solid_analytic_metadata(&mut converted, solid, source_tolerance)?;
Ok(converted)
}
#[allow(clippy::too_many_arguments)]
pub fn attach_face_metadata(
converted: &mut KernelMeshConversion,
mesh_face_id: u32,
hint: SurfaceHint,
source_face_id: Option<u64>,
source_face_name: Option<String>,
source_surface_id: Option<String>,
orientation: Option<i8>,
source_tolerance: Option<f64>,
) -> Result<(), RecognitionError> {
if orientation.is_some_and(|sense| !matches!(sense, -1 | 1)) {
return Err(RecognitionError::InvalidMesh(
"source orientation must be -1 or +1".into(),
));
}
let triangle_indices: Vec<usize> = converted
.triangle_face_ids
.iter()
.enumerate()
.filter_map(|(triangle, &face)| (face == Some(mesh_face_id)).then_some(triangle))
.collect();
if triangle_indices.is_empty() {
return Err(RecognitionError::InvalidSelection(format!(
"kernel mesh contains no triangles for sequential face {mesh_face_id}"
)));
}
converted.mesh.source_metadata.push(SourceMetadata {
version: 1,
triangle_indices,
hint,
source_face_id,
source_face_name,
source_surface_id,
orientation,
source_tolerance,
});
Ok(())
}
pub fn region_carrier_from_surface(
surface: AnalyticSurface,
orientation: i8,
) -> Result<brep_kernel::RegionCarrier, RecognitionError> {
if !matches!(orientation, -1 | 1) {
return Err(RecognitionError::InvalidSelection(
"surface orientation must be -1 or +1".into(),
));
}
if !surface.is_valid() {
return Err(RecognitionError::FitFailed {
surface: Some(surface.surface_type().name()),
reason: "cannot convert invalid analytic parameters".into(),
});
}
let sign = orientation as f64;
Ok(match surface {
AnalyticSurface::Plane(plane) => brep_kernel::RegionCarrier::Plane {
origin: vec3_to_kernel(plane.origin),
normal: vec3_to_kernel(plane.normal * sign),
},
AnalyticSurface::Cylinder(cylinder) => brep_kernel::RegionCarrier::Cylinder {
axis_point: vec3_to_kernel(cylinder.axis_origin),
axis_dir: vec3_to_kernel(cylinder.axis),
radius: cylinder.radius,
sense: orientation,
},
AnalyticSurface::Cone(cone) => brep_kernel::RegionCarrier::Cone {
apex: vec3_to_kernel(cone.apex),
axis_dir: vec3_to_kernel(cone.axis),
half_angle_rad: cone.half_angle,
sense: orientation,
},
AnalyticSurface::Sphere(sphere) => brep_kernel::RegionCarrier::Sphere {
center: vec3_to_kernel(sphere.center),
radius: sphere.radius,
sense: orientation,
},
AnalyticSurface::Torus(torus) => brep_kernel::RegionCarrier::Torus {
center: vec3_to_kernel(torus.center),
axis_dir: vec3_to_kernel(torus.axis),
major_radius: torus.major_radius,
minor_radius: torus.minor_radius,
sense: orientation,
},
})
}