use crate::topology::{BrepSolid, EdgeRecord, FaceRecord};
use crate::{AffineTransform, AnalyticSurface, MateAxis, MatePlane, NurbsCurve, Vec3};
use serde::Serialize;
const RESOLVE_TOLERANCE: f64 = 1e-9;
const VERTEX_SNAP_TOLERANCE: f64 = 1e-6;
const CIRCLE_VERIFY_SAMPLES: usize = 16;
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SelectionGeometry {
Plane { origin: Vec3, normal: Vec3 },
Axis {
origin: Vec3,
direction: Vec3,
radius: Option<f64>,
},
Sphere { center: Vec3, radius: f64 },
Circle { center: Vec3, axis: Vec3, radius: f64 },
Line { origin: Vec3, direction: Vec3 },
Point { position: Vec3 },
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum ResolveError {
NotFound { name: String },
Unsupported { name: String, detail: String },
Geometry { name: String, detail: String },
}
impl ResolveError {
pub fn status(&self) -> &'static str {
match self {
ResolveError::NotFound { .. } | ResolveError::Geometry { .. } => "invalid-selection",
ResolveError::Unsupported { .. } => "unsupported-selection",
}
}
}
impl std::fmt::Display for ResolveError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResolveError::NotFound { name } => write!(formatter, "selection '{name}' not found"),
ResolveError::Unsupported { name, detail } => {
write!(formatter, "selection '{name}' has no analytic frame: {detail}")
}
ResolveError::Geometry { name, detail } => {
write!(formatter, "selection '{name}' failed to resolve: {detail}")
}
}
}
}
impl SelectionGeometry {
pub fn mate_plane(&self) -> Option<MatePlane> {
match *self {
SelectionGeometry::Plane { origin, normal } => Some(MatePlane {
origin: triple(origin),
normal: triple(normal),
}),
_ => None,
}
}
pub fn mate_axis(&self) -> Option<MateAxis> {
let (origin, direction) = match *self {
SelectionGeometry::Axis {
origin, direction, ..
}
| SelectionGeometry::Line { origin, direction } => (origin, direction),
SelectionGeometry::Circle { center, axis, .. } => (center, axis),
_ => return None,
};
Some(MateAxis {
origin: triple(origin),
direction: triple(direction),
})
}
pub fn representative_point(&self) -> Vec3 {
match *self {
SelectionGeometry::Plane { origin, .. }
| SelectionGeometry::Axis { origin, .. }
| SelectionGeometry::Line { origin, .. } => origin,
SelectionGeometry::Sphere { center, .. }
| SelectionGeometry::Circle { center, .. } => center,
SelectionGeometry::Point { position } => position,
}
}
pub fn transformed(&self, transform: &AffineTransform) -> Result<Self, ResolveError> {
let scale = uniform_scale(transform).ok_or_else(|| ResolveError::Unsupported {
name: "transform".into(),
detail: "selection frames transform only by rigid/uniform-scale matrices".into(),
})?;
let direction = |vector: Vec3| {
linear(transform, vector)
.normalized()
.map_err(|error| ResolveError::Geometry {
name: "transform".into(),
detail: error,
})
};
Ok(match *self {
SelectionGeometry::Plane { origin, normal } => SelectionGeometry::Plane {
origin: transform.point(origin),
normal: direction(normal)?,
},
SelectionGeometry::Axis {
origin,
direction: axis,
radius,
} => SelectionGeometry::Axis {
origin: transform.point(origin),
direction: direction(axis)?,
radius: radius.map(|radius| radius * scale),
},
SelectionGeometry::Sphere { center, radius } => SelectionGeometry::Sphere {
center: transform.point(center),
radius: radius * scale,
},
SelectionGeometry::Circle {
center,
axis,
radius,
} => SelectionGeometry::Circle {
center: transform.point(center),
axis: direction(axis)?,
radius: radius * scale,
},
SelectionGeometry::Line {
origin,
direction: axis,
} => SelectionGeometry::Line {
origin: transform.point(origin),
direction: direction(axis)?,
},
SelectionGeometry::Point { position } => SelectionGeometry::Point {
position: transform.point(position),
},
})
}
}
fn triple(vector: Vec3) -> [f64; 3] {
[vector.x, vector.y, vector.z]
}
fn linear(transform: &AffineTransform, vector: Vec3) -> Vec3 {
let m = transform.elements;
Vec3::new(
m[0] * vector.x + m[1] * vector.y + m[2] * vector.z,
m[4] * vector.x + m[5] * vector.y + m[6] * vector.z,
m[8] * vector.x + m[9] * vector.y + m[10] * vector.z,
)
}
fn uniform_scale(transform: &AffineTransform) -> Option<f64> {
let m = transform.elements;
let columns = [
Vec3::new(m[0], m[4], m[8]),
Vec3::new(m[1], m[5], m[9]),
Vec3::new(m[2], m[6], m[10]),
];
let lengths = [
columns[0].length(),
columns[1].length(),
columns[2].length(),
];
let scale = (lengths[0] + lengths[1] + lengths[2]) / 3.0;
if !(scale > 0.0) {
return None;
}
if lengths
.iter()
.any(|length| (length - scale).abs() > 1e-9 * scale)
{
return None;
}
let orthogonality = 1e-9 * scale * scale;
if columns[0].dot(columns[1]).abs() > orthogonality
|| columns[1].dot(columns[2]).abs() > orthogonality
|| columns[0].dot(columns[2]).abs() > orthogonality
{
return None;
}
Some(scale)
}
pub fn split_component_namespace(name: &str) -> (Vec<&str>, &str) {
let mut chain = Vec::new();
let mut local = name;
while let Some((head, tail)) = local.split_once(':') {
if !is_component_reference(head) {
break;
}
chain.push(head);
local = tail;
}
(chain, local)
}
pub fn is_component_reference(name: &str) -> bool {
name.strip_prefix("ACOMP")
.map(|digits| !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()))
.unwrap_or(false)
}
pub fn resolve_named_selection(
solid: &BrepSolid,
name: &str,
) -> Result<SelectionGeometry, ResolveError> {
if let Some(record) = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.name.as_deref() == Some(name))
{
return resolve_face_record(solid, record);
}
if let Some(record) = solid
.edges
.iter()
.find(|edge| edge.name.as_deref() == Some(name))
{
return resolve_edge_record(record);
}
Err(ResolveError::NotFound { name: name.into() })
}
pub fn resolve_face_selection(
solid: &BrepSolid,
face_id: u64,
) -> Result<SelectionGeometry, ResolveError> {
let record = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == face_id)
.ok_or_else(|| ResolveError::NotFound {
name: format!("face {face_id}"),
})?;
resolve_face_record(solid, record)
}
pub fn resolve_edge_selection(
solid: &BrepSolid,
edge_id: u64,
) -> Result<SelectionGeometry, ResolveError> {
let record = solid
.edges
.iter()
.find(|edge| edge.id == edge_id)
.ok_or_else(|| ResolveError::NotFound {
name: format!("edge {edge_id}"),
})?;
resolve_edge_record(record)
}
pub fn resolve_vertex_selection(
solid: &BrepSolid,
position: Vec3,
) -> Result<SelectionGeometry, ResolveError> {
let mut best: Option<(f64, Vec3)> = None;
for vertex in &solid.vertices {
let distance = vertex.point.sub(position).length();
if best
.map(|(best_distance, _)| distance < best_distance)
.unwrap_or(true)
{
best = Some((distance, vertex.point));
}
}
match best {
Some((distance, point))
if distance <= VERTEX_SNAP_TOLERANCE * crate::solid_scale(solid) =>
{
Ok(SelectionGeometry::Point { position: point })
}
_ => Err(ResolveError::NotFound {
name: format!(
"vertex near ({}, {}, {})",
position.x, position.y, position.z
),
}),
}
}
pub fn resolve_component_point(solids: &[&BrepSolid]) -> Result<SelectionGeometry, ResolveError> {
let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
let mut any = false;
let mut include = |point: Vec3| {
low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
any = true;
};
for solid in solids {
for vertex in &solid.vertices {
include(vertex.point);
}
for shell in &solid.shells {
for face in &shell.faces {
for row in &face.surface.control_points {
for control in row {
let point = control.point().map_err(|error| ResolveError::Geometry {
name: "component".into(),
detail: error,
})?;
include(point);
}
}
}
}
}
if !any {
return Err(ResolveError::Geometry {
name: "component".into(),
detail: "component has no geometry to anchor".into(),
});
}
Ok(SelectionGeometry::Point {
position: low.add(high).scale(0.5),
})
}
fn face_label(record: &FaceRecord) -> String {
record
.name
.clone()
.unwrap_or_else(|| format!("face {}", record.id))
}
fn resolve_face_record(
solid: &BrepSolid,
record: &FaceRecord,
) -> Result<SelectionGeometry, ResolveError> {
match record.surface.analytic() {
Some(AnalyticSurface::RuledRevolution {
frame, rho0, rho1, ..
}) => {
let span = rho0.abs().max(rho1.abs());
let radius = ((rho0 - rho1).abs() <= RESOLVE_TOLERANCE * (1.0 + span))
.then_some(0.5 * (rho0 + rho1));
Ok(SelectionGeometry::Axis {
origin: frame.origin,
direction: frame.axis,
radius,
})
}
Some(AnalyticSurface::Sphere { frame, radius }) => Ok(SelectionGeometry::Sphere {
center: frame.origin,
radius: *radius,
}),
Some(AnalyticSurface::Torus { frame, .. }) => Ok(SelectionGeometry::Axis {
origin: frame.origin,
direction: frame.axis,
radius: None,
}),
Some(AnalyticSurface::Revolution {
frame, generatrix, ..
}) => Ok(SelectionGeometry::Axis {
origin: frame.origin,
direction: frame.axis,
radius: revolution_cylinder_radius(frame, generatrix),
}),
Some(AnalyticSurface::Plane { .. }) | None => planar_face(solid, record),
}
}
fn revolution_cylinder_radius(
frame: &crate::RevolutionFrame,
generatrix: &NurbsCurve,
) -> Option<f64> {
if !control_net_is_colinear(generatrix) {
return None;
}
let [t0, t1] = generatrix.domain().ok()?;
let radial = |t: f64| -> Option<f64> {
let point = generatrix.evaluate(t).ok()?;
let delta = point.sub(frame.origin);
Some(delta.sub(frame.axis.scale(delta.dot(frame.axis))).length())
};
let rho0 = radial(t0)?;
let rho_mid = radial(0.5 * (t0 + t1))?;
let rho1 = radial(t1)?;
let span = rho0.abs().max(rho1.abs());
let tolerance = RESOLVE_TOLERANCE * (1.0 + span);
((rho0 - rho1).abs() <= tolerance && (rho_mid - rho0).abs() <= tolerance)
.then_some((rho0 + rho_mid + rho1) / 3.0)
}
fn planar_face(solid: &BrepSolid, record: &FaceRecord) -> Result<SelectionGeometry, ResolveError> {
let geometry = (|| -> Result<Option<SelectionGeometry>, String> {
let [u0, u1] = record.surface.domain_u()?;
let [v0, v1] = record.surface.domain_v()?;
let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
let center_normal = record.surface.normal(um, vm)?;
for u in [u0, um, u1] {
for v in [v0, vm, v1] {
if record.surface.normal(u, v)?.dot(center_normal) < 1.0 - 1e-6 {
return Ok(None);
}
}
}
let normal = if record.same_sense {
center_normal
} else {
center_normal.scale(-1.0)
}
.normalized()?;
let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
let mut any = false;
for loop_record in &record.loops {
for coedge in &loop_record.coedges {
let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
continue;
};
if edge.degenerate {
continue;
}
for step in 0..=4 {
let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
let point = edge.curve.evaluate(t)?;
low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
any = true;
}
}
}
let plane_point = record.surface.evaluate(um, vm)?;
let center = if any {
low.add(high).scale(0.5)
} else {
plane_point
};
let signed = center.sub(plane_point).dot(normal);
let origin = center.sub(normal.scale(signed));
Ok(Some(SelectionGeometry::Plane { origin, normal }))
})();
match geometry {
Ok(Some(frame)) => Ok(frame),
Ok(None) => Err(ResolveError::Unsupported {
name: face_label(record),
detail: "face carries no analytic frame (freeform surface)".into(),
}),
Err(error) => Err(ResolveError::Geometry {
name: face_label(record),
detail: error,
}),
}
}
fn edge_label(record: &EdgeRecord) -> String {
record
.name
.clone()
.unwrap_or_else(|| format!("edge {}", record.id))
}
fn resolve_edge_record(record: &EdgeRecord) -> Result<SelectionGeometry, ResolveError> {
if record.degenerate {
return Err(ResolveError::Unsupported {
name: edge_label(record),
detail: "degenerate edge".into(),
});
}
match edge_geometry(record) {
Ok(Some(frame)) => Ok(frame),
Ok(None) => Err(ResolveError::Unsupported {
name: edge_label(record),
detail: "edge is neither straight nor circular".into(),
}),
Err(error) => Err(ResolveError::Geometry {
name: edge_label(record),
detail: error,
}),
}
}
fn edge_geometry(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
if let Some(line) = straight_edge_line(record)? {
return Ok(Some(line));
}
circular_edge_circle(record)
}
fn control_net_extent(curve: &NurbsCurve) -> f64 {
let mut extent = 0.0_f64;
for control in &curve.control_points {
if let Ok(point) = control.point() {
extent = extent
.max(point.x.abs())
.max(point.y.abs())
.max(point.z.abs());
}
}
extent
}
fn control_net_is_colinear(curve: &NurbsCurve) -> bool {
let points: Vec<Vec3> = curve
.control_points
.iter()
.filter_map(|control| control.point().ok())
.collect();
let Some((&first, rest)) = points.split_first() else {
return false;
};
let tolerance = RESOLVE_TOLERANCE * (1.0 + control_net_extent(curve));
let Some(direction) = rest
.iter()
.map(|point| point.sub(first))
.max_by(|a, b| a.length().total_cmp(&b.length()))
.and_then(|chord| chord.normalized().ok())
else {
return false;
};
points.iter().all(|point| {
let offset = point.sub(first);
offset.sub(direction.scale(offset.dot(direction))).length() <= tolerance
})
}
fn straight_edge_line(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
if !control_net_is_colinear(&record.curve) {
return Ok(None);
}
let start = record.curve.evaluate(record.t0)?;
let end = record.curve.evaluate(record.t1)?;
let chord = end.sub(start);
if chord.length() <= RESOLVE_TOLERANCE * (1.0 + control_net_extent(&record.curve)) {
return Ok(None);
}
Ok(Some(SelectionGeometry::Line {
origin: start.add(end).scale(0.5),
direction: chord.normalized()?,
}))
}
fn circular_edge_circle(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
let span = record.t1 - record.t0;
if !(span > 0.0) {
return Ok(None);
}
let at = |fraction: f64| record.curve.evaluate(record.t0 + span * fraction);
let (a, b, c) = (at(1.0 / 6.0)?, at(0.5)?, at(5.0 / 6.0)?);
let Some(center) = crate::analytic_surface::circumcenter(a, b, c) else {
return Ok(None);
};
let Ok(axis) = b.sub(a).cross(c.sub(b)).normalized() else {
return Ok(None);
};
let radius = a.sub(center).length();
let tolerance = RESOLVE_TOLERANCE * (1.0 + control_net_extent(&record.curve));
for step in 0..=CIRCLE_VERIFY_SAMPLES {
let point = at(step as f64 / CIRCLE_VERIFY_SAMPLES as f64)?;
let delta = point.sub(center);
if (delta.length() - radius).abs() > tolerance || delta.dot(axis).abs() > tolerance {
return Ok(None);
}
}
Ok(Some(SelectionGeometry::Circle {
center,
axis,
radius,
}))
}