1use crate::support::{Seat, face_from_edges, planar_face, segment_between};
24use ogeom_algo::{Built, make_edge_between, make_vertex};
25use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
26use ogeom_geom::{BSplineCurve, Curve, ExtrusionSurface, PlaneSurface};
27use ogeom_math::bspline::Weighted;
28use ogeom_math::{Direction, KnotVector, Plane, Point, Vector};
29use ogeom_topo::{Model, Shape};
30
31#[derive(Debug, Clone, Copy, PartialEq)]
33pub enum RoundProfile {
34 Elliptical,
39 Conic(f64),
44}
45
46impl RoundProfile {
47 pub(crate) fn weight(self, cos_alpha: f64) -> OgeomResult<f64> {
50 match self {
51 Self::Elliptical => Ok(((1.0 - cos_alpha) * 0.5).max(0.0).sqrt()),
52 Self::Conic(rho) => {
53 if !(rho > 0.0 && rho < 1.0) {
54 ogeom_bail!(
55 Construction,
56 "a conic round's rho of {rho} is not strictly between 0 and 1"
57 );
58 }
59 Ok(rho / (1.0 - rho))
60 }
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy)]
67pub(crate) enum Section {
68 Ball(f64),
70 Conic {
73 d: [f64; 2],
75 w: f64,
77 },
78}
79
80impl Section {
81 pub(crate) fn size(self) -> f64 {
84 match self {
85 Self::Ball(radius) => radius,
86 Self::Conic { d, .. } => d[0].max(d[1]),
87 }
88 }
89
90 pub(crate) fn centre(self, seat: &Seat, tol: Tolerances) -> OgeomResult<Option<Vector>> {
94 match self {
95 Self::Ball(radius) => crate::fillet::seat_centre(seat, radius, tol),
96 Self::Conic { d, w } => {
97 let f = legs(seat, tol)?;
98 Ok(Some((f[0] * d[0] + f[1] * d[1]) / (2.0 * (1.0 + w))))
99 }
100 }
101 }
102
103 pub(crate) fn points(
107 self,
108 seat: &Seat,
109 at: Point,
110 inward: Vector,
111 tol: Tolerances,
112 ) -> OgeomResult<[Point; 6]> {
113 match self {
114 Self::Ball(radius) => Ok(crate::fillet::section_at(seat, at, inward, radius)),
115 Self::Conic { d, .. } => {
116 let f = legs(seat, tol)?;
117 let touch = [at + f[0] * d[0], at + f[1] * d[1]];
118 Ok([
119 at,
120 touch[0],
121 touch[1],
122 at.midpoint(touch[0]),
123 at.midpoint(touch[1]),
124 at + inward,
125 ])
126 }
127 }
128 }
129}
130
131pub(crate) fn legs(seat: &Seat, tol: Tolerances) -> OgeomResult<[Vector; 2]> {
134 let sign = if seat.convex { 1.0 } else { -1.0 };
135 Ok([seat.leg(0, tol)? * sign, seat.leg(1, tol)? * sign])
136}
137
138pub(crate) fn seated_conic(
142 model: &mut Model,
143 solid: &Shape,
144 seat: &Seat,
145 section: Section,
146 edge: Option<&Shape>,
147 trims: &[(Point, Vector)],
148 tol: Tolerances,
149) -> OgeomResult<Built> {
150 let Section::Conic { d, w } = section else {
151 ogeom_bail!(Construction, "a conic band asked of a ball's section");
152 };
153 let (first, second) = if seat
157 .along
158 .dot(seat.normals[0].cross(seat.normals[1]))
159 .is_sign_positive()
160 {
161 (0, 1)
162 } else {
163 (1, 0)
164 };
165 let n1 = seat.normals[first];
166 let n2 = seat.normals[second];
167 let sweep = seat.along.dot(n1.cross(n2)).atan2(n1.dot(n2));
168 if sweep <= tol.angular() {
169 ogeom_bail!(Construction, "the faces are parallel; there is no corner");
170 }
171 let sign = if seat.convex { 1.0 } else { -1.0 };
172 let f = legs(seat, tol)?;
173 let (fa, fb) = (f[first], f[second]);
174 let (da, db) = (d[first], d[second]);
175
176 let travel = seat.end - seat.start;
177 let length = travel.magnitude();
178 let apex0 = seat.start;
179 let apex1 = seat.end;
180 let contact_a0 = apex0 + fa * da;
181 let contact_b0 = apex0 + fb * db;
182 let contact_a1 = contact_a0 + travel;
183 let contact_b1 = contact_b0 + travel;
184 let along_dir = Direction::new(seat.along, tol)?;
185
186 let arc_curve = |h: f64| -> OgeomResult<Curve> {
189 let lift = seat.along * h;
190 conic_arc([contact_a0 + lift, apex0 + lift, contact_b0 + lift], w, tol)
191 };
192 let arc = |model: &mut Model, h: f64, from: &Shape, to: &Shape| -> OgeomResult<Shape> {
193 Ok(make_edge_between(model, arc_curve(h)?, (0.0, 1.0), from, to, tol)?.shape)
194 };
195
196 let leg_a = planar_face(
198 model,
199 &[apex0, contact_a0, contact_a1, apex1],
200 n1 * sign,
201 tol,
202 )?;
203 let leg_b = planar_face(
204 model,
205 &[apex0, contact_b0, contact_b1, apex1],
206 n2 * sign,
207 tol,
208 )?;
209
210 let reach = da.max(db);
211 let cap = |model: &mut Model,
212 apex: Point,
213 ca: Point,
214 cb: Point,
215 h: f64,
216 outward: Vector|
217 -> OgeomResult<Shape> {
218 let plane = Plane::through(apex, Direction::new(outward, tol)?);
219 let span = (apex.distance(ca).max(apex.distance(cb)) + reach).max(1.0) * 2.0;
220 let surface = PlaneSurface::over(plane, (-span, span), (-span, span))?;
221 let apex_v = make_vertex(model, apex).shape;
222 let ca_v = make_vertex(model, ca).shape;
223 let cb_v = make_vertex(model, cb).shape;
224 let edges = vec![
225 segment_between(model, (&apex_v, apex), (&ca_v, ca), tol)?,
226 arc(model, h, &ca_v, &cb_v)?,
227 segment_between(model, (&cb_v, cb), (&apex_v, apex), tol)?,
228 ];
229 let edges: Vec<Shape> = if (ca - apex).cross(cb - apex).dot(outward) < 0.0 {
230 edges.iter().rev().map(Shape::reversed).collect()
231 } else {
232 edges
233 };
234 face_from_edges(model, surface.into(), &edges, tol)
235 };
236 let cap0 = cap(model, apex0, contact_a0, contact_b0, 0.0, -seat.along)?;
237 let cap1 = cap(model, apex1, contact_a1, contact_b1, length, seat.along)?;
238
239 let band = {
244 let surface = ExtrusionSurface::new(arc_curve(0.0)?, along_dir, length)?;
245 let a0_v = make_vertex(model, contact_a0).shape;
246 let b0_v = make_vertex(model, contact_b0).shape;
247 let a1_v = make_vertex(model, contact_a1).shape;
248 let b1_v = make_vertex(model, contact_b1).shape;
249 let edges = vec![
250 arc(model, 0.0, &a0_v, &b0_v)?,
251 segment_between(model, (&b0_v, contact_b0), (&b1_v, contact_b1), tol)?,
252 arc(model, length, &a1_v, &b1_v)?.reversed(),
253 segment_between(model, (&a0_v, contact_a0), (&a1_v, contact_a1), tol)?.reversed(),
254 ];
255 face_from_edges(model, surface.into(), &edges, tol)?
256 };
257
258 let faces = [leg_a, leg_b, cap0, cap1, band.reversed()];
259 let mut wedge = crate::support::wedge_solid(model, &faces, false, tol)?;
260 let span = length + (reach * 2.0) * 4.0;
261 for &plane in trims {
262 wedge = crate::support::trimmed_wedge(model, &wedge, plane, span, tol)?;
263 }
264 crate::support::apply_wedge_solid(model, solid, edge, &wedge, !seat.convex, tol)
265}
266
267const WEIGHT_RANGE: (f64, f64) = (0.7, 1.25);
272
273pub(crate) fn conic_arc(p: [Point; 3], w: f64, tol: Tolerances) -> OgeomResult<Curve> {
280 let (control, weight, knots) = conic_pieces(p, w);
281 let control = control
282 .into_iter()
283 .enumerate()
284 .map(|(i, q)| Weighted::new(q, if i % 2 == 1 { weight } else { 1.0 }, tol))
285 .collect::<OgeomResult<Vec<_>>>()?;
286 Ok(Curve::BSpline(BSplineCurve::rational(
287 KnotVector::new(knots, 2)?,
288 control,
289 )?))
290}
291
292pub(crate) fn conic_pieces(p: [Point; 3], w: f64) -> (Vec<Point>, f64, Vec<f64>) {
299 let mut pieces = vec![p];
300 let mut weight = w;
301 for _ in 0..8 {
302 if weight >= WEIGHT_RANGE.0 && weight <= WEIGHT_RANGE.1 {
303 break;
304 }
305 pieces = pieces
306 .into_iter()
307 .flat_map(|[p0, a, p2]| {
308 let near =
309 Point::from_vector((p0.to_vector() + a.to_vector() * weight) / (1.0 + weight));
310 let far =
311 Point::from_vector((a.to_vector() * weight + p2.to_vector()) / (1.0 + weight));
312 let middle = near.midpoint(far);
313 [[p0, near, middle], [middle, far, p2]]
314 })
315 .collect();
316 weight = ((1.0 + weight) * 0.5).sqrt();
317 }
318 let n = pieces.len();
319 let mut knots = vec![0.0; 3];
320 let mut control = Vec::with_capacity(2 * n + 1);
321 for (i, [p0, a, _]) in pieces.iter().enumerate() {
322 control.push(*p0);
323 control.push(*a);
324 if i + 1 < n {
325 #[allow(clippy::cast_precision_loss, reason = "at most 256 pieces")]
326 let k = (i + 1) as f64 / n as f64;
327 knots.extend([k, k]);
328 }
329 }
330 control.push(pieces[n - 1][2]);
331 knots.extend([1.0; 3]);
332 (control, weight, knots)
333}
334
335#[allow(
361 clippy::too_many_arguments,
362 reason = "the chamfer's distances' own, and the profile"
363)]
364pub fn fillet_edge_distances(
365 model: &mut Model,
366 solid: &Shape,
367 edge: &Shape,
368 face: &Shape,
369 on_face: f64,
370 on_other: f64,
371 profile: RoundProfile,
372 tol: Tolerances,
373) -> OgeomResult<Built> {
374 for d in [on_face, on_other] {
375 if !d.is_finite() || d <= tol.confusion() {
376 ogeom_bail!(
377 Construction,
378 "a round set back {d} from its edge rounds nothing"
379 );
380 }
381 }
382 if let RoundProfile::Conic(_) = profile {
383 profile.weight(0.0)?;
384 }
385 if profile == RoundProfile::Elliptical
386 && on_face == on_other
387 && constant_dihedral(model, solid, edge, tol)?
388 {
389 let radius = equal_round(model, solid, edge, face, on_face, tol)?;
391 return crate::fillet_edge(model, solid, edge, radius, tol);
392 }
393 ogeom_algo::on_own_nodes(
394 model,
395 solid,
396 &[edge.clone(), face.clone()],
397 |model, solid, parts| {
398 fillet_edge_distances_own(
399 model, solid, &parts[0], &parts[1], on_face, on_other, profile, tol,
400 )
401 },
402 )
403}
404
405#[allow(clippy::too_many_arguments, reason = "fillet_edge_distances' own")]
407fn fillet_edge_distances_own(
408 model: &mut Model,
409 solid: &Shape,
410 edge: &Shape,
411 face: &Shape,
412 on_face: f64,
413 on_other: f64,
414 profile: RoundProfile,
415 tol: Tolerances,
416) -> OgeomResult<Built> {
417 if let Some(rim) = rim_conic(model, solid, edge, face, on_face, on_other, profile, tol)? {
418 let mut built = if rim.whole {
419 rim_round(model, solid, edge, &rim, tol)?
420 } else {
421 rim_fits(model, solid, edge, &rim, tol)?;
422 let size = rim.size();
423 crate::fillet::revolved_arc_fillet(
424 model,
425 solid,
426 edge,
427 &rim.circle,
428 size,
429 Some(&rim),
430 None,
431 tol,
432 )?
433 };
434 crate::support::credit_new_faces(model, solid, edge, &mut built)?;
435 return Ok(built);
436 }
437 if !straight_between_planes(model, solid, edge, tol)? {
438 let weight = match profile {
441 RoundProfile::Elliptical => crate::marched::ConicWeight::Elliptical,
442 RoundProfile::Conic(_) => crate::marched::ConicWeight::Fixed(profile.weight(0.0)?),
443 };
444 let spec = crate::Chamfer::Distances {
445 face: face.clone(),
446 on_face,
447 on_other,
448 };
449 return crate::chamfer_marched::marched_bevel_with(
450 model,
451 solid,
452 edge,
453 &spec,
454 &[],
455 crate::marched::Chord::Conic(weight),
456 tol,
457 );
458 }
459 let Some(section) =
460 straight_section(model, solid, edge, face, on_face, on_other, profile, tol)?
461 else {
462 ogeom_bail!(
463 Construction,
464 "the circular round asked as an asymmetric one"
465 );
466 };
467 contacts_fit(model, solid, edge, section, tol)?;
468 let mut built = crate::fillet::planar_fillet(model, solid, edge, section, None, tol)?;
469 crate::support::credit_new_faces(model, solid, edge, &mut built)?;
470 Ok(built)
471}
472
473#[allow(clippy::too_many_arguments, reason = "the request's own")]
478pub(crate) fn straight_section(
479 model: &Model,
480 solid: &Shape,
481 edge: &Shape,
482 face: &Shape,
483 on_face: f64,
484 on_other: f64,
485 profile: RoundProfile,
486 tol: Tolerances,
487) -> OgeomResult<Option<Section>> {
488 let (seat, named) = straight_seat(model, solid, edge, face, tol)?;
489 if profile == RoundProfile::Elliptical && on_face == on_other {
490 return Ok(None);
491 }
492 let mut d = [0.0; 2];
493 d[named] = on_face;
494 d[1 - named] = on_other;
495 let f = legs(&seat, tol)?;
496 let w = profile.weight(f[0].dot(f[1]).clamp(-1.0, 1.0))?;
497 Ok(Some(Section::Conic { d, w }))
498}
499
500pub(crate) fn straight_seat(
504 model: &Model,
505 solid: &Shape,
506 edge: &Shape,
507 face: &Shape,
508 tol: Tolerances,
509) -> OgeomResult<(Seat, usize)> {
510 let (curve, _) = crate::support::edge_curve(model, edge, tol)?;
511 if !matches!(curve, Curve::Line(_)) || !crate::support::hosts_planar(model, solid, edge, tol)? {
512 ogeom_bail!(
513 Construction,
514 "an asymmetric round is built on a straight edge between two planes or a \
515 rim where a planar cap meets a coaxial cylinder or cone; this edge is \
516 neither; see docs/CAPABILITIES.md, fillet.asymmetric-rounds"
517 );
518 }
519 let seat = crate::support::planar_seat(model, solid, edge, tol)?;
520 let named = if seat.faces[0].is_same(face) {
521 0
522 } else if seat.faces[1].is_same(face) {
523 1
524 } else {
525 ogeom_bail!(
526 Construction,
527 "the face an asymmetric round's first distance runs along is not one of the \
528 edge's two faces"
529 );
530 };
531 Ok((seat, named))
532}
533
534pub(crate) fn equal_round(
537 model: &Model,
538 solid: &Shape,
539 edge: &Shape,
540 face: &Shape,
541 distance: f64,
542 tol: Tolerances,
543) -> OgeomResult<f64> {
544 if !distance.is_finite() || distance <= tol.confusion() {
545 ogeom_bail!(
546 Construction,
547 "a round set back {distance} from its edge rounds nothing"
548 );
549 }
550 let profile = RoundProfile::Elliptical;
551 if let Some(rim) = rim_conic(model, solid, edge, face, distance, distance, profile, tol)? {
552 return Ok(rim.equal_radius(distance));
553 }
554 straight_section(model, solid, edge, face, distance, distance, profile, tol)?;
555 equal_radius(model, solid, edge, distance, tol)
556}
557
558fn equal_radius(
561 model: &Model,
562 solid: &Shape,
563 edge: &Shape,
564 distance: f64,
565 tol: Tolerances,
566) -> OgeomResult<f64> {
567 let seat = crate::support::planar_seat(model, solid, edge, tol)?;
568 let f = legs(&seat, tol)?;
569 let cos = f[0].dot(f[1]).clamp(-1.0, 1.0);
570 let sin = f[0].cross(f[1]).magnitude();
571 if sin <= tol.angular() || 1.0 + cos <= tol.angular() {
572 ogeom_bail!(
573 Construction,
574 "the faces meet too sharply or too flat to round"
575 );
576 }
577 Ok(distance * sin / (1.0 + cos))
578}
579
580fn contacts_fit(
585 model: &Model,
586 solid: &Shape,
587 edge: &Shape,
588 section: Section,
589 tol: Tolerances,
590) -> OgeomResult<()> {
591 let Section::Conic { d, .. } = section else {
592 return Ok(());
593 };
594 let seat = crate::support::planar_seat(model, solid, edge, tol)?;
595 let f = legs(&seat, tol)?;
596 for fraction in [0.25, 0.5, 0.75] {
597 let p = seat.start + (seat.end - seat.start) * fraction;
598 for i in 0..2 {
599 let contact = p + f[i] * d[i];
600 if !crate::support::on_face_or_boundary(model, solid, &seat.faces[i], contact, tol)? {
601 ogeom_bail!(
602 Construction,
603 "an asymmetric round sets back {} along a face of the edge through \
604 {p:?}, past that face's side; see docs/CAPABILITIES.md, \
605 fillet.asymmetric-rounds",
606 d[i]
607 );
608 }
609 }
610 }
611 Ok(())
612}
613
614pub(crate) struct Asymmetric {
616 pub edge: Shape,
618 pub face: Shape,
620 pub on_face: f64,
622 pub on_other: f64,
624 pub profile: RoundProfile,
626}
627
628pub(crate) fn after_symmetric(
635 model: &mut Model,
636 others: &[Shape],
637 mut asymmetric: Vec<Asymmetric>,
638 symmetric: impl FnOnce(&mut Model) -> OgeomResult<Built>,
639 tol: Tolerances,
640) -> OgeomResult<Built> {
641 asymmetric.sort_by_key(|a| a.edge.node());
642 let ends = |model: &Model, edge: &Shape| -> OgeomResult<Vec<Shape>> {
643 Ok(match ogeom_algo::edge_vertices(model, edge)? {
644 Some((a, b)) if a.is_same(&b) => vec![a],
645 Some((a, b)) => vec![a, b],
646 None => Vec::new(),
647 })
648 };
649 let n = asymmetric.len();
650 let mut chain: Vec<usize> = (0..n).collect();
652 let root = |chain: &[usize], mut i: usize| {
653 while chain[i] != i {
654 i = chain[i];
655 }
656 i
657 };
658 let mut meeting: std::collections::BTreeMap<ogeom_topo::TShapeId, usize> =
659 std::collections::BTreeMap::new();
660 for (i, a) in asymmetric.iter().enumerate() {
661 let own = ends(model, &a.edge)?;
662 for v in &own {
663 *meeting.entry(v.node()).or_default() += 1;
664 }
665 for other in others {
666 if ends(model, other)?
667 .iter()
668 .any(|v| own.iter().any(|w| w.is_same(v)))
669 {
670 ogeom_bail!(
671 Construction,
672 "an asymmetric round meets another blended edge at a vertex, and the \
673 corner where they meet is not closed yet; see docs/CAPABILITIES.md, \
674 fillet.asymmetric-rounds"
675 );
676 }
677 }
678 for (j, b) in asymmetric.iter().enumerate().skip(i + 1) {
679 let theirs = ends(model, &b.edge)?;
680 let Some(v) = own.iter().find(|v| theirs.iter().any(|w| w.is_same(v))) else {
681 continue;
682 };
683 if !continues(model, a, b, v, tol)? {
684 ogeom_bail!(
685 Construction,
686 "two asymmetric rounds meet at a vertex other than as one tangent chain \
687 naming one face with the same distances and section, and the corner \
688 where they meet is not closed yet; see docs/CAPABILITIES.md, \
689 fillet.asymmetric-rounds"
690 );
691 }
692 let (ri, rj) = (root(&chain, i), root(&chain, j));
693 chain[ri.max(rj)] = ri.min(rj);
694 }
695 }
696 if meeting.values().any(|&k| k > 2) {
697 ogeom_bail!(
698 Construction,
699 "three asymmetric rounds meet at a vertex, and the corner where they meet is \
700 not closed yet; see docs/CAPABILITIES.md, fillet.asymmetric-rounds"
701 );
702 }
703 let mut built = symmetric(model)?;
704 let mut groups: std::collections::BTreeMap<usize, Vec<usize>> =
705 std::collections::BTreeMap::new();
706 for i in 0..n {
707 groups.entry(root(&chain, i)).or_default().push(i);
708 }
709 for members in groups.into_values() {
710 let mut traced = Vec::with_capacity(members.len());
711 for &i in &members {
712 traced.push(trace_request(model, &built, &asymmetric[i], tol)?);
713 }
714 if let [(edge, face)] = traced.as_slice() {
715 let a = &asymmetric[members[0]];
716 let next = fillet_edge_distances(
717 model,
718 &built.shape,
719 edge,
720 face,
721 a.on_face,
722 a.on_other,
723 a.profile,
724 tol,
725 )?;
726 built = Built::new(next.shape.clone(), built.history.then(&next.history));
727 continue;
728 }
729 let solid = built.shape.clone();
733 let (made, wedges) = crate::support::collecting_wedges(|| -> OgeomResult<()> {
734 for (&i, (edge, face)) in members.iter().zip(&traced) {
735 let a = &asymmetric[i];
736 fillet_edge_distances_own(
737 model, &solid, edge, face, a.on_face, a.on_other, a.profile, tol,
738 )?;
739 }
740 Ok(())
741 });
742 made?;
743 let set_aside = wedges
744 .into_iter()
745 .filter_map(|w| w.edges.first().cloned().map(|e| (e, w)))
746 .collect();
747 built = crate::fillet::apply_set_aside(model, built, set_aside, tol)?;
748 }
749 Ok(built)
750}
751
752fn continues(
756 model: &Model,
757 a: &Asymmetric,
758 b: &Asymmetric,
759 vertex: &Shape,
760 tol: Tolerances,
761) -> OgeomResult<bool> {
762 if !a.face.is_same(&b.face)
763 || a.on_face != b.on_face
764 || a.on_other != b.on_other
765 || a.profile != b.profile
766 {
767 return Ok(false);
768 }
769 let at = {
770 let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
771 return Ok(false);
772 };
773 vertex.transform(model.datums())?.apply(data.point)
774 };
775 let tangent = |edge: &Shape| -> OgeomResult<Vector> {
776 use ogeom_geom::Curve3d as _;
777 let (curve, (t0, t1)) = crate::support::edge_curve(model, edge, tol)?;
778 let t = if curve.point_at(t0, tol)?.distance(at) <= curve.point_at(t1, tol)?.distance(at) {
779 t0
780 } else {
781 t1
782 };
783 let d = curve.d1_at(t, tol)?;
784 Ok(d / d.magnitude())
785 };
786 Ok(tangent(&a.edge)?.cross(tangent(&b.edge)?).magnitude() <= tol.angular() * 10.0)
787}
788
789fn trace_request(
792 model: &Model,
793 built: &Built,
794 a: &Asymmetric,
795 tol: Tolerances,
796) -> OgeomResult<(Shape, Shape)> {
797 let images = built.history.trace(&a.edge);
798 let one_rim = !images.is_empty() && {
801 let circle = |e: &Shape| match crate::support::edge_curve(model, e, tol) {
802 Ok((Curve::Circle(c), _)) => Some(c.circle()),
803 _ => None,
804 };
805 match circle(&a.edge) {
806 Some(rim) => images.iter().all(|e| {
807 circle(e).is_some_and(|c| {
808 c.centre().distance(rim.centre()) <= tol.confusion()
809 && (c.radius() - rim.radius()).abs() <= tol.confusion()
810 })
811 }),
812 None => false,
813 }
814 };
815 let edge = match images {
816 [edge] => edge.clone(),
817 [edge, ..] if one_rim => edge.clone(),
818 _ => ogeom_bail!(
819 Construction,
820 "an asymmetric round's edge does not come through the request's other blends \
821 as one edge"
822 ),
823 };
824 let beside = faces_beside(model, &built.shape, &edge, tol)?;
827 let images = built.history.trace(&a.face);
828 let Some(face) = beside
829 .iter()
830 .find(|f| images.iter().any(|g| g.is_same(f)))
831 .cloned()
832 else {
833 ogeom_bail!(
834 Construction,
835 "the face an asymmetric round's first distance runs along is not one of the \
836 edge's two faces"
837 );
838 };
839 Ok((edge, face))
840}
841
842pub(crate) struct RimConic {
846 seat: crate::support::RevolvedSeat,
848 circle: ogeom_geom::CircleCurve,
850 whole: bool,
852 wall_depth: f64,
854 cap_rho: f64,
856 points: [Point; 3],
859 w: f64,
861}
862
863impl RimConic {
864 pub(crate) fn size(&self) -> f64 {
866 let [wall, rim, cap] = self.points;
867 wall.distance(rim).max(cap.distance(rim))
868 }
869
870 pub(crate) fn contacts(
874 &self,
875 seat: &crate::support::RevolvedSeat,
876 ) -> crate::support::RevolvedBall {
877 let wall_height = -seat.tau * self.wall_depth;
878 crate::support::RevolvedBall {
879 rho: self.cap_rho,
880 height: 0.0,
881 wall_rho: seat.radius_at(wall_height),
882 wall_height,
883 sweep: 0.0,
884 }
885 }
886
887 pub(crate) fn meridian(
890 &self,
891 seat: &crate::support::RevolvedSeat,
892 theta: f64,
893 tol: Tolerances,
894 ) -> OgeomResult<Curve> {
895 let turn = ogeom_math::Transform::rotation(
896 ogeom_math::Axis {
897 location: seat.centre,
898 direction: Direction::new(seat.up, tol)?,
899 },
900 theta,
901 );
902 conic_arc(self.points.map(|p| turn.apply(p)), self.w, tol)
903 }
904
905 pub(crate) fn band_sector(
911 &self,
912 model: &mut Model,
913 seat: &crate::support::RevolvedSeat,
914 (theta0, sweep): (f64, f64),
915 [wall, cap, start, end]: [&Shape; 4],
916 tol: Tolerances,
917 ) -> OgeomResult<Shape> {
918 use ogeom_geom::Surface as _;
919 let axis = ogeom_math::Axis {
920 location: seat.centre,
921 direction: Direction::new(seat.up, tol)?,
922 };
923 let profile = self.meridian(seat, theta0, tol)?;
924 let surface: ogeom_geom::SurfaceGeometry =
925 ogeom_geom::RevolutionSurface::over(profile, axis, (0.0, sweep))?.into();
926 let surface_id = model.geometry_mut().add_surface(surface.clone());
927 let line = |from: (f64, f64),
928 along: (f64, f64),
929 to: f64|
930 -> OgeomResult<ogeom_geom::PlanarCurve> {
931 Ok(ogeom_geom::Line2d::over(
932 ogeom_math::Axis2::new(
933 ogeom_math::Point2::new(from.0, from.1),
934 ogeom_math::Direction2::new(ogeom_math::Vector2::new(along.0, along.1), tol)?,
935 ),
936 0.0,
937 to,
938 )?
939 .into())
940 };
941 for (edge, pcurve, range) in [
942 (wall, line((0.0, 0.0), (1.0, 0.0), sweep)?, (0.0, sweep)),
943 (cap, line((0.0, 1.0), (1.0, 0.0), sweep)?, (0.0, sweep)),
944 (start, line((0.0, 0.0), (0.0, 1.0), 1.0)?, (0.0, 1.0)),
945 (end, line((sweep, 0.0), (0.0, 1.0), 1.0)?, (0.0, 1.0)),
946 ] {
947 ogeom_algo::attach_pcurve(
948 model,
949 edge,
950 pcurve,
951 surface_id,
952 ogeom_topo::Location::identity(),
953 range,
954 )?;
955 }
956 let wire = ogeom_algo::make_wire(
957 model,
958 &[wall.clone(), end.clone(), cap.reversed(), start.reversed()],
959 tol,
960 )?
961 .shape;
962 let face =
963 ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
964 let middle = surface.point_at(sweep * 0.5, 0.5, tol)?;
965 let (du, dv) = surface.d1_at(sweep * 0.5, 0.5, tol)?;
966 let rim = ogeom_math::Transform::rotation(axis, theta0 + sweep * 0.5).apply(self.points[1]);
967 Ok(if du.cross(dv).dot(middle - rim) >= 0.0 {
968 face
969 } else {
970 face.reversed()
971 })
972 }
973 fn equal_radius(&self, distance: f64) -> f64 {
978 let [wall, rim, cap] = self.points;
979 let (a, b) = (wall - rim, cap - rim);
980 let (a, b) = (a / a.magnitude(), b / b.magnitude());
981 let cos = a.dot(b).clamp(-1.0, 1.0);
982 let sin = a.cross(b).magnitude();
983 distance * sin / (1.0 + cos)
984 }
985}
986
987#[allow(clippy::too_many_arguments, reason = "the request's own")]
992fn rim_conic(
993 model: &Model,
994 solid: &Shape,
995 edge: &Shape,
996 face: &Shape,
997 on_face: f64,
998 on_other: f64,
999 profile: RoundProfile,
1000 tol: Tolerances,
1001) -> OgeomResult<Option<RimConic>> {
1002 let (curve, range) = crate::support::edge_curve(model, edge, tol)?;
1003 let Curve::Circle(circle) = curve else {
1004 return Ok(None);
1005 };
1006 let seat = match crate::support::revolved_seat(model, solid, edge, &circle, tol) {
1007 Ok(seat) => seat,
1008 Err(ogeom_core::OgeomError::Construction(_)) => return Ok(None),
1011 Err(e) => return Err(e),
1012 };
1013 let closed = ogeom_algo::edge_vertices(model, edge)?.is_some_and(|(a, b)| a.is_same(&b));
1014 let whole = closed || crate::support::runs_whole_turn(model, &seat, &circle, range, tol)?;
1015 let (on_cap, on_wall) = if seat.cap_face.is_same(face) {
1016 (on_face, on_other)
1017 } else if seat.wall_face.is_same(face) {
1018 (on_other, on_face)
1019 } else {
1020 ogeom_bail!(
1021 Construction,
1022 "the face an asymmetric round's first distance runs along is not one of the \
1023 edge's two faces"
1024 );
1025 };
1026 let wall_depth = on_wall / seat.slope.hypot(1.0);
1029 let wall_height = -seat.tau * wall_depth;
1030 let wall_rho = seat.radius_at(wall_height);
1031 let cap_rho = seat.radius - seat.sigma * seat.tau * on_cap;
1032 if wall_rho <= tol.confusion() || cap_rho <= tol.confusion() {
1033 ogeom_bail!(
1034 Construction,
1035 "an asymmetric round of {on_cap} on the cap and {on_wall} on the wall reaches \
1036 the axis of a rim of radius {}",
1037 seat.radius
1038 );
1039 }
1040 let x = seat.x_ref.vector();
1041 let points = [
1042 seat.centre + seat.up * wall_height + x * wall_rho,
1043 seat.centre + x * seat.radius,
1044 seat.centre + x * cap_rho,
1045 ];
1046 let (a, b) = (points[0] - points[1], points[2] - points[1]);
1047 let (a, b) = (a / a.magnitude(), b / b.magnitude());
1048 let w = profile.weight(a.dot(b).clamp(-1.0, 1.0))?;
1049 Ok(Some(RimConic {
1050 seat,
1051 circle,
1052 whole,
1053 wall_depth,
1054 cap_rho,
1055 points,
1056 w,
1057 }))
1058}
1059
1060fn rim_round(
1064 model: &mut Model,
1065 solid: &Shape,
1066 edge: &Shape,
1067 rim: &RimConic,
1068 tol: Tolerances,
1069) -> OgeomResult<Built> {
1070 use ogeom_geom::{Curve3d as _, Surface as _};
1071 let seat = &rim.seat;
1072 rim_fits(model, solid, edge, rim, tol)?;
1073 let flanks = crate::support::revolved_flanks(model, seat, rim.wall_depth, rim.cap_rho, tol)?;
1074 let profile = conic_arc(rim.points, rim.w, tol)?;
1075 let middle = profile.point_at(0.5, tol)?;
1076 let axis = ogeom_math::Axis {
1077 location: seat.centre,
1078 direction: Direction::new(seat.up, tol)?,
1079 };
1080 let surface: ogeom_geom::SurfaceGeometry =
1081 ogeom_geom::RevolutionSurface::new(profile, axis, core::f64::consts::TAU)?.into();
1082 let band = ogeom_algo::make_profile_revolution_band(
1083 model,
1084 &surface,
1085 &flanks.wall_ring,
1086 &flanks.cap_ring,
1087 tol,
1088 )?;
1089 let (du, dv) = surface.d1_at(0.0, 0.5, tol)?;
1092 let band = if du.cross(dv).dot(middle - rim.points[1]) < 0.0 {
1093 band.reversed()
1094 } else {
1095 band
1096 };
1097 let faces = [flanks.wall_band, flanks.annulus, band];
1098 crate::support::apply_wedge(model, solid, Some(edge), &faces, seat.additive(), tol)
1099}
1100
1101fn faces_beside(
1104 model: &Model,
1105 solid: &Shape,
1106 edge: &Shape,
1107 tol: Tolerances,
1108) -> OgeomResult<[Shape; 2]> {
1109 let (curve, _) = crate::support::edge_curve(model, edge, tol)?;
1110 if let Curve::Circle(circle) = curve {
1111 let seat = crate::support::revolved_seat(model, solid, edge, &circle, tol)?;
1112 return Ok([seat.cap_face, seat.wall_face]);
1113 }
1114 if straight_between_planes(model, solid, edge, tol)? {
1115 return Ok(crate::support::planar_seat(model, solid, edge, tol)?.faces);
1116 }
1117 let mut found: Vec<Shape> = Vec::new();
1118 for f in ogeom_topo::explore_unique(model, solid, ogeom_topo::ShapeType::Face)? {
1119 let holds = ogeom_topo::explore(
1120 model,
1121 &f,
1122 ogeom_topo::Filter::OfType(ogeom_topo::ShapeType::Edge),
1123 )?
1124 .iter()
1125 .any(|e| crate::support::same_occurrence(model, e, edge, tol));
1126 if holds {
1127 found.push(f);
1128 }
1129 }
1130 let [a, b] = <[Shape; 2]>::try_from(found).map_err(|_| {
1131 ogeom_core::ogeom_err!(Construction, "an asymmetric round's edge has not two faces")
1132 })?;
1133 Ok([a, b])
1134}
1135
1136fn straight_between_planes(
1138 model: &Model,
1139 solid: &Shape,
1140 edge: &Shape,
1141 tol: Tolerances,
1142) -> OgeomResult<bool> {
1143 let (curve, _) = crate::support::edge_curve(model, edge, tol)?;
1144 Ok(matches!(curve, Curve::Line(_)) && crate::support::hosts_planar(model, solid, edge, tol)?)
1145}
1146
1147fn constant_dihedral(
1151 model: &Model,
1152 solid: &Shape,
1153 edge: &Shape,
1154 tol: Tolerances,
1155) -> OgeomResult<bool> {
1156 if straight_between_planes(model, solid, edge, tol)? {
1157 return Ok(true);
1158 }
1159 let (curve, _) = crate::support::edge_curve(model, edge, tol)?;
1160 let Curve::Circle(circle) = curve else {
1161 return Ok(false);
1162 };
1163 Ok(crate::support::revolved_seat(model, solid, edge, &circle, tol).is_ok())
1164}
1165
1166fn rim_fits(
1169 model: &Model,
1170 solid: &Shape,
1171 edge: &Shape,
1172 rim: &RimConic,
1173 tol: Tolerances,
1174) -> OgeomResult<()> {
1175 use ogeom_geom::Curve3d as _;
1176 let seat = &rim.seat;
1177 let (curve, (t0, t1)) = crate::support::edge_curve(model, edge, tol)?;
1178 for fraction in [0.25, 0.5, 0.75] {
1179 let on_rim = curve.point_at(t0 + (t1 - t0) * fraction, tol)?;
1180 let radial = {
1181 let off = on_rim - seat.centre;
1182 let r = off - seat.up * off.dot(seat.up);
1183 r / r.magnitude()
1184 };
1185 let across = |p: Point| {
1186 let off = p - seat.centre;
1187 let height = off.dot(seat.up);
1188 let r = (off - seat.up * height).magnitude();
1189 seat.centre + seat.up * height + radial * r
1190 };
1191 for (face, p) in [
1192 (&seat.wall_face, rim.points[0]),
1193 (&seat.cap_face, rim.points[2]),
1194 ] {
1195 if !crate::support::on_face_or_boundary(model, solid, face, across(p), tol)? {
1196 ogeom_bail!(
1197 Construction,
1198 "an asymmetric round on a rim sets back past the side of a face beside \
1199 it; see docs/CAPABILITIES.md, fillet.asymmetric-rounds"
1200 );
1201 }
1202 }
1203 }
1204 Ok(())
1205}
1206
1207pub(crate) struct AsymmetricCorners {
1211 pub corners: Vec<(
1213 Shape,
1214 crate::corner::CornerFrame,
1215 Vec<f64>,
1216 Vec<crate::setback::CornerEnd>,
1217 )>,
1218 pub edges: Vec<Shape>,
1220}
1221
1222pub(crate) fn asymmetric_corners(
1228 model: &Model,
1229 solid: &Shape,
1230 rounds: &[(Shape, f64)],
1231 bevels: &[(Shape, crate::Chamfer)],
1232 asymmetric: &[Asymmetric],
1233 tol: Tolerances,
1234) -> OgeomResult<AsymmetricCorners> {
1235 use crate::setback::CornerEnd;
1236 let ends = |edge: &Shape| -> OgeomResult<Vec<Shape>> {
1237 Ok(match ogeom_algo::edge_vertices(model, edge)? {
1238 Some((a, b)) if a.is_same(&b) => vec![a],
1239 Some((a, b)) => vec![a, b],
1240 None => Vec::new(),
1241 })
1242 };
1243 let mut all: Vec<(Shape, usize, usize)> = Vec::new();
1246 all.extend(
1247 rounds
1248 .iter()
1249 .enumerate()
1250 .map(|(i, (e, _))| (e.clone(), 0, i)),
1251 );
1252 all.extend(
1253 bevels
1254 .iter()
1255 .enumerate()
1256 .map(|(i, (e, _))| (e.clone(), 1, i)),
1257 );
1258 all.extend(
1259 asymmetric
1260 .iter()
1261 .enumerate()
1262 .map(|(i, a)| (a.edge.clone(), 2, i)),
1263 );
1264 let mut at: std::collections::BTreeMap<ogeom_topo::TShapeId, (Shape, Vec<usize>)> =
1265 std::collections::BTreeMap::new();
1266 for (k, (edge, _, _)) in all.iter().enumerate() {
1267 for v in ends(edge)? {
1268 at.entry(v.node())
1269 .or_insert_with(|| (v.clone(), Vec::new()))
1270 .1
1271 .push(k);
1272 }
1273 }
1274 let solid_edges = ogeom_topo::explore_unique(model, solid, ogeom_topo::ShapeType::Edge)?;
1275 let mut corners = Vec::new();
1276 let mut edges: Vec<Shape> = Vec::new();
1277 for (vertex, here) in at.into_values() {
1278 let asym_here: Vec<usize> = here.iter().copied().filter(|&k| all[k].1 == 2).collect();
1279 if asym_here.is_empty() || here.len() < 2 {
1280 continue;
1281 }
1282 if here.len() == 2 && asym_here.len() == 2 {
1284 let (a, b) = (&asymmetric[all[here[0]].2], &asymmetric[all[here[1]].2]);
1285 if continues(model, a, b, &vertex, tol)? {
1286 continue;
1287 }
1288 }
1289 let mut meeting = 0;
1290 for e in &solid_edges {
1291 if ends(e)?.iter().any(|v| v.is_same(&vertex)) {
1292 meeting += 1;
1293 }
1294 }
1295 if meeting != 3 || here.len() != 3 {
1296 ogeom_bail!(
1297 Construction,
1298 "an asymmetric round meets another blended edge at a vertex, and such a \
1299 corner is closed only where three edges meet and all three are blended; \
1300 see docs/CAPABILITIES.md, fillet.asymmetric-rounds"
1301 );
1302 }
1303 let largest = here
1304 .iter()
1305 .map(|&k| match all[k].1 {
1306 0 => rounds[all[k].2].1,
1307 1 => 0.0,
1308 _ => asymmetric[all[k].2]
1309 .on_face
1310 .max(asymmetric[all[k].2].on_other),
1311 })
1312 .fold(0.0, f64::max)
1313 .max(tol.confusion() * 10.0);
1314 let Ok(frame) = crate::corner::corner_frame(model, solid, &vertex, largest, tol) else {
1315 ogeom_bail!(
1316 Construction,
1317 "an asymmetric round meets other blended edges at a vertex that is not a \
1318 convex corner of planes, and the corner is not closed there; see \
1319 docs/CAPABILITIES.md, fillet.asymmetric-rounds"
1320 );
1321 };
1322 let n = frame.d.len();
1323 let point = |v: &Shape| -> OgeomResult<Point> {
1324 let Some(data) = model.node(v).and_then(|n| n.data().as_vertex()) else {
1325 ogeom_bail!(Dangling, "a vertex is not in this model");
1326 };
1327 Ok(v.transform(model.datums())?.apply(data.point))
1328 };
1329 let mut widths = Vec::with_capacity(n);
1330 let mut kinds = Vec::with_capacity(n);
1331 for k in 0..n {
1332 let (ma, mb) = (frame.m[frame.ring[k]], frame.m[frame.ring[(k + 1) % n]]);
1333 let dk = frame.d[k].vector();
1334 let mut found = None;
1335 for &j in &here {
1336 let edge = &all[j].0;
1337 let Some((a, z)) = ogeom_algo::edge_vertices(model, edge)? else {
1338 continue;
1339 };
1340 let far = if a.is_same(&vertex) { z } else { a };
1341 let p = point(&far)? - frame.corner;
1342 let length = p.magnitude();
1343 if length <= tol.confusion() || p.dot(dk) / length < 1.0 - tol.angular().sqrt() {
1344 continue;
1345 }
1346 let on = |normals: [Vector; 2], d: [f64; 2]| -> OgeomResult<(f64, f64)> {
1348 let pick = |m: Vector| {
1349 (0..2).find(|&i| normals[i].cross(m).magnitude() <= tol.angular() * 10.0)
1350 };
1351 let (Some(ia), Some(ib)) = (pick(ma), pick(mb)) else {
1352 ogeom_bail!(Construction, "a band at a corner does not lie on its faces");
1353 };
1354 Ok((d[ia], d[ib]))
1355 };
1356 found = Some(match all[j].1 {
1357 0 => {
1358 let cos = -ma.dot(mb);
1359 let sin = (1.0 - cos * cos).max(0.0).sqrt();
1360 let w = rounds[all[j].2].1 * (1.0 + cos) / sin;
1361 ((w, w), CornerEnd::Round)
1362 }
1363 1 => {
1364 let crate::chamfer::Legs::Straight(seat, d) =
1365 crate::chamfer::legs(model, solid, edge, &bevels[all[j].2].1, tol)?
1366 else {
1367 ogeom_bail!(
1368 Construction,
1369 "a corner with an asymmetric round takes bevels on straight \
1370 edges between planes"
1371 );
1372 };
1373 (on(seat.normals, d)?, CornerEnd::Bevel)
1374 }
1375 _ => {
1376 let a = &asymmetric[all[j].2];
1377 let Some(Section::Conic { d, .. }) = straight_section(
1378 model, solid, edge, &a.face, a.on_face, a.on_other, a.profile, tol,
1379 )?
1380 else {
1381 ogeom_bail!(Construction, "an asymmetric round asked as a round");
1382 };
1383 let seat = crate::support::planar_seat(model, solid, edge, tol)?;
1384 (on(seat.normals, d)?, CornerEnd::Round)
1385 }
1386 });
1387 }
1388 let Some((w, kind)) = found else {
1389 ogeom_bail!(
1390 Construction,
1391 "an edge at a corner of asymmetric rounds is not one of the request's, or \
1392 is curved"
1393 );
1394 };
1395 widths.push(w);
1396 kinds.push(kind);
1397 }
1398 let setback = crate::setback::least_setbacks_widths(&frame, &widths, tol)?;
1399 for &k in &here {
1400 if !edges.iter().any(|e| e.is_same(&all[k].0)) {
1401 edges.push(all[k].0.clone());
1402 }
1403 }
1404 corners.push((vertex, frame, setback, kinds));
1405 }
1406 for e in &edges {
1408 for v in ends(e)? {
1409 let corner_here = corners.iter().any(|(c, ..)| c.is_same(&v));
1410 let others_here = all.iter().any(|(o, _, _)| {
1411 !o.is_same(e) && ends(o).is_ok_and(|w| w.iter().any(|x| x.is_same(&v)))
1412 });
1413 if others_here && !corner_here {
1414 ogeom_bail!(
1415 Construction,
1416 "an edge at a corner of asymmetric rounds meets another blended edge at \
1417 its far end, and that meeting is not closed yet; see \
1418 docs/CAPABILITIES.md, fillet.asymmetric-rounds"
1419 );
1420 }
1421 }
1422 }
1423 Ok(AsymmetricCorners { corners, edges })
1424}
1425
1426pub(crate) type CornerEdges<'a> = (
1429 &'a [(Shape, f64)],
1430 &'a [(Shape, crate::Chamfer)],
1431 &'a [Asymmetric],
1432);
1433
1434pub(crate) fn close_asymmetric_corners(
1440 model: &mut Model,
1441 solid: &Shape,
1442 mut built: Built,
1443 corners: &AsymmetricCorners,
1444 (rounds, bevels, asymmetric): CornerEdges<'_>,
1445 tol: Tolerances,
1446) -> OgeomResult<Built> {
1447 if corners.corners.is_empty() {
1448 return Ok(built);
1449 }
1450 let traced_edge = |built: &Built, edge: &Shape| -> OgeomResult<Shape> {
1451 let [one] = built.history.trace(edge) else {
1452 ogeom_bail!(
1453 Construction,
1454 "an edge at a corner of asymmetric rounds does not come through the request's \
1455 other blends as one edge"
1456 );
1457 };
1458 Ok(one.clone())
1459 };
1460 let traced_face =
1461 |model: &Model, built: &Built, edge: &Shape, face: &Shape| -> OgeomResult<Shape> {
1462 let beside = faces_beside(model, &built.shape, edge, tol)?;
1463 let images = built.history.trace(face);
1464 beside
1465 .iter()
1466 .find(|f| images.iter().any(|g| g.is_same(f)))
1467 .cloned()
1468 .ok_or_else(|| {
1469 ogeom_core::ogeom_err!(
1470 Construction,
1471 "the face a blend at a corner names is not beside its edge"
1472 )
1473 })
1474 };
1475 let mut bevels: Vec<&(Shape, crate::Chamfer)> = bevels.iter().collect();
1476 bevels.sort_by_key(|(e, _)| e.node());
1477 for (edge, spec) in bevels {
1478 let now = traced_edge(&built, edge)?;
1479 let spec = match spec {
1480 crate::Chamfer::Symmetric(d) => crate::Chamfer::Symmetric(*d),
1481 crate::Chamfer::Distances {
1482 face,
1483 on_face,
1484 on_other,
1485 } => crate::Chamfer::Distances {
1486 face: traced_face(model, &built, &now, face)?,
1487 on_face: *on_face,
1488 on_other: *on_other,
1489 },
1490 crate::Chamfer::Angle {
1491 face,
1492 distance,
1493 angle,
1494 } => crate::Chamfer::Angle {
1495 face: traced_face(model, &built, &now, face)?,
1496 distance: *distance,
1497 angle: *angle,
1498 },
1499 };
1500 let next = crate::chamfer_edges_with(model, &built.shape, &[(now, spec)], tol)?;
1501 built = Built::new(next.shape.clone(), built.history.then(&next.history));
1502 }
1503 let mut rounds: Vec<&(Shape, f64)> = rounds.iter().collect();
1504 rounds.sort_by_key(|(e, _)| e.node());
1505 for (edge, r) in rounds {
1506 let now = traced_edge(&built, edge)?;
1507 let next = crate::fillet_edge(model, &built.shape, &now, *r, tol)?;
1508 built = Built::new(next.shape.clone(), built.history.then(&next.history));
1509 }
1510 let mut asymmetric: Vec<&Asymmetric> = asymmetric.iter().collect();
1511 asymmetric.sort_by_key(|a| a.edge.node());
1512 for a in asymmetric {
1513 let now = traced_edge(&built, &a.edge)?;
1514 let face = traced_face(model, &built, &now, &a.face)?;
1515 let next = fillet_edge_distances(
1516 model,
1517 &built.shape,
1518 &now,
1519 &face,
1520 a.on_face,
1521 a.on_other,
1522 a.profile,
1523 tol,
1524 )?;
1525 built = Built::new(next.shape.clone(), built.history.then(&next.history));
1526 }
1527 for (vertex, frame, setback, kinds) in &corners.corners {
1528 built = crate::setback::close_corner_ends(
1529 model,
1530 solid,
1531 built,
1532 vertex,
1533 frame,
1534 setback,
1535 kinds,
1536 ogeom_geom::Continuity::G1,
1537 tol,
1538 )?;
1539 }
1540 Ok(built)
1541}
1542
1543#[allow(
1565 clippy::too_many_arguments,
1566 reason = "fillet_edge_distances' own, and the bound"
1567)]
1568pub fn largest_fillet_distances(
1569 model: &Model,
1570 solid: &Shape,
1571 edge: &Shape,
1572 face: &Shape,
1573 on_face: f64,
1574 on_other: f64,
1575 up_to: f64,
1576 tol: Tolerances,
1577) -> OgeomResult<Option<f64>> {
1578 let conf = tol.confusion();
1579 if !up_to.is_finite() || up_to <= conf {
1580 ogeom_bail!(
1581 Construction,
1582 "a largest scale up to {up_to} asks for nothing"
1583 );
1584 }
1585 for d in [on_face, on_other] {
1586 if !d.is_finite() || d <= conf {
1587 ogeom_bail!(
1588 Construction,
1589 "a round set back {d} from its edge rounds nothing"
1590 );
1591 }
1592 }
1593 let profile = RoundProfile::Elliptical;
1594 let rim = rim_conic(model, solid, edge, face, on_face, on_other, profile, tol)?;
1595 if rim.is_none() {
1596 straight_seat(model, solid, edge, face, tol)?;
1597 }
1598 let mut largest = crate::fits::legs_largest(
1599 model,
1600 solid,
1601 edge,
1602 up_to,
1603 |f| if f.is_same(face) { on_face } else { on_other },
1604 tol,
1605 )?;
1606 if let Some(rim) = &rim {
1607 let seat = &rim.seat;
1610 if seat.sigma * seat.tau > 0.0 {
1611 let on_cap = if seat.cap_face.is_same(face) {
1612 on_face
1613 } else {
1614 on_other
1615 };
1616 largest = largest.min((seat.radius - conf) / on_cap);
1617 }
1618 }
1619 if largest * on_face.min(on_other) <= conf {
1620 return Ok(None);
1621 }
1622 let (a, b) = (largest * on_face, largest * on_other);
1623 let checked = crate::support::keeping_face_meshes(|| -> OgeomResult<()> {
1624 match rim_conic(model, solid, edge, face, a, b, profile, tol)? {
1625 Some(rim) => rim_fits(model, solid, edge, &rim, tol),
1626 None => {
1627 let (seat, named) = straight_seat(model, solid, edge, face, tol)?;
1628 let mut d = [0.0; 2];
1629 d[named] = a;
1630 d[1 - named] = b;
1631 let f = legs(&seat, tol)?;
1632 let w = profile.weight(f[0].dot(f[1]).clamp(-1.0, 1.0))?;
1633 contacts_fit(model, solid, edge, Section::Conic { d, w }, tol)
1634 }
1635 }
1636 });
1637 if let Err(refusal) = checked {
1638 ogeom_bail!(
1639 NotDone,
1640 "the largest scale read from the seat, {largest}, does not pass the round's \
1641 check: {refusal}"
1642 );
1643 }
1644 Ok(Some(largest))
1645}