Skip to main content

ogeom_fillet/
blend.rs

1//! Rounds and bevels in one request, closed as one corner where they meet.
2
3use std::collections::BTreeMap;
4
5use crate::setback::CornerEnd;
6use crate::{Chamfer, ChamferCorner, RoundProfile};
7use ogeom_algo::Built;
8use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
9use ogeom_geom::Continuity;
10use ogeom_topo::{Model, Shape, ShapeType, TShapeId, explore_unique};
11
12/// How [`blend_edges`] blends one edge.
13#[derive(Debug, Clone)]
14pub enum Blend {
15    /// A round of this radius, as [`fillet_edges_with`](crate::fillet_edges_with).
16    Round(f64),
17    /// A bevel, as [`chamfer_edges_with`](crate::chamfer_edges_with).
18    Chamfer(Chamfer),
19    /// A round meeting `face` `on_face` from the edge and the edge's other
20    /// face `on_other` from it, its section `profile`, as
21    /// [`fillet_edge_distances`](crate::fillet_edge_distances).
22    RoundDistances {
23        /// The face the first distance runs along.
24        face: Shape,
25        /// The distance along `face`.
26        on_face: f64,
27        /// The distance along the edge's other face.
28        on_other: f64,
29        /// The section between the two contacts.
30        profile: RoundProfile,
31    },
32}
33
34/// Round and bevel the edges of `solid` as `edges` says, in one request.
35///
36/// A request of rounds alone is
37/// [`fillet_edges_with`](crate::fillet_edges_with), and one of bevels
38/// alone [`chamfer_edges_cornered`](crate::chamfer_edges_cornered) with
39/// `corner`, exactly. Where both are asked, the bevels are cut first and
40/// the rounds then, each kind in the order of its edges' nodes, so the
41/// order of the request does not matter; at a vertex where only one kind
42/// meets, that is what the kind's own call builds.
43///
44/// At a vertex where a round meets a bevel, every edge there must be in
45/// the request, and the vertex a convex corner of planes: each band is
46/// stopped at the least setback where its section clears every other band
47/// and one of its own widths past it, the cell short of the sections cut
48/// away (all that the order of the build leaves there), and the opening
49/// closed by one patch. On each face between two bands it is trimmed by a
50/// conic tangent to both bands' lines there. The patch is tangent to each
51/// round's band and to each face, and meets each bevel's end in position
52/// only: the bevel meets its faces in a crease, so where its end meets a
53/// face the patch takes the face's tangent plane.
54///
55/// A vertex of three edges where one round meets one bevel and the third
56/// edge is left sharp closes as
57/// [`round_vertex_setback_sharp`](crate::round_vertex_setback_sharp)
58/// closes a corner: the sharp edge is cut as far out as the farther band,
59/// at `Q`, where a three-sided patch held across both of its faces turns
60/// its normal from one face's to the other's, and one patch over the rest
61/// meets the round's band and the faces between rounds tangent, and the
62/// bevel's end and the trims from it in position only.
63///
64/// # Errors
65///
66/// As the rounds' and the bevels' own calls, and
67/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) where
68/// an edge is asked twice, or a round meets a bevel at a vertex where an
69/// edge is left sharp other than one beside one round and one bevel; or
70/// where that vertex is not a convex corner of planes.
71pub fn blend_edges(
72    model: &mut Model,
73    solid: &Shape,
74    edges: &[(Shape, Blend)],
75    corner: ChamferCorner,
76    tol: Tolerances,
77) -> OgeomResult<Built> {
78    let mut rounds: Vec<(Shape, f64)> = Vec::new();
79    let mut bevels: Vec<(Shape, Chamfer)> = Vec::new();
80    let mut asymmetric: Vec<crate::asym::Asymmetric> = Vec::new();
81    for (k, (edge, blend)) in edges.iter().enumerate() {
82        if edges[..k].iter().any(|(e, _)| e.is_same(edge)) {
83            ogeom_bail!(Construction, "an edge is asked to be blended twice");
84        }
85        match blend {
86            Blend::Round(r) => rounds.push((edge.clone(), *r)),
87            Blend::Chamfer(c) => bevels.push((edge.clone(), c.clone())),
88            Blend::RoundDistances {
89                face,
90                on_face,
91                on_other,
92                profile,
93            } => {
94                // Equal distances, elliptical: the circular round, which
95                // meets the request's other rounds and bevels as one.
96                if *profile == RoundProfile::Elliptical && on_face == on_other {
97                    let r = crate::asym::equal_round(model, solid, edge, face, *on_face, tol)?;
98                    rounds.push((edge.clone(), r));
99                } else {
100                    asymmetric.push(crate::asym::Asymmetric {
101                        edge: edge.clone(),
102                        face: face.clone(),
103                        on_face: *on_face,
104                        on_other: *on_other,
105                        profile: *profile,
106                    });
107                }
108            }
109        }
110    }
111    if !asymmetric.is_empty() {
112        // The corners where an asymmetric round meets the request's other
113        // edges, read off the solid as it stands, their edges set apart.
114        let corners =
115            crate::asym::asymmetric_corners(model, solid, &rounds, &bevels, &asymmetric, tol)?;
116        let at_corner = |e: &Shape| corners.edges.iter().any(|c| c.is_same(e));
117        let (corner_rounds, rounds): (Vec<_>, Vec<_>) =
118            rounds.into_iter().partition(|(e, _)| at_corner(e));
119        let (corner_bevels, bevels): (Vec<_>, Vec<_>) =
120            bevels.into_iter().partition(|(e, _)| at_corner(e));
121        let (corner_asymmetric, asymmetric): (Vec<_>, Vec<_>) =
122            asymmetric.into_iter().partition(|a| at_corner(&a.edge));
123        let others: Vec<Shape> = rounds
124            .iter()
125            .map(|(e, _)| e.clone())
126            .chain(bevels.iter().map(|(e, _)| e.clone()))
127            .chain(corners.edges.iter().cloned())
128            .collect();
129        let symmetric_only = rounds.is_empty() && bevels.is_empty();
130        let built = crate::asym::after_symmetric(
131            model,
132            &others,
133            asymmetric,
134            |model| {
135                if symmetric_only {
136                    Ok(Built::new(solid.clone(), ogeom_algo::History::new()))
137                } else {
138                    blend_symmetric(model, solid, rounds, bevels, corner, tol)
139                }
140            },
141            tol,
142        )?;
143        return crate::asym::close_asymmetric_corners(
144            model,
145            solid,
146            built,
147            &corners,
148            (&corner_rounds, &corner_bevels, &corner_asymmetric),
149            tol,
150        );
151    }
152    blend_symmetric(model, solid, rounds, bevels, corner, tol)
153}
154
155/// [`blend_edges`] of the rounds and bevels of one radius each.
156fn blend_symmetric(
157    model: &mut Model,
158    solid: &Shape,
159    mut rounds: Vec<(Shape, f64)>,
160    mut bevels: Vec<(Shape, Chamfer)>,
161    corner: ChamferCorner,
162    tol: Tolerances,
163) -> OgeomResult<Built> {
164    if rounds.is_empty() {
165        return crate::chamfer_edges_cornered(model, solid, &bevels, corner, tol);
166    }
167    if bevels.is_empty() {
168        return crate::fillet_edges_with(model, solid, &rounds, tol);
169    }
170    rounds.sort_by_key(|(e, _)| e.node());
171    bevels.sort_by_key(|(e, _)| e.node());
172
173    // The vertices where a round meets a bevel, each with the request's
174    // edges there.
175    let mut at: BTreeMap<TShapeId, (Shape, Vec<usize>, Vec<usize>)> = BTreeMap::new();
176    for (i, (edge, _)) in rounds.iter().enumerate() {
177        for v in ends(model, edge)? {
178            at.entry(v.node())
179                .or_insert_with(|| (v.clone(), Vec::new(), Vec::new()))
180                .1
181                .push(i);
182        }
183    }
184    for (i, (edge, _)) in bevels.iter().enumerate() {
185        for v in ends(model, edge)? {
186            at.entry(v.node())
187                .or_insert_with(|| (v.clone(), Vec::new(), Vec::new()))
188                .2
189                .push(i);
190        }
191    }
192    let solid_edges = explore_unique(model, solid, ShapeType::Edge)?;
193    let mut mixed: Vec<(Shape, Vec<usize>, Vec<usize>)> = Vec::new();
194    for (vertex, r, b) in at.into_values() {
195        if r.is_empty() || b.is_empty() {
196            continue;
197        }
198        let mut meeting = 0;
199        for e in &solid_edges {
200            if ends(model, e)?.iter().any(|v| v.is_same(&vertex)) {
201                meeting += 1;
202            }
203        }
204        // One edge left sharp beside one round and one bevel: the opening
205        // crosses it at a point, where the patch's normal turns.
206        let one_sharp = meeting == 3 && r.len() == 1 && b.len() == 1;
207        if meeting != r.len() + b.len() && !one_sharp {
208            ogeom_bail!(
209                Construction,
210                "a round meets a bevel at a vertex where an edge is left sharp: the \
211                 patch closing the round's opening would cross the bevel's sharp end \
212                 line on the face beside it and be tangent there to two planes; blend \
213                 every edge at the vertex, or leave one sharp beside one round and one \
214                 bevel"
215            );
216        }
217        mixed.push((vertex, r, b));
218    }
219
220    // Each mixed corner's frame and setbacks, read off the solid as it
221    // stands.
222    let mut corners = Vec::with_capacity(mixed.len());
223    for (vertex, r, b) in &mixed {
224        let largest = r.iter().map(|&i| rounds[i].1).fold(0.0, f64::max);
225        let frame = crate::corner::corner_frame(model, solid, vertex, largest, tol)?;
226        let n = frame.d.len();
227        let mut widths = Vec::with_capacity(n);
228        let mut kinds = Vec::with_capacity(n);
229        let mut sharp: Option<usize> = None;
230        for k in 0..n {
231            let (ma, mb) = (frame.m[frame.ring[k]], frame.m[frame.ring[(k + 1) % n]]);
232            let dk = frame.d[k].vector();
233            let along = |edge: &Shape| -> OgeomResult<bool> {
234                let Some((a, z)) = ogeom_algo::edge_vertices(model, edge)? else {
235                    return Ok(false);
236                };
237                let far = if a.is_same(vertex) { z } else { a };
238                let p = point_of(model, &far)? - frame.corner;
239                let length = p.magnitude();
240                Ok(length > tol.confusion() && p.dot(dk) / length >= 1.0 - tol.angular().sqrt())
241            };
242            let mut found = None;
243            for &i in r {
244                if along(&rounds[i].0)? {
245                    let cos = -ma.dot(mb);
246                    let sin = (1.0 - cos * cos).max(0.0).sqrt();
247                    if sin <= tol.angular() {
248                        ogeom_bail!(
249                            Construction,
250                            "two faces at a corner meet flat; there is no edge to round"
251                        );
252                    }
253                    let w = rounds[i].1 * (1.0 + cos) / sin;
254                    found = Some(((w, w), CornerEnd::Round));
255                }
256            }
257            for &i in b {
258                if along(&bevels[i].0)? {
259                    let (edge, spec) = &bevels[i];
260                    let crate::chamfer::Legs::Straight(seat, d) =
261                        crate::chamfer::legs(model, solid, edge, spec, tol)?
262                    else {
263                        ogeom_bail!(
264                            Construction,
265                            "a corner of rounds and bevels takes bevels on straight edges \
266                             between planes"
267                        );
268                    };
269                    let on = |m: ogeom_math::Vector| {
270                        (0..2)
271                            .find(|&j| seat.normals[j].cross(m).magnitude() <= tol.angular() * 10.0)
272                    };
273                    let (Some(ja), Some(jb)) = (on(ma), on(mb)) else {
274                        ogeom_bail!(
275                            Construction,
276                            "a bevel at a corner does not lie on the corner's faces"
277                        );
278                    };
279                    found = Some(((d[ja], d[jb]), CornerEnd::Bevel));
280                }
281            }
282            let Some((w, kind)) = found.or_else(|| {
283                // The one edge left sharp: a band of no width.
284                (sharp.is_none() && n == 3 && r.len() + b.len() == 2).then(|| {
285                    sharp = Some(k);
286                    ((0.0, 0.0), CornerEnd::Round)
287                })
288            }) else {
289                ogeom_bail!(
290                    Construction,
291                    "an edge at a corner of rounds and bevels is not one of the request's, \
292                     or is curved"
293                );
294            };
295            widths.push(w);
296            kinds.push(kind);
297        }
298        let mut setback = crate::setback::least_setbacks_widths(&frame, &widths, tol)?;
299        if let Some(ks) = sharp {
300            // The sharp edge cut as far out as the farther of the bands
301            // beside it, clear of their corner.
302            setback[ks] = (0..n)
303                .filter(|&k| k != ks)
304                .map(|k| setback[k])
305                .fold(0.0, f64::max);
306        }
307        corners.push((vertex.clone(), frame, setback, kinds, sharp));
308    }
309
310    // The bevels, then the rounds on what they leave.
311    let beveled = crate::chamfer_edges_cornered(model, solid, &bevels, corner, tol)?;
312    let mut traced: Vec<(Shape, f64)> = Vec::with_capacity(rounds.len());
313    for (edge, r) in &rounds {
314        let now = beveled.history.trace(edge);
315        let [one] = now else {
316            ogeom_bail!(
317                Construction,
318                "a round's edge does not come through the bevels as one edge"
319            );
320        };
321        traced.push((one.clone(), *r));
322    }
323    let rounded = crate::fillet_edges_with(model, &beveled.shape, &traced, tol)?;
324    let mut built = Built {
325        shape: rounded.shape.clone(),
326        history: beveled.history.then(&rounded.history),
327    };
328    for (vertex, frame, setback, kinds, sharp) in &corners {
329        built = match sharp {
330            Some(ks) => crate::sharp::close_sharp_corner(
331                model, solid, built, vertex, frame, setback, *ks, kinds, tol,
332            )?,
333            None => crate::setback::close_corner_ends(
334                model,
335                solid,
336                built,
337                vertex,
338                frame,
339                setback,
340                kinds,
341                Continuity::G1,
342                tol,
343            )?,
344        };
345    }
346    Ok(built)
347}
348
349/// An edge's two vertices.
350fn ends(model: &Model, edge: &Shape) -> OgeomResult<Vec<Shape>> {
351    Ok(match ogeom_algo::edge_vertices(model, edge)? {
352        Some((a, b)) => vec![a, b],
353        None => Vec::new(),
354    })
355}
356
357/// A vertex's point, placed.
358fn point_of(model: &Model, vertex: &Shape) -> OgeomResult<ogeom_math::Point> {
359    let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
360        ogeom_bail!(Dangling, "a vertex is not in this model");
361    };
362    Ok(vertex.transform(model.datums())?.apply(data.point))
363}