brep_kernel/offset/
shell_rounded.rs1use crate::topology::FaceRecord;
14use crate::{
15 boolean_operation, boolean_operation_nary, offset_solid, split_solid_by_plane, thicken_trimmed_sheet, BooleanOperation, BooleanOptions, BrepSolid,
16 KernelRefusal, KernelStage, NurbsCurve, OffsetCorners, OrRefuse, Vec3,
17};
18
19pub const SHELL_ROUNDED_REFUSAL: &str = "offsetShellRounded";
21
22fn input(what: &'static str, message: String) -> KernelRefusal {
23 KernelRefusal::input(KernelStage::Collect, what, format!("{SHELL_ROUNDED_REFUSAL}: {message}"))
24}
25
26fn opening_plane(face: &FaceRecord) -> Result<(Vec3, Vec3), KernelRefusal> {
29 let stage = KernelStage::Collect;
30 let surface = &face.surface;
31 let [u0, u1] = surface.domain_u().or_refuse(stage, "offsetShellRounded.domain")?;
32 let [v0, v1] = surface.domain_v().or_refuse(stage, "offsetShellRounded.domain")?;
33 let at = |a: f64, b: f64| (u0 + (u1 - u0) * a, v0 + (v1 - v0) * b);
34 let (u, v) = at(0.5, 0.5);
35 let origin = surface.evaluate(u, v).or_refuse(stage, "offsetShellRounded.point")?;
36 let normal = surface.normal(u, v).or_refuse(stage, "offsetShellRounded.normal")?;
37 let size = surface.evaluate(u0, v0).or_refuse(stage, "offsetShellRounded.point")?.sub(surface.evaluate(u1, v1).or_refuse(stage, "offsetShellRounded.point")?).length();
38 for a in [0.0, 0.25, 0.5, 0.75, 1.0] {
39 for b in [0.0, 0.25, 0.5, 0.75, 1.0] {
40 let (u, v) = at(a, b);
41 let point = surface.evaluate(u, v).or_refuse(stage, "offsetShellRounded.point")?;
42 if point.sub(origin).dot(normal).abs() > 1e-9 * size {
43 return Err(KernelRefusal::unsupported(stage, "opening_flat", format!("{SHELL_ROUNDED_REFUSAL}: opening face {} is not flat", face.id)));
44 }
45 }
46 }
47 Ok((origin, if face.same_sense { normal } else { normal.scale(-1.0) }))
48}
49
50pub fn offset_shell_rounded(source: &BrepSolid, opening_face_ids: &[u64], distance: f64) -> Result<BrepSolid, KernelRefusal> {
64 if !distance.is_finite() || distance.abs() <= 1e-12 {
65 return Err(input("distance", "distance must be a nonzero finite value".into()));
66 }
67 let d = distance.abs();
68 let mut openings = Vec::with_capacity(opening_face_ids.len());
69 for id in opening_face_ids {
70 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}")))?;
71 openings.push((face, opening_plane(face)?));
72 }
73 let options = BooleanOptions::default();
74 if distance > 0.0 {
75 let edges = |face: &FaceRecord| -> Vec<u64> { face.loops.iter().flat_map(|record| &record.coedges).map(|coedge| coedge.edge_id).collect() };
77 for (index, (first, _)) in openings.iter().enumerate() {
78 for (second, _) in openings.iter().skip(index + 1) {
79 if edges(first).iter().any(|edge| edges(second).contains(edge)) {
80 return Err(KernelRefusal::unsupported(
81 KernelStage::Collect,
82 "openings_adjacent",
83 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),
84 ));
85 }
86 }
87 }
88 let mut run_on = vec![source.clone()];
90 for (face, _) in &openings {
91 let loops: Vec<Vec<NurbsCurve>> = face.loops.iter().map(|record| record.coedges.iter().map(|coedge| coedge.pcurve.clone()).collect()).collect();
92 let outward = if face.same_sense { 2.0 * d } else { -2.0 * d };
93 run_on.extend(thicken_trimmed_sheet(&face.surface, &loops, outward, false)?);
94 }
95 let body = if run_on.len() == 1 { run_on.remove(0) } else { boolean_operation_nary(&run_on, BooleanOperation::Union)? };
96 let cavity = offset_solid(&body, -d, OffsetCorners::Rounded)?.solid;
97 boolean_operation(source, &cavity, BooleanOperation::Subtract, &options)
98 } else {
99 let mut grown = offset_solid(source, d, OffsetCorners::Rounded)?.solid;
100 for (_, (origin, normal)) in &openings {
101 grown = split_solid_by_plane(&grown, *origin, *normal).or_refuse(KernelStage::Fragment, "offsetShellRounded.split")?.0;
102 }
103 boolean_operation(&grown, source, BooleanOperation::Subtract, &options)
104 }
105}
106