1use 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#[derive(Debug, Clone)]
14pub enum Blend {
15 Round(f64),
17 Chamfer(Chamfer),
19 RoundDistances {
23 face: Shape,
25 on_face: f64,
27 on_other: f64,
29 profile: RoundProfile,
31 },
32}
33
34pub 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 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 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
155fn 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 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 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 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 (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 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 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
349fn 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
357fn 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}