Skip to main content

brep_kernel/construction/
sweep_round_corners.rs

1//! A sweep of a plane section along straight runs, carried round each corner
2//! by turning it about the corner.
3//!
4//! Each run is the section slid straight along it, ending flat at the corner.
5//! At a corner the section is turned about the line through the corner square
6//! to both runs, from square to the one run to square to the next. That line
7//! lies in the section's plane and may cross the section, so the section is
8//! cut along it and each part revolved by itself; the part outside the corner
9//! fills the wedge the two runs leave open (for a disc, a piece of a ball),
10//! and the part inside it lies where the runs overlap. The pieces are united.
11use crate::{
12    boolean_operation, boolean_operation_nary, extrude_profile_brep, face_from_surface_trimmed, make_plane, revolve_profile_brep, solid_from_polygons,
13    BooleanOperation, BooleanOptions, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, OrRefuse, Vec3, Vec4,
14};
15
16/// The prefix of every refusal of this operation.
17pub const SWEEP_ROUND_CORNERS_REFUSAL: &str = "sweepRoundCorners";
18
19fn input(what: &'static str, message: String) -> KernelRefusal {
20    KernelRefusal::input(KernelStage::Collect, what, format!("{SWEEP_ROUND_CORNERS_REFUSAL}: {message}"))
21}
22
23/// `curve` with every point of it moved by `map`, a rigid motion (which
24/// carries a rational curve pole by pole).
25fn moved(curve: &NurbsCurve, map: &dyn Fn(Vec3) -> Vec3) -> Result<NurbsCurve, KernelRefusal> {
26    let poles = curve.control_points.iter().map(|pole| Vec4::from_point(map(Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w)), pole.w)).collect();
27    NurbsCurve::new(curve.degree, curve.knots.clone(), poles).or_refuse(KernelStage::Collect, "sweepRoundCorners.move")
28}
29
30/// The solid `section` sweeps along the straight runs through `path`, turned
31/// about each corner.
32///
33/// `section` is a closed loop of curves (or one closed curve) in the plane
34/// through `path[0]` square to the first run. Consecutive runs may turn by any
35/// angle short of a half turn; runs in one line simply continue.
36///
37/// Refuses a path of fewer than two points or with a run of no length, a
38/// corner that turns straight back, and whatever the extrudes, the revolves
39/// or the union refuse.
40pub fn sweep_profile_round_corners(section: &[NurbsCurve], path: &[Vec3]) -> Result<BrepSolid, KernelRefusal> {
41    if path.len() < 2 {
42        return Err(input("path", "the path needs at least two points".into()));
43    }
44    if section.is_empty() {
45        return Err(input("section", "the section is empty".into()));
46    }
47    let stage = KernelStage::Collect;
48    let poles = |curves: &[NurbsCurve]| -> Vec<Vec3> {
49        curves.iter().flat_map(|curve| &curve.control_points).map(|pole| Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w)).collect()
50    };
51    let reach = poles(section).iter().map(|point| point.sub(path[0]).length()).fold(0.0, f64::max);
52    if reach <= 0.0 {
53        return Err(input("section", "the section has no extent".into()));
54    }
55    let mut section = section.to_vec();
56    let mut parts = Vec::new();
57    for index in 0..path.len() - 1 {
58        let run = path[index + 1].sub(path[index]);
59        let along = run.normalized().map_err(|_| input("path", format!("run {index} has no length")))?;
60        parts.push(extrude_profile_brep(&section, along, run.length())?);
61        section = section.iter().map(|curve| moved(curve, &|point| point.add(run))).collect::<Result<_, _>>()?;
62        let Some(after) = path.get(index + 2) else { break };
63        let next = after.sub(path[index + 1]).normalized().map_err(|_| input("path", format!("run {} has no length", index + 1)))?;
64        let turn = along.cross(next);
65        if turn.length() <= 1e-12 {
66            if along.dot(next) < 0.0 {
67                return Err(input("corner", format!("the path turns straight back at point {}", index + 1)));
68            }
69            continue;
70        }
71        let corner = path[index + 1];
72        let axis = turn.normalized().or_refuse(stage, "sweepRoundCorners.axis")?;
73        let angle = turn.length().atan2(along.dot(next));
74        // the section's plane at the corner: the axis, and the line across it
75        let across = axis.cross(along);
76        let span = 4.0 * reach;
77        let plane = make_plane(corner.sub(axis.scale(span)).sub(across.scale(span)), axis, across, 2.0 * span, 2.0 * span).or_refuse(stage, "sweepRoundCorners.plane")?;
78        let sheet = face_from_surface_trimmed(&plane, &[section.clone()])?;
79        for side in [1.0, -1.0] {
80            // the block on one side of the axis, holding that side of the section
81            let at = |a: f64, b: f64, c: f64| corner.add(axis.scale(a * span)).add(across.scale(side * b * span)).add(along.scale(c * span));
82            let mut faces = Vec::new();
83            for s in [0.0, 1.0] {
84                faces.push(vec![at(-1.0, s, -1.0), at(1.0, s, -1.0), at(1.0, s, 1.0), at(-1.0, s, 1.0)]);
85                faces.push(vec![at(2.0 * s - 1.0, 0.0, -1.0), at(2.0 * s - 1.0, 1.0, -1.0), at(2.0 * s - 1.0, 1.0, 1.0), at(2.0 * s - 1.0, 0.0, 1.0)]);
86                faces.push(vec![at(-1.0, 0.0, 2.0 * s - 1.0), at(1.0, 0.0, 2.0 * s - 1.0), at(1.0, 1.0, 2.0 * s - 1.0), at(-1.0, 1.0, 2.0 * s - 1.0)]);
87            }
88            let block = solid_from_polygons(&faces)?;
89            let part = boolean_operation(&sheet, &block, BooleanOperation::Intersect, &BooleanOptions::default())?;
90            for face in part.shells.iter().flat_map(|shell| &shell.faces) {
91                if face.loops.len() != 1 {
92                    return Err(KernelRefusal::unsupported(KernelStage::Fragment, "section_holes", format!("{SWEEP_ROUND_CORNERS_REFUSAL}: a section with a hole is not turned about a corner")));
93                }
94                let mut outline = Vec::new();
95                for coedge in &face.loops[0].coedges {
96                    let edge = part.edges.iter().find(|edge| edge.id == coedge.edge_id).ok_or_else(|| input("section", "the cut section refers to a missing edge".into()))?;
97                    // the edge is its curve between t0 and t1
98                    let [low, high] = edge.curve.domain().or_refuse(stage, "sweepRoundCorners.domain")?;
99                    let margin = 1e-9 * (high - low);
100                    let mut piece = edge.curve.clone();
101                    if edge.t1 < high - margin {
102                        piece = piece.split(edge.t1).or_refuse(stage, "sweepRoundCorners.trim")?.0;
103                    }
104                    if edge.t0 > low + margin {
105                        piece = piece.split(edge.t0).or_refuse(stage, "sweepRoundCorners.trim")?.1;
106                    }
107                    outline.push(if coedge.forward { piece } else { piece.reversed().or_refuse(stage, "sweepRoundCorners.reverse")? });
108                }
109                parts.push(revolve_profile_brep(&outline, corner, axis, angle).or_refuse(KernelStage::Fragment, "sweepRoundCorners.turn")?);
110            }
111        }
112        section = section.iter().map(|curve| moved(curve, &|point| corner.add(point.sub(corner).rotate_about(axis, angle)))).collect::<Result<_, _>>()?;
113    }
114    if parts.len() == 1 {
115        return Ok(parts.remove(0));
116    }
117    boolean_operation_nary(&parts, BooleanOperation::Union)
118}
119