Skip to main content

brep_kernel/offset/
shell_rounded.rs

1//! A shell whose wall is joined by arcs: every point of the wall is within the
2//! thickness of a kept face.
3//!
4//! [`crate::offset_shell`] joins the moved faces sharp, extending them until
5//! they meet. Here the wall is the parallel one: inward, the solid less
6//! everything at least the thickness from its kept faces, so the cavity is
7//! rounded along the solid's concave edges; outward, everything outside the
8//! solid within the thickness of its kept faces, so the wall is rounded about
9//! the solid's convex edges and corners. Both come from the whole-solid
10//! parallel offset ([`crate::offset_solid`] with
11//! [`crate::OffsetCorners::Rounded`]), which is exact; an opening is a flat
12//! face, whose plane ends the wall.
13use 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
19/// The prefix of every refusal of this operation.
20pub 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
26/// A point of a flat face's plane and its outward unit normal; refuses a face
27/// that is not flat.
28fn 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
50/// `source` shelled to a wall `distance` thick, joined by arcs, open at the
51/// flat faces `opening_face_ids`. The sign is [`crate::offset_shell`]'s: a
52/// positive distance puts the wall inside the solid, a negative one outside.
53///
54/// Inward, the wall is the solid less every point of it at least the distance
55/// from its kept faces. Outward, it is every point outside the solid within
56/// the distance of its kept faces, ended by the openings' planes. With no
57/// opening the result is the solid's own shell and the parallel one, inside or
58/// outside it.
59///
60/// Refuses an opening that is not flat, an inward wall with two openings that
61/// share an edge, and whatever the parallel offset or
62/// the Booleans refuse.
63pub 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        // two openings that share an edge would need the corner between their run-ons filled too
76        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        // the solid run on past each opening, so that the cavity comes out through it
89        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