use std::collections::HashSet;
use brepkit_math::mat::Mat4;
use brepkit_math::nurbs::curve::NurbsCurve;
use brepkit_math::nurbs::surface::NurbsSurface;
use brepkit_math::tolerance::Tolerance;
use brepkit_math::vec::Vec3;
use brepkit_topology::Topology;
use brepkit_topology::edge::{EdgeCurve, EdgeId};
use brepkit_topology::face::{FaceId, FaceSurface};
use brepkit_topology::solid::SolidId;
use brepkit_topology::vertex::VertexId;
use brepkit_topology::wire::WireId;
#[allow(clippy::too_many_lines)]
pub fn transform_solid(
topo: &mut Topology,
solid: SolidId,
matrix: &Mat4,
) -> Result<(), crate::OperationsError> {
let tol = Tolerance::new();
if tol.approx_eq(matrix.determinant(), 0.0) {
return Err(crate::OperationsError::InvalidInput {
reason: "transform matrix is degenerate (zero determinant)".into(),
});
}
let (vertex_ids, edge_ids, face_ids) = collect_solid_entities(topo, solid)?;
for vid in vertex_ids {
let vertex = topo.vertex_mut(vid)?;
let new_point = matrix.mul_point(vertex.point());
vertex.set_point(new_point);
}
transform_edges(topo, &edge_ids, matrix)?;
let normal_matrix = matrix.inverse()?.transpose();
for fid in face_ids {
let face = topo.face(fid)?;
match face.surface() {
FaceSurface::Plane { normal, .. } => {
let n = *normal;
let transformed =
normal_matrix.mul_point(brepkit_math::vec::Point3::new(n.x(), n.y(), n.z()));
let origin = normal_matrix.mul_point(brepkit_math::vec::Point3::new(0.0, 0.0, 0.0));
let raw = Vec3::new(
transformed.x() - origin.x(),
transformed.y() - origin.y(),
transformed.z() - origin.z(),
);
let new_normal = raw.normalize()?;
let wire = topo.wire(face.outer_wire())?;
let first_oe = &wire.edges()[0];
let edge = topo.edge(first_oe.edge())?;
let ref_vid = if first_oe.is_forward() {
edge.start()
} else {
edge.end()
};
let ref_point = topo.vertex(ref_vid)?.point();
let new_d = new_normal.dot(Vec3::new(ref_point.x(), ref_point.y(), ref_point.z()));
let face_mut = topo.face_mut(fid)?;
face_mut.set_surface(FaceSurface::Plane {
normal: new_normal,
d: new_d,
});
}
FaceSurface::Nurbs(s) => {
let new_control_points: Vec<Vec<_>> = s
.control_points()
.iter()
.map(|row| row.iter().map(|pt| matrix.mul_point(*pt)).collect())
.collect();
let new_surface = NurbsSurface::new(
s.degree_u(),
s.degree_v(),
s.knots_u().to_vec(),
s.knots_v().to_vec(),
new_control_points,
s.weights().to_vec(),
);
topo.face_mut(fid)?
.set_surface(FaceSurface::Nurbs(new_surface?));
}
FaceSurface::Cylinder(cyl) => {
let new_origin = matrix.mul_point(cyl.origin());
let new_axis = transform_direction(matrix, cyl.axis())?;
let new_radius = scaled_radius(matrix, cyl.axis(), cyl.radius());
let new_cyl = brepkit_math::surfaces::CylindricalSurface::new(
new_origin, new_axis, new_radius,
)?;
topo.face_mut(fid)?
.set_surface(FaceSurface::Cylinder(new_cyl));
}
FaceSurface::Cone(cone) => {
if is_uniform_scale(matrix) {
let new_apex = matrix.mul_point(cone.apex());
let new_axis = transform_direction(matrix, cone.axis())?;
let new_cone = brepkit_math::surfaces::ConicalSurface::new(
new_apex,
new_axis,
cone.half_angle(),
)?;
topo.face_mut(fid)?.set_surface(FaceSurface::Cone(new_cone));
} else {
let v_range = analytic_face_v_range(topo, fid, |pt| cone.project_point(pt).1)?;
let cone_clone = cone.clone();
let nurbs = brepkit_heal::construct::convert_surface::cone_to_nurbs(
&cone_clone,
v_range,
)
.map_err(|e| crate::OperationsError::InvalidInput {
reason: format!("cone_to_nurbs failed: {e}"),
})?;
let transformed = transform_nurbs_surface(&nurbs, matrix)?;
topo.face_mut(fid)?
.set_surface(FaceSurface::Nurbs(transformed));
}
}
FaceSurface::Sphere(sph) => {
if is_uniform_scale(matrix) {
let new_center = matrix.mul_point(sph.center());
let m = &matrix.0;
let sx = (m[0][0] * m[0][0] + m[1][0] * m[1][0] + m[2][0] * m[2][0]).sqrt();
let new_sph = brepkit_math::surfaces::SphericalSurface::new(
new_center,
sph.radius() * sx,
)?;
topo.face_mut(fid)?
.set_surface(FaceSurface::Sphere(new_sph));
} else {
let (v_min, v_max) = sphere_face_v_range(topo, fid, sph)?;
let sph_clone = sph.clone();
let nurbs = sphere_to_transformed_nurbs(&sph_clone, matrix, v_min, v_max)?;
topo.face_mut(fid)?.set_surface(FaceSurface::Nurbs(nurbs));
}
}
FaceSurface::Torus(tor) => {
if is_uniform_scale(matrix) {
let new_center = matrix.mul_point(tor.center());
let m = &matrix.0;
let sx = (m[0][0] * m[0][0] + m[1][0] * m[1][0] + m[2][0] * m[2][0]).sqrt();
let new_tor = brepkit_math::surfaces::ToroidalSurface::new(
new_center,
tor.major_radius() * sx,
tor.minor_radius() * sx,
)?;
topo.face_mut(fid)?.set_surface(FaceSurface::Torus(new_tor));
} else {
let tor_clone = tor.clone();
let nurbs =
brepkit_heal::construct::convert_surface::torus_to_nurbs(&tor_clone)
.map_err(|e| crate::OperationsError::InvalidInput {
reason: format!("torus_to_nurbs failed: {e}"),
})?;
let transformed = transform_nurbs_surface(&nurbs, matrix)?;
topo.face_mut(fid)?
.set_surface(FaceSurface::Nurbs(transformed));
}
}
}
}
Ok(())
}
fn sphere_face_v_range(
topo: &Topology,
face_id: FaceId,
sph: &brepkit_math::surfaces::SphericalSurface,
) -> Result<(f64, f64), crate::OperationsError> {
use std::f64::consts::FRAC_PI_2;
let face = topo.face(face_id)?;
let wire = topo.wire(face.outer_wire())?;
let mut v_vals = Vec::new();
for oe in wire.edges() {
let edge = topo.edge(oe.edge())?;
let pt = topo.vertex(edge.start())?.point();
let (_u, v) = sph.project_point(pt);
v_vals.push(v);
}
if v_vals.is_empty() {
return Ok((-FRAC_PI_2, FRAC_PI_2));
}
let boundary_v = v_vals.iter().copied().sum::<f64>() / v_vals.len() as f64;
let center = sph.center();
let avg_boundary_z: f64 = {
let mut sum = 0.0;
for oe in wire.edges() {
let edge = topo.edge(oe.edge())?;
let pt = topo.vertex(edge.start())?.point();
sum += pt.z() - center.z();
}
sum / wire.edges().len() as f64
};
if boundary_v.abs() < 0.1 {
let mut has_pole_north = false;
let mut has_pole_south = false;
for oe in wire.edges() {
let edge = topo.edge(oe.edge())?;
if edge.start() == edge.end() {
let pt = topo.vertex(edge.start())?.point();
let dz = pt.z() - center.z();
if dz > 0.0 {
has_pole_north = true;
} else {
has_pole_south = true;
}
}
}
if has_pole_north {
return Ok((boundary_v, FRAC_PI_2));
}
if has_pole_south {
return Ok((-FRAC_PI_2, boundary_v));
}
if avg_boundary_z >= 0.0 {
return Ok((boundary_v, FRAC_PI_2));
}
return Ok((-FRAC_PI_2, boundary_v));
}
if boundary_v > 0.0 {
Ok((boundary_v, FRAC_PI_2))
} else {
Ok((-FRAC_PI_2, boundary_v))
}
}
fn scaled_radius(matrix: &Mat4, axis: Vec3, radius: f64) -> f64 {
let perp = if axis.x().abs() < 0.9 {
Vec3::new(1.0, 0.0, 0.0)
.cross(axis)
.normalize()
.unwrap_or(Vec3::new(1.0, 0.0, 0.0))
} else {
Vec3::new(0.0, 1.0, 0.0)
.cross(axis)
.normalize()
.unwrap_or(Vec3::new(0.0, 1.0, 0.0))
};
let origin = brepkit_math::vec::Point3::new(0.0, 0.0, 0.0);
let end =
brepkit_math::vec::Point3::new(perp.x() * radius, perp.y() * radius, perp.z() * radius);
let t_origin = matrix.mul_point(origin);
let t_end = matrix.mul_point(end);
let diff = t_end - t_origin;
diff.length()
}
#[allow(clippy::too_many_lines)]
fn transform_face_surface(
topo: &mut Topology,
fid: FaceId,
matrix: &Mat4,
normal_matrix: &Mat4,
) -> Result<(), crate::OperationsError> {
let face = topo.face(fid)?;
match face.surface() {
FaceSurface::Plane { normal, .. } => {
let n = *normal;
let transformed =
normal_matrix.mul_point(brepkit_math::vec::Point3::new(n.x(), n.y(), n.z()));
let origin = normal_matrix.mul_point(brepkit_math::vec::Point3::new(0.0, 0.0, 0.0));
let raw = Vec3::new(
transformed.x() - origin.x(),
transformed.y() - origin.y(),
transformed.z() - origin.z(),
);
let new_normal = raw.normalize()?;
let wire = topo.wire(face.outer_wire())?;
let first_oe =
wire.edges()
.first()
.ok_or_else(|| crate::OperationsError::InvalidInput {
reason: "face has empty outer wire".into(),
})?;
let edge = topo.edge(first_oe.edge())?;
let ref_vid = if first_oe.is_forward() {
edge.start()
} else {
edge.end()
};
let ref_point = topo.vertex(ref_vid)?.point();
let new_d = new_normal.dot(Vec3::new(ref_point.x(), ref_point.y(), ref_point.z()));
topo.face_mut(fid)?.set_surface(FaceSurface::Plane {
normal: new_normal,
d: new_d,
});
}
FaceSurface::Nurbs(s) => {
let new_control_points: Vec<Vec<_>> = s
.control_points()
.iter()
.map(|row| row.iter().map(|pt| matrix.mul_point(*pt)).collect())
.collect();
let new_surface = NurbsSurface::new(
s.degree_u(),
s.degree_v(),
s.knots_u().to_vec(),
s.knots_v().to_vec(),
new_control_points,
s.weights().to_vec(),
);
topo.face_mut(fid)?
.set_surface(FaceSurface::Nurbs(new_surface?));
}
FaceSurface::Cylinder(cyl) => {
let new_origin = matrix.mul_point(cyl.origin());
let new_axis = transform_direction(matrix, cyl.axis())?;
let new_radius = scaled_radius(matrix, cyl.axis(), cyl.radius());
let new_cyl =
brepkit_math::surfaces::CylindricalSurface::new(new_origin, new_axis, new_radius)?;
topo.face_mut(fid)?
.set_surface(FaceSurface::Cylinder(new_cyl));
}
FaceSurface::Cone(cone) => {
if is_uniform_scale(matrix) {
let new_apex = matrix.mul_point(cone.apex());
let new_axis = transform_direction(matrix, cone.axis())?;
let new_cone = brepkit_math::surfaces::ConicalSurface::new(
new_apex,
new_axis,
cone.half_angle(),
)?;
topo.face_mut(fid)?.set_surface(FaceSurface::Cone(new_cone));
} else {
let v_range = analytic_face_v_range(topo, fid, |pt| cone.project_point(pt).1)?;
let cone_clone = cone.clone();
let nurbs =
brepkit_heal::construct::convert_surface::cone_to_nurbs(&cone_clone, v_range)
.map_err(|e| crate::OperationsError::InvalidInput {
reason: format!("cone_to_nurbs failed: {e}"),
})?;
let transformed = transform_nurbs_surface(&nurbs, matrix)?;
topo.face_mut(fid)?
.set_surface(FaceSurface::Nurbs(transformed));
}
}
FaceSurface::Sphere(sph) => {
if is_uniform_scale(matrix) {
let new_center = matrix.mul_point(sph.center());
let m = &matrix.0;
let sx = (m[0][0] * m[0][0] + m[1][0] * m[1][0] + m[2][0] * m[2][0]).sqrt();
let new_sph =
brepkit_math::surfaces::SphericalSurface::new(new_center, sph.radius() * sx)?;
topo.face_mut(fid)?
.set_surface(FaceSurface::Sphere(new_sph));
} else {
let (v_min, v_max) = sphere_face_v_range(topo, fid, sph)?;
let sph_clone = sph.clone();
let nurbs = sphere_to_transformed_nurbs(&sph_clone, matrix, v_min, v_max)?;
topo.face_mut(fid)?.set_surface(FaceSurface::Nurbs(nurbs));
}
}
FaceSurface::Torus(tor) => {
if is_uniform_scale(matrix) {
let new_center = matrix.mul_point(tor.center());
let m = &matrix.0;
let sx = (m[0][0] * m[0][0] + m[1][0] * m[1][0] + m[2][0] * m[2][0]).sqrt();
let new_tor = brepkit_math::surfaces::ToroidalSurface::new(
new_center,
tor.major_radius() * sx,
tor.minor_radius() * sx,
)?;
topo.face_mut(fid)?.set_surface(FaceSurface::Torus(new_tor));
} else {
let tor_clone = tor.clone();
let nurbs = brepkit_heal::construct::convert_surface::torus_to_nurbs(&tor_clone)
.map_err(|e| crate::OperationsError::InvalidInput {
reason: format!("torus_to_nurbs failed: {e}"),
})?;
let transformed = transform_nurbs_surface(&nurbs, matrix)?;
topo.face_mut(fid)?
.set_surface(FaceSurface::Nurbs(transformed));
}
}
}
Ok(())
}
fn analytic_face_v_range(
topo: &Topology,
face_id: FaceId,
project_v: impl Fn(brepkit_math::vec::Point3) -> f64,
) -> Result<(f64, f64), crate::OperationsError> {
let face = topo.face(face_id)?;
let wire = topo.wire(face.outer_wire())?;
let mut v_min = f64::INFINITY;
let mut v_max = f64::NEG_INFINITY;
for oe in wire.edges() {
let edge = topo.edge(oe.edge())?;
let pt = topo.vertex(edge.start())?.point();
let v = project_v(pt);
v_min = v_min.min(v);
v_max = v_max.max(v);
}
if v_min >= v_max {
v_min = 0.0;
v_max = 1.0;
}
Ok((v_min, v_max))
}
fn transform_nurbs_surface(
surface: &NurbsSurface,
matrix: &Mat4,
) -> Result<NurbsSurface, crate::OperationsError> {
let new_cps: Vec<Vec<_>> = surface
.control_points()
.iter()
.map(|row| row.iter().map(|pt| matrix.mul_point(*pt)).collect())
.collect();
Ok(NurbsSurface::new(
surface.degree_u(),
surface.degree_v(),
surface.knots_u().to_vec(),
surface.knots_v().to_vec(),
new_cps,
surface.weights().to_vec(),
)?)
}
fn is_uniform_scale(matrix: &Mat4) -> bool {
let m = &matrix.0;
let sx = (m[0][0] * m[0][0] + m[1][0] * m[1][0] + m[2][0] * m[2][0]).sqrt();
let sy = (m[0][1] * m[0][1] + m[1][1] * m[1][1] + m[2][1] * m[2][1]).sqrt();
let sz = (m[0][2] * m[0][2] + m[1][2] * m[1][2] + m[2][2] * m[2][2]).sqrt();
let avg = (sx + sy + sz) / 3.0;
let rel = 0.01; (sx - avg).abs() < avg * rel && (sy - avg).abs() < avg * rel && (sz - avg).abs() < avg * rel
}
#[allow(clippy::cast_precision_loss)]
fn sphere_to_transformed_nurbs(
sph: &brepkit_math::surfaces::SphericalSurface,
matrix: &Mat4,
v_min: f64,
v_max: f64,
) -> Result<NurbsSurface, crate::OperationsError> {
use std::f64::consts::TAU;
let n_u = 33; let n_v = 17;
let mut rows: Vec<Vec<brepkit_math::vec::Point3>> = Vec::with_capacity(n_v);
for iv in 0..n_v {
let v = v_min + (v_max - v_min) * (iv as f64) / ((n_v - 1) as f64);
let mut row = Vec::with_capacity(n_u);
for iu in 0..n_u {
let u = TAU * (iu as f64) / ((n_u - 1) as f64);
let pt = sph.evaluate(u, v);
row.push(matrix.mul_point(pt));
}
rows.push(row);
}
let nurbs = brepkit_math::nurbs::surface_fitting::interpolate_surface(&rows, 3, 3)?;
Ok(nurbs)
}
fn transform_direction(matrix: &Mat4, dir: Vec3) -> Result<Vec3, crate::OperationsError> {
let origin = matrix.mul_point(brepkit_math::vec::Point3::new(0.0, 0.0, 0.0));
let tip = matrix.mul_point(brepkit_math::vec::Point3::new(dir.x(), dir.y(), dir.z()));
let raw = Vec3::new(
tip.x() - origin.x(),
tip.y() - origin.y(),
tip.z() - origin.z(),
);
Ok(raw.normalize()?)
}
#[allow(clippy::too_many_lines)]
fn transform_edges(
topo: &mut Topology,
edge_ids: &HashSet<EdgeId>,
matrix: &Mat4,
) -> Result<(), crate::OperationsError> {
let origin = matrix.mul_point(brepkit_math::vec::Point3::new(0.0, 0.0, 0.0));
let transform_dir = |d: Vec3| -> Vec3 {
matrix.mul_point(brepkit_math::vec::Point3::new(d.x(), d.y(), d.z())) - origin
};
for &eid in edge_ids {
let edge = topo.edge(eid)?;
let new_curve = match edge.curve() {
EdgeCurve::Line => None,
EdgeCurve::NurbsCurve(c) => {
let new_control_points: Vec<_> = c
.control_points()
.iter()
.map(|pt| matrix.mul_point(*pt))
.collect();
Some(EdgeCurve::NurbsCurve(NurbsCurve::new(
c.degree(),
c.knots().to_vec(),
new_control_points,
c.weights().to_vec(),
)?))
}
EdgeCurve::Circle(c) => {
let new_center = matrix.mul_point(c.center());
let new_u = transform_dir(c.u_axis());
let new_v = transform_dir(c.v_axis());
let su = new_u.length();
let sv = new_v.length();
let new_normal = new_u.cross(new_v).normalize()?;
if (su - sv).abs() < 1e-12 * su.max(sv).max(1.0) {
Some(EdgeCurve::Circle(
brepkit_math::curves::Circle3D::with_axes(
new_center,
new_normal,
c.radius() * su,
new_u.normalize()?,
new_v.normalize()?,
)?,
))
} else {
let (semi_major, semi_minor, u_dir, v_dir) = if su >= sv {
(
c.radius() * su,
c.radius() * sv,
new_u.normalize()?,
new_v.normalize()?,
)
} else {
(
c.radius() * sv,
c.radius() * su,
new_v.normalize()?,
new_u.normalize()?,
)
};
Some(EdgeCurve::Ellipse(
brepkit_math::curves::Ellipse3D::with_axes(
new_center, new_normal, semi_major, semi_minor, u_dir, v_dir,
)?,
))
}
}
EdgeCurve::Ellipse(e) => {
let new_center = matrix.mul_point(e.center());
let new_u = transform_dir(e.u_axis());
let new_v = transform_dir(e.v_axis());
let new_normal = new_u.cross(new_v).normalize()?;
Some(EdgeCurve::Ellipse(
brepkit_math::curves::Ellipse3D::with_axes(
new_center,
new_normal,
e.semi_major() * new_u.length(),
e.semi_minor() * new_v.length(),
new_u.normalize()?,
new_v.normalize()?,
)?,
))
}
};
if let Some(curve) = new_curve {
topo.edge_mut(eid)?.set_curve(curve);
}
}
Ok(())
}
pub fn transform_wire(
topo: &mut Topology,
wire_id: WireId,
matrix: &Mat4,
) -> Result<(), crate::OperationsError> {
let tol = Tolerance::new();
if tol.approx_eq(matrix.determinant(), 0.0) {
return Err(crate::OperationsError::InvalidInput {
reason: "transform matrix is degenerate (zero determinant)".into(),
});
}
let (vertex_ids, edge_ids) = collect_wire_entities(topo, wire_id)?;
for vid in vertex_ids {
let vertex = topo.vertex_mut(vid)?;
let new_point = matrix.mul_point(vertex.point());
vertex.set_point(new_point);
}
transform_edges(topo, &edge_ids, matrix)?;
Ok(())
}
#[allow(clippy::too_many_lines)]
pub fn transform_face(
topo: &mut Topology,
face_id: FaceId,
matrix: &Mat4,
) -> Result<(), crate::OperationsError> {
let tol = Tolerance::new();
if tol.approx_eq(matrix.determinant(), 0.0) {
return Err(crate::OperationsError::InvalidInput {
reason: "transform matrix is degenerate (zero determinant)".into(),
});
}
let (vertex_ids, edge_ids) = collect_face_entities(topo, face_id)?;
for vid in vertex_ids {
let vertex = topo.vertex_mut(vid)?;
let new_point = matrix.mul_point(vertex.point());
vertex.set_point(new_point);
}
transform_edges(topo, &edge_ids, matrix)?;
let normal_matrix = matrix.inverse()?.transpose();
transform_face_surface(topo, face_id, matrix, &normal_matrix)?;
Ok(())
}
fn collect_face_entities(
topo: &Topology,
face_id: FaceId,
) -> Result<(HashSet<VertexId>, HashSet<EdgeId>), crate::OperationsError> {
let mut vertex_ids = HashSet::new();
let mut edge_ids = HashSet::new();
let face = topo.face(face_id)?;
let wire_ids: Vec<_> = std::iter::once(face.outer_wire())
.chain(face.inner_wires().iter().copied())
.collect();
for wid in wire_ids {
let wire = topo.wire(wid)?;
for oe in wire.edges() {
let eid = oe.edge();
edge_ids.insert(eid);
let edge = topo.edge(eid)?;
vertex_ids.insert(edge.start());
vertex_ids.insert(edge.end());
}
}
Ok((vertex_ids, edge_ids))
}
fn collect_wire_entities(
topo: &Topology,
wire_id: WireId,
) -> Result<(HashSet<VertexId>, HashSet<EdgeId>), crate::OperationsError> {
let mut vertex_ids = HashSet::new();
let mut edge_ids = HashSet::new();
let wire = topo.wire(wire_id)?;
for oe in wire.edges() {
let eid = oe.edge();
edge_ids.insert(eid);
let edge = topo.edge(eid)?;
vertex_ids.insert(edge.start());
vertex_ids.insert(edge.end());
}
Ok((vertex_ids, edge_ids))
}
#[allow(clippy::type_complexity)]
fn collect_solid_entities(
topo: &Topology,
solid: SolidId,
) -> Result<(HashSet<VertexId>, HashSet<EdgeId>, HashSet<FaceId>), crate::OperationsError> {
let mut vertex_ids = HashSet::new();
let mut edge_ids = HashSet::new();
let mut face_ids = HashSet::new();
let solid_data = topo.solid(solid)?;
let shell_ids: Vec<_> = std::iter::once(solid_data.outer_shell())
.chain(solid_data.inner_shells().iter().copied())
.collect();
for shell_id in shell_ids {
let shell = topo.shell(shell_id)?;
let fids: Vec<_> = shell.faces().to_vec();
for face_id in fids {
face_ids.insert(face_id);
let face = topo.face(face_id)?;
let wire_ids: Vec<_> = std::iter::once(face.outer_wire())
.chain(face.inner_wires().iter().copied())
.collect();
for wire_id in wire_ids {
let wire = topo.wire(wire_id)?;
for oe in wire.edges() {
let eid = oe.edge();
edge_ids.insert(eid);
let edge = topo.edge(eid)?;
vertex_ids.insert(edge.start());
vertex_ids.insert(edge.end());
}
}
}
}
Ok((vertex_ids, edge_ids, face_ids))
}
#[cfg(test)]
mod tests;