brep_kernel/construction/
sweep_round_corners.rs1use 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
16pub 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
23fn 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
30pub 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(§ion, 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 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 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 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