use crate::topology::FaceRecord;
use crate::{
boolean_operation, boolean_operation_nary, offset_solid, split_solid_by_plane, thicken_trimmed_sheet, BooleanOperation, BooleanOptions, BrepSolid,
KernelRefusal, KernelStage, NurbsCurve, OffsetCorners, OrRefuse, Vec3,
};
pub const SHELL_ROUNDED_REFUSAL: &str = "offsetShellRounded";
fn input(what: &'static str, message: String) -> KernelRefusal {
KernelRefusal::input(KernelStage::Collect, what, format!("{SHELL_ROUNDED_REFUSAL}: {message}"))
}
fn opening_plane(face: &FaceRecord) -> Result<(Vec3, Vec3), KernelRefusal> {
let stage = KernelStage::Collect;
let surface = &face.surface;
let [u0, u1] = surface.domain_u().or_refuse(stage, "offsetShellRounded.domain")?;
let [v0, v1] = surface.domain_v().or_refuse(stage, "offsetShellRounded.domain")?;
let at = |a: f64, b: f64| (u0 + (u1 - u0) * a, v0 + (v1 - v0) * b);
let (u, v) = at(0.5, 0.5);
let origin = surface.evaluate(u, v).or_refuse(stage, "offsetShellRounded.point")?;
let normal = surface.normal(u, v).or_refuse(stage, "offsetShellRounded.normal")?;
let size = surface.evaluate(u0, v0).or_refuse(stage, "offsetShellRounded.point")?.sub(surface.evaluate(u1, v1).or_refuse(stage, "offsetShellRounded.point")?).length();
for a in [0.0, 0.25, 0.5, 0.75, 1.0] {
for b in [0.0, 0.25, 0.5, 0.75, 1.0] {
let (u, v) = at(a, b);
let point = surface.evaluate(u, v).or_refuse(stage, "offsetShellRounded.point")?;
if point.sub(origin).dot(normal).abs() > 1e-9 * size {
return Err(KernelRefusal::unsupported(stage, "opening_flat", format!("{SHELL_ROUNDED_REFUSAL}: opening face {} is not flat", face.id)));
}
}
}
Ok((origin, if face.same_sense { normal } else { normal.scale(-1.0) }))
}
pub fn offset_shell_rounded(source: &BrepSolid, opening_face_ids: &[u64], distance: f64) -> Result<BrepSolid, KernelRefusal> {
if !distance.is_finite() || distance.abs() <= 1e-12 {
return Err(input("distance", "distance must be a nonzero finite value".into()));
}
let d = distance.abs();
let mut openings = Vec::with_capacity(opening_face_ids.len());
for id in opening_face_ids {
let face = source.shells.iter().flat_map(|shell| &shell.faces).find(|face| face.id == *id).ok_or_else(|| input("opening", format!("the solid has no face {id}")))?;
openings.push((face, opening_plane(face)?));
}
let options = BooleanOptions::default();
if distance > 0.0 {
let edges = |face: &FaceRecord| -> Vec<u64> { face.loops.iter().flat_map(|record| &record.coedges).map(|coedge| coedge.edge_id).collect() };
for (index, (first, _)) in openings.iter().enumerate() {
for (second, _) in openings.iter().skip(index + 1) {
if edges(first).iter().any(|edge| edges(second).contains(edge)) {
return Err(KernelRefusal::unsupported(
KernelStage::Collect,
"openings_adjacent",
format!("{SHELL_ROUNDED_REFUSAL}: openings {} and {} share an edge; an inward wall open at two faces that meet is not built", first.id, second.id),
));
}
}
}
let mut run_on = vec![source.clone()];
for (face, _) in &openings {
let loops: Vec<Vec<NurbsCurve>> = face.loops.iter().map(|record| record.coedges.iter().map(|coedge| coedge.pcurve.clone()).collect()).collect();
let outward = if face.same_sense { 2.0 * d } else { -2.0 * d };
run_on.extend(thicken_trimmed_sheet(&face.surface, &loops, outward, false)?);
}
let body = if run_on.len() == 1 { run_on.remove(0) } else { boolean_operation_nary(&run_on, BooleanOperation::Union)? };
let cavity = offset_solid(&body, -d, OffsetCorners::Rounded)?.solid;
boolean_operation(source, &cavity, BooleanOperation::Subtract, &options)
} else {
let mut grown = offset_solid(source, d, OffsetCorners::Rounded)?.solid;
for (_, (origin, normal)) in &openings {
grown = split_solid_by_plane(&grown, *origin, *normal).or_refuse(KernelStage::Fragment, "offsetShellRounded.split")?.0;
}
boolean_operation(&grown, source, BooleanOperation::Subtract, &options)
}
}