1use crate::support::{
15 Seat, apply_wedge, edge_curve, face_from_edges, planar_face, planar_seat, revolved_flanks,
16 revolved_seat, segment_between,
17};
18use ogeom_algo::{Built, make_edge_between, make_revolution_band, make_vertex};
19use ogeom_core::{OgeomError, OgeomResult, Tolerances, ogeom_bail};
20use ogeom_geom::{
21 CircleCurve, Curve, CylinderSurface, PlaneSurface, SurfaceGeometry, TorusSurface,
22};
23use ogeom_math::{Circle, Cylinder, Direction, Frame, Plane, Point, Torus, Vector};
24use ogeom_topo::{Model, Shape};
25
26pub fn fillet_edge(
43 model: &mut Model,
44 solid: &Shape,
45 edge: &Shape,
46 radius: f64,
47 tol: Tolerances,
48) -> OgeomResult<Built> {
49 fillet_edge_meeting(model, solid, edge, radius, None, tol)
50}
51
52#[derive(Debug, Clone, Copy)]
56pub(crate) struct Mate {
57 pub ends: [(Point, Vector); 2],
59 pub settled: [bool; 2],
63}
64
65impl Mate {
66 pub(crate) fn settled_at(mates: &[Self], index: usize, at: Point, tol: Tolerances) -> bool {
76 let other = mates.iter().any(|mate| {
77 mate.ends
78 .iter()
79 .zip(mate.settled)
80 .any(|((p, _), settled)| settled && p.distance(at) <= tol.confusion() * 1e3)
81 });
82 let own = mates.get(index).is_some_and(|mate| {
83 let nearer = usize::from(mate.ends[1].0.distance(at) < mate.ends[0].0.distance(at));
84 mate.settled[nearer]
85 });
86 other || own
87 }
88}
89
90fn fillet_edge_meeting(
101 model: &mut Model,
102 solid: &Shape,
103 edge: &Shape,
104 radius: f64,
105 mates: Option<(usize, &[Mate])>,
106 tol: Tolerances,
107) -> OgeomResult<Built> {
108 if !radius.is_finite() || radius <= tol.confusion() {
109 ogeom_bail!(Construction, "a fillet of radius {radius} rounds nothing");
110 }
111 let (curve, crange) = edge_curve(model, edge, tol)?;
112 let closed = ogeom_algo::edge_vertices(model, edge)?.is_some_and(|(a, b)| a.is_same(&b));
113 match curve {
114 Curve::Line(_) if crate::support::hosts_planar(model, solid, edge, tol)? => {
115 planar_fillet(model, solid, edge, radius, mates, tol)
116 }
117 Curve::Line(_) => match crate::ruled::ruled_fillet(model, solid, edge, radius, mates, tol)?
118 {
119 Some(built) => Ok(built),
120 None => crate::marched::marched_fillet(model, solid, edge, radius, mates, tol),
121 },
122 Curve::Circle(c) => {
123 let seat = match revolved_seat(model, solid, edge, &c, tol) {
128 Ok(seat) => seat,
129 Err(OgeomError::Construction(_)) => {
130 return crate::marched::marched_fillet(model, solid, edge, radius, mates, tol);
131 }
132 Err(e) => return Err(e),
133 };
134 if closed {
135 return revolved_fillet(model, solid, edge, &c, radius, tol);
136 }
137 let frame = seat.frame_at(seat.centre, tol)?;
142 let angle_of = |t: f64| -> OgeomResult<f64> {
143 use ogeom_geom::Curve3d as _;
144 let local = frame.to_local(c.point_at(t, tol)?);
145 Ok(local.y.atan2(local.x))
146 };
147 let mid = f64::midpoint(
148 angle_of(crange.0)?,
149 angle_of(crange.0)?
150 + (angle_of(crange.1)? - angle_of(crange.0)?)
151 .rem_euclid(core::f64::consts::TAU),
152 );
153 let complement = mid + core::f64::consts::PI;
154 let dir = frame.x().vector() * complement.cos() + frame.y().vector() * complement.sin();
155 let eps = seat.radius * 1e-2;
156 let probe = seat.centre + dir * seat.radius - seat.up * (seat.tau * eps);
157 let deflection = ogeom_mesh::Deflection {
158 chord: eps * 0.1,
159 ..ogeom_mesh::Deflection::default()
160 };
161 let whole_turn =
162 ogeom_algo::classify_on_face(model, &seat.wall_face, probe, deflection, tol)?
163 == ogeom_algo::Containment::In;
164 if whole_turn {
165 revolved_fillet(model, solid, edge, &c, radius, tol)
166 } else {
167 revolved_arc_fillet(model, solid, edge, &c, radius, tol)
168 }
169 }
170 _ => crate::marched::marched_fillet(model, solid, edge, radius, mates, tol),
171 }
172}
173
174pub fn fillet_edges(
211 model: &mut Model,
212 solid: &Shape,
213 edges: &[Shape],
214 radius: f64,
215 tol: Tolerances,
216) -> OgeomResult<Built> {
217 if edges.is_empty() {
218 ogeom_bail!(Construction, "a chain of no edges rounds nothing");
219 }
220 for edge in edges {
223 crate::support::ball_fits(model, solid, edge, radius, tol)?;
224 }
225 use ogeom_geom::Curve3d as _;
226 let mut corners: Vec<(Shape, usize)> = Vec::new();
229 for edge in edges {
230 let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? else {
231 continue;
232 };
233 for v in [a, b] {
234 if let Some(slot) = corners.iter_mut().find(|(held, _)| held.is_same(&v)) {
235 slot.1 += 1;
236 } else {
237 corners.push((v, 1));
238 }
239 }
240 }
241 corners.retain(|(_, count)| *count >= 3);
242 let round = |model: &mut Model, built: Built, vertex: &Shape| -> OgeomResult<Built> {
245 match crate::corner::round_vertex(model, &built.shape, vertex, radius, tol) {
246 Ok(rounded) => Ok(Built {
247 shape: rounded.shape,
248 history: built.history.then(&rounded.history),
249 }),
250 Err(OgeomError::Construction(_)) => Ok(built),
251 Err(other) => Err(other),
252 }
253 };
254 let mut built: Option<Built> = None;
258 let mut rounded: Vec<Point> = Vec::new();
259 for (vertex, _) in &corners {
260 let start = built
261 .take()
262 .unwrap_or_else(|| Built::from_nothing(solid.clone()));
263 let before = start.shape.clone();
264 let after = round(model, start, vertex)?;
265 if !after.shape.is_same(&before)
266 && let Some(point) = model
267 .node(vertex)
268 .and_then(|n| n.data().as_vertex())
269 .map(|d| d.point)
270 {
271 rounded.push(vertex.transform(model.datums())?.apply(point));
272 }
273 built = Some(after);
274 }
275 let mates: Vec<Mate> = edges
276 .iter()
277 .map(|e| -> OgeomResult<Mate> {
278 let (curve, range) = edge_curve(model, e, tol)?;
279 let end_of = |t: f64, leaving: f64| -> OgeomResult<(Point, Vector)> {
280 let d = curve.d1_at(t, tol)?;
281 let m = d.magnitude();
282 let unit = if m > tol.confusion() {
283 d / m
284 } else {
285 Vector::ZERO
286 };
287 Ok((curve.point_at(t, tol)?, unit * leaving))
288 };
289 let ends = [end_of(range.0, 1.0)?, end_of(range.1, -1.0)?];
290 let settled = ends.map(|(p, _)| {
291 rounded
292 .iter()
293 .any(|q| q.distance(p) <= tol.confusion() * 1e3)
294 });
295 Ok(Mate { ends, settled })
296 })
297 .collect::<OgeomResult<_>>()?;
298 for (index, edge) in edges.iter().enumerate() {
299 let (current, targets): (Shape, Vec<Shape>) = match &built {
305 None => (solid.clone(), vec![edge.clone()]),
306 Some(b) => {
307 let traced = b.history.trace(edge);
308 if traced.is_empty() {
309 ogeom_bail!(
310 Construction,
311 "an earlier blend in the chain consumed this edge; \
312 the chain's members interfere"
313 );
314 }
315 let mut found = Vec::with_capacity(traced.len());
316 for one in traced {
317 found.extend(refind_edges(model, &b.shape, one, tol)?);
318 }
319 (b.shape.clone(), found)
320 }
321 };
322 let mut current = current;
323 for target in &targets {
324 let live = refind_edges(model, ¤t, target, tol)?;
327 let [target] = live.as_slice() else {
328 ogeom_bail!(
329 Construction,
330 "a blend in the chain split a piece of a later edge again; \
331 the chain's members interfere"
332 );
333 };
334 let mut step =
335 fillet_edge_meeting(model, ¤t, target, radius, Some((index, &mates)), tol)?;
336 if !target.is_same(edge) {
339 step.history.delete(edge);
340 }
341 current = step.shape.clone();
342 built = Some(match built {
343 None => step,
344 Some(prev) => Built {
345 shape: step.shape.clone(),
346 history: prev.history.then(&step.history),
347 },
348 });
349 }
350 }
351 Ok(built.unwrap_or_else(|| unreachable!()))
352}
353
354pub fn fillet_edge_variable(
368 model: &mut Model,
369 solid: &Shape,
370 edge: &Shape,
371 start_radius: f64,
372 end_radius: f64,
373 tol: Tolerances,
374) -> OgeomResult<Built> {
375 for radius in [start_radius, end_radius] {
376 if !radius.is_finite() || radius <= tol.confusion() {
377 ogeom_bail!(Construction, "a fillet of radius {radius} rounds nothing");
378 }
379 }
380 if (start_radius - end_radius).abs() <= tol.confusion() {
381 return fillet_edge(model, solid, edge, start_radius, tol);
382 }
383 let seat = planar_seat(model, solid, edge, tol)?;
384 let (first, second) = if seat
385 .along
386 .dot(seat.normals[0].cross(seat.normals[1]))
387 .is_sign_positive()
388 {
389 (0, 1)
390 } else {
391 (1, 0)
392 };
393 let sign = if seat.convex { 1.0 } else { -1.0 };
394 let n1 = seat.normals[first] * sign;
395 let n2 = seat.normals[second] * sign;
396 let a = seat.leg(first, tol)? * sign;
397 let b = seat.leg(second, tol)? * sign;
398 let bisector = {
399 let u = a + b;
400 let m = u.magnitude();
401 if m <= tol.angular() {
402 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
403 }
404 u / m
405 };
406 let depth = -n1.dot(bisector);
407 if depth <= tol.angular() {
408 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
409 }
410 let sweep = seat.along.dot(n1.cross(n2)).atan2(n1.dot(n2)) * sign;
411 if sweep <= tol.angular() {
412 ogeom_bail!(Construction, "the faces are parallel; there is no corner");
413 }
414
415 let radii = [start_radius, end_radius];
419 let apex = [seat.start, seat.end];
420 let centre = [
421 apex[0] + bisector * (radii[0] / depth),
422 apex[1] + bisector * (radii[1] / depth),
423 ];
424 let contact_a = [centre[0] + n1 * radii[0], centre[1] + n1 * radii[1]];
425 let contact_b = [centre[0] + n2 * radii[0], centre[1] + n2 * radii[1]];
426
427 let half_cos = (sweep / 2.0).cos();
430 let arc_mid = (n1 + n2) / (1.0 + n1.dot(n2));
431 let weights = [1.0, half_cos, 1.0];
432
433 let u_knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 1.0, 1.0], 1)?;
437 let v_knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], 2)?;
438 let mut net: Vec<ogeom_math::Weighted<Point>> = Vec::with_capacity(6);
439 for i in 0..2 {
440 let arms = [n1, arc_mid, n2];
441 for (j, arm) in arms.iter().enumerate() {
442 net.push(ogeom_math::Weighted::new(
443 centre[i] + *arm * radii[i],
444 weights[j],
445 tol,
446 )?);
447 }
448 }
449 let grid = ogeom_math::ControlGrid::new(net, 2, 3)?;
450 let surface: SurfaceGeometry =
451 ogeom_geom::BSplineSurface::rational(u_knots, v_knots, grid)?.into();
452 let surface_id = model.geometry_mut().add_surface(surface.clone());
453
454 let va = [
456 make_vertex(model, contact_a[0]).shape,
457 make_vertex(model, contact_a[1]).shape,
458 ];
459 let vb = [
460 make_vertex(model, contact_b[0]).shape,
461 make_vertex(model, contact_b[1]).shape,
462 ];
463
464 let rail = |model: &mut Model,
467 from: (&Shape, Point),
468 to: (&Shape, Point),
469 row: f64|
470 -> OgeomResult<Shape> {
471 let line = ogeom_geom::LineCurve::segment(from.1, to.1, tol)?;
472 let curve = Curve::Line(line);
473 let domain = ogeom_geom::Curve3d::domain(&curve);
474 let built = make_edge_between(model, curve, domain, from.0, to.0, tol)?.shape;
475 let knots = ogeom_math::KnotVector::new(vec![domain.0, domain.0, domain.1, domain.1], 1)?;
476 let pcurve = ogeom_geom::BSpline2d::new(
477 knots,
478 vec![
479 ogeom_math::Point2::new(0.0, row),
480 ogeom_math::Point2::new(1.0, row),
481 ],
482 tol,
483 )?;
484 ogeom_algo::attach_pcurve(
485 model,
486 &built,
487 pcurve.into(),
488 surface_id,
489 ogeom_topo::Location::identity(),
490 domain,
491 )?;
492 Ok(built)
493 };
494 let rail_a = rail(model, (&va[0], contact_a[0]), (&va[1], contact_a[1]), 0.0)?;
495 let rail_b = rail(model, (&vb[0], contact_b[0]), (&vb[1], contact_b[1]), 1.0)?;
496
497 let arc_edge = |model: &mut Model, i: usize| -> OgeomResult<Shape> {
500 let knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], 2)?;
501 let control = vec![
502 ogeom_math::Weighted::new(contact_a[i], 1.0, tol)?,
503 ogeom_math::Weighted::new(centre[i] + arc_mid * radii[i], half_cos, tol)?,
504 ogeom_math::Weighted::new(contact_b[i], 1.0, tol)?,
505 ];
506 let curve = Curve::BSpline(ogeom_geom::BSplineCurve::rational(knots, control)?);
507 let built = make_edge_between(model, curve, (0.0, 1.0), &va[i], &vb[i], tol)?.shape;
508 #[allow(clippy::cast_precision_loss)]
509 let column = ogeom_geom::Line2d::over(
510 ogeom_math::Axis2::new(
511 ogeom_math::Point2::new(i as f64, 0.0),
512 ogeom_math::Direction2::Y,
513 ),
514 0.0,
515 1.0,
516 )?;
517 ogeom_algo::attach_pcurve(
518 model,
519 &built,
520 column.into(),
521 surface_id,
522 ogeom_topo::Location::identity(),
523 (0.0, 1.0),
524 )?;
525 Ok(built)
526 };
527 let arc0 = arc_edge(model, 0)?;
528 let arc1 = arc_edge(model, 1)?;
529
530 let blend = {
534 let wire = ogeom_algo::make_wire(
535 model,
536 &[
537 rail_a.clone(),
538 arc1.clone(),
539 rail_b.reversed(),
540 arc0.reversed(),
541 ],
542 tol,
543 )?
544 .shape;
545 let face =
546 ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
547 use ogeom_geom::Surface as _;
548 let s_mid = surface.point_at(0.5, 0.5, tol)?;
549 let (du, dv) = surface.d1_at(0.5, 0.5, tol)?;
550 let apex_mid = apex[0].midpoint(apex[1]);
551 let outward = s_mid - apex_mid.midpoint(s_mid);
552 if du.cross(dv).dot(outward) >= 0.0 {
553 face
554 } else {
555 face.reversed()
556 }
557 };
558
559 let leg_a = planar_face(
563 model,
564 &[apex[0], contact_a[0], contact_a[1], apex[1]],
565 n1 * sign,
566 tol,
567 )?;
568 let leg_b = planar_face(
569 model,
570 &[apex[0], contact_b[0], contact_b[1], apex[1]],
571 n2 * sign,
572 tol,
573 )?;
574 let cap = |model: &mut Model, i: usize, outward: Vector| -> OgeomResult<Built> {
575 let plane = Plane::through(apex[i], Direction::new(outward, tol)?);
576 let reach = (apex[i].distance(contact_a[i]) + apex[i].distance(contact_b[i]) + 1.0) * 2.0;
577 let cap_surface: SurfaceGeometry =
578 PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
579 let apex_v = make_vertex(model, apex[i]).shape;
580 let edges = vec![
581 crate::support::segment_between(
582 model,
583 (&apex_v, apex[i]),
584 (&va[i], contact_a[i]),
585 tol,
586 )?,
587 if i == 0 { arc0.clone() } else { arc1.clone() },
588 crate::support::segment_between(
589 model,
590 (&vb[i], contact_b[i]),
591 (&apex_v, apex[i]),
592 tol,
593 )?,
594 ];
595 ogeom_algo::make_face_with_pcurves(model, cap_surface, &[edges], tol)
596 };
597 let cap0 = cap(model, 0, -seat.along)?.shape;
598 let cap1 = cap(model, 1, seat.along)?.shape;
599
600 let faces = [leg_a, leg_b, cap0, cap1, blend];
601 apply_wedge(model, solid, Some(edge), &faces, !seat.convex, tol)
602}
603
604fn planar_fillet(
606 model: &mut Model,
607 solid: &Shape,
608 edge: &Shape,
609 radius: f64,
610 mates: Option<(usize, &[Mate])>,
611 tol: Tolerances,
612) -> OgeomResult<Built> {
613 let mut seat = planar_seat(model, solid, edge, tol)?;
614 for end in [true, false] {
623 let Some((index, mates)) = mates else {
624 break;
625 };
626 let at = if end { seat.start } else { seat.end };
627 let outward = if end { -seat.along } else { seat.along };
628 let chain = mates.iter().enumerate().any(|(i, mate)| {
631 i != index
632 && mate.ends.iter().any(|(p, leaving)| {
633 p.distance(at) <= tol.confusion() * 1e3
634 && leaving.cross(outward).magnitude() <= 1e-2
635 && leaving.dot(outward) > 0.0
636 })
637 });
638 if chain {
639 continue;
640 }
641 if Mate::settled_at(mates, index, at, tol) {
642 continue;
643 }
644 let hosts = [&seat.faces[0], &seat.faces[1]];
645 if !crate::marched::crease_terminates_at(model, solid, edge, hosts, at, tol)?
646 || !crate::marched::neighbour_blend_at(
647 model,
648 solid,
649 hosts,
650 at,
651 seat.convex,
652 radius,
653 tol,
654 )?
655 {
656 continue;
657 }
658 let a = seat.leg(0, tol)? * if seat.convex { 1.0 } else { -1.0 };
660 let b = seat.leg(1, tol)? * if seat.convex { 1.0 } else { -1.0 };
661 let bisector = {
662 let u = a + b;
663 let m = u.magnitude();
664 if m <= tol.angular() {
665 continue;
666 }
667 u / m
668 };
669 let depth = seat.normals[0].dot(bisector).abs();
670 if depth <= tol.angular() {
671 continue;
672 }
673 let centre_at = |s: f64| at + outward * s + bisector * (radius / depth);
674 let deflection = ogeom_mesh::Deflection {
675 chord: (radius * 1e-2).max(tol.confusion() * 1e3),
676 ..ogeom_mesh::Deflection::default()
677 };
678 let step = radius / 8.0;
679 let mut reach: Option<f64> = None;
680 for k in 1..=32 {
681 let s = step * f64::from(k);
682 let inside =
683 ogeom_algo::classify_in_solid(model, solid, centre_at(s), deflection, tol)?
684 == ogeom_algo::Containment::In;
685 if inside != seat.convex {
686 reach = Some(s + radius * 0.25);
687 break;
688 }
689 }
690 if std::env::var_os("OGEOM_DEBUG_RUNOUT").is_some() {
691 eprintln!("RUNOUT straight seat end {end}: reach {reach:?}");
692 }
693 let Some(reach) = reach else {
694 continue;
695 };
696 if end {
697 seat.start -= seat.along * reach;
698 } else {
699 seat.end += seat.along * reach;
700 }
701 }
702 seated_fillet(model, solid, &seat, radius, Some(edge), tol)
703}
704
705pub(crate) fn seated_fillet(
713 model: &mut Model,
714 solid: &Shape,
715 seat: &Seat,
716 radius: f64,
717 edge: Option<&Shape>,
718 tol: Tolerances,
719) -> OgeomResult<Built> {
720 let (first, second) = if seat
724 .along
725 .dot(seat.normals[0].cross(seat.normals[1]))
726 .is_sign_positive()
727 {
728 (0, 1)
729 } else {
730 (1, 0)
731 };
732 let n1 = seat.normals[first];
733 let n2 = seat.normals[second];
734 let sign = if seat.convex { 1.0 } else { -1.0 };
738 let a = seat.leg(first, tol)? * sign;
739 let b = seat.leg(second, tol)? * sign;
740
741 let bisector = {
744 let u = a + b;
745 let m = u.magnitude();
746 if m <= tol.angular() {
747 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
748 }
749 u / m
750 };
751 let depth = (n1.dot(bisector)).abs();
752 if depth <= tol.angular() {
753 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
754 }
755 let centre = seat.start + bisector * (radius / depth);
756 let sweep = seat.along.dot(n1.cross(n2)).atan2(n1.dot(n2));
757 if sweep <= tol.angular() {
758 ogeom_bail!(Construction, "the faces are parallel; there is no corner");
759 }
760
761 let travel = seat.end - seat.start;
762 let length = travel.magnitude();
763 let apex0 = seat.start;
764 let apex1 = seat.end;
765 let contact_a0 = centre + n1 * (radius * sign);
766 let contact_b0 = centre + n2 * (radius * sign);
767 let contact_a1 = contact_a0 + travel;
768 let contact_b1 = contact_b0 + travel;
769
770 let along_dir = Direction::new(seat.along, tol)?;
771 let radial_dir = Direction::new(n1 * sign, tol)?;
772
773 let arc = |model: &mut Model, h: f64, from: &Shape, to: &Shape| -> OgeomResult<Shape> {
779 let frame = Frame::new(centre + seat.along * h, along_dir, radial_dir, tol)?;
780 let circle = Circle::new(frame, radius, tol)?;
781 let curve = Curve::Circle(CircleCurve::new(circle));
782 Ok(make_edge_between(model, curve, (0.0, sweep), from, to, tol)?.shape)
783 };
784
785 let leg_a = planar_face(
789 model,
790 &[apex0, contact_a0, contact_a1, apex1],
791 n1 * sign,
792 tol,
793 )?;
794 let leg_b = planar_face(
795 model,
796 &[apex0, contact_b0, contact_b1, apex1],
797 n2 * sign,
798 tol,
799 )?;
800
801 let cap = |model: &mut Model,
803 apex: Point,
804 ca: Point,
805 cb: Point,
806 h: f64,
807 outward: Vector|
808 -> OgeomResult<Shape> {
809 let plane = Plane::through(apex, Direction::new(outward, tol)?);
810 let reach = (apex.distance(ca).max(apex.distance(cb)) + radius).max(1.0) * 2.0;
811 let surface = PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?;
812 let apex_v = make_vertex(model, apex).shape;
813 let ca_v = make_vertex(model, ca).shape;
814 let cb_v = make_vertex(model, cb).shape;
815 let edges = vec![
816 segment_between(model, (&apex_v, apex), (&ca_v, ca), tol)?,
817 arc(model, h, &ca_v, &cb_v)?,
818 segment_between(model, (&cb_v, cb), (&apex_v, apex), tol)?,
819 ];
820 face_from_edges(model, surface.into(), &edges, tol)
821 };
822 let cap0 = cap(model, apex0, contact_a0, contact_b0, 0.0, -seat.along)?;
823 let cap1 = cap(model, apex1, contact_a1, contact_b1, length, seat.along)?;
824
825 let blend = {
829 let frame = Frame::new(centre, along_dir, radial_dir, tol)?;
830 let surface = CylinderSurface::new(Cylinder::new(frame, radius, tol)?, (0.0, length))?;
831 let a0_v = make_vertex(model, contact_a0).shape;
832 let b0_v = make_vertex(model, contact_b0).shape;
833 let a1_v = make_vertex(model, contact_a1).shape;
834 let b1_v = make_vertex(model, contact_b1).shape;
835 let edges = vec![
836 arc(model, 0.0, &a0_v, &b0_v)?,
837 segment_between(model, (&b0_v, contact_b0), (&b1_v, contact_b1), tol)?,
838 arc(model, length, &a1_v, &b1_v)?.reversed(),
839 segment_between(model, (&a0_v, contact_a0), (&a1_v, contact_a1), tol)?.reversed(),
840 ];
841 face_from_edges(model, surface.into(), &edges, tol)?
842 };
843
844 let faces = [leg_a, leg_b, cap0, cap1, blend.reversed()];
845 apply_wedge(model, solid, edge, &faces, !seat.convex, tol)
846}
847
848fn revolved_fillet(
860 model: &mut Model,
861 solid: &Shape,
862 edge: &Shape,
863 rim: &CircleCurve,
864 radius: f64,
865 tol: Tolerances,
866) -> OgeomResult<Built> {
867 let seat = revolved_seat(model, solid, edge, rim, tol)?;
868
869 let tube_rho = seat.sigma.mul_add(-(seat.tau * radius), seat.radius);
870 if tube_rho <= tol.confusion() {
871 ogeom_bail!(
872 Construction,
873 "a fillet of radius {radius} swallows the axis of a rim of \
874 radius {}",
875 seat.radius
876 );
877 }
878 let tube_level = seat.centre - seat.up * (seat.tau * radius);
879 let flanks = revolved_flanks(model, &seat, radius, tube_rho, tol)?;
880
881 let blend_band = {
884 let surface: SurfaceGeometry = TorusSurface::new(Torus::new(
885 seat.frame_at(tube_level, tol)?,
886 tube_rho,
887 radius,
888 tol,
889 )?)
890 .into();
891 make_revolution_band(model, &surface, &flanks.wall_ring, &flanks.cap_ring, tol)?.reversed()
892 };
893
894 let faces = [flanks.wall_band, flanks.annulus, blend_band];
895 apply_wedge(model, solid, Some(edge), &faces, seat.additive(), tol)
896}
897
898fn refind_edges(
914 model: &Model,
915 solid: &Shape,
916 edge: &Shape,
917 tol: Tolerances,
918) -> OgeomResult<Vec<Shape>> {
919 use ogeom_geom::Curve3d as _;
920 use ogeom_topo::ShapeType;
921 let (curve, range) = edge_curve(model, edge, tol)?;
922 let within = |t: f64| -> bool {
926 let slack = (range.1 - range.0).abs() * 1e-3;
927 let t = if curve.is_periodic() {
928 let (lo, hi) = curve.domain();
929 let period = hi - lo;
930 if period > 0.0 {
931 range.0 + (t - range.0).rem_euclid(period)
932 } else {
933 t
934 }
935 } else {
936 t
937 };
938 t >= range.0.min(range.1) - slack && t <= range.0.max(range.1) + slack
939 };
940 let mut matches: Vec<Shape> = Vec::new();
941 for candidate in ogeom_topo::explore_unique(model, solid, ShapeType::Edge)? {
942 if candidate.is_same(edge) {
943 return Ok(vec![candidate]);
944 }
945 let Ok((c_curve, c_range)) = edge_curve(model, &candidate, tol) else {
946 continue;
947 };
948 let mut all_on = true;
949 for i in 0..3 {
950 let t = c_range.0 + (c_range.1 - c_range.0) * f64::from(i) / 2.0;
951 let p = c_curve.point_at(t, tol)?;
952 let projected = ogeom_algo::project_on_curve(&curve, p, 64, tol)?;
953 if projected.distance > tol.confusion() * 100.0
954 || (i == 1 && !within(projected.parameter))
955 {
956 all_on = false;
957 break;
958 }
959 }
960 if !all_on || matches.iter().any(|m| m.is_same(&candidate)) {
961 continue;
962 }
963 let mid = c_curve.point_at(f64::midpoint(c_range.0, c_range.1), tol)?;
964 let mut normals: Vec<Vector> = Vec::new();
965 for face in ogeom_topo::explore_unique(model, solid, ShapeType::Face)? {
966 let touches = ogeom_topo::explore_unique(model, &face, ShapeType::Edge)?
967 .iter()
968 .any(|e| crate::support::same_occurrence(model, e, &candidate, tol));
969 if let (true, Some(normal)) = (
970 touches,
971 crate::marched::face_normal_near(model, &face, mid, tol)?,
972 ) {
973 normals.push(normal);
974 }
975 }
976 if matches!(normals.as_slice(), [first, second]
980 if first.cross(*second).magnitude() <= 1e-2)
981 {
982 continue;
983 }
984 matches.push(candidate);
985 }
986 if matches.is_empty() {
987 ogeom_bail!(
988 Construction,
989 "an earlier blend in the chain consumed this edge; the chain's \
990 members interfere"
991 );
992 }
993 Ok(matches)
994}
995
996fn revolved_arc_fillet(
1005 model: &mut Model,
1006 solid: &Shape,
1007 edge: &Shape,
1008 rim: &CircleCurve,
1009 radius: f64,
1010 tol: Tolerances,
1011) -> OgeomResult<Built> {
1012 use ogeom_geom::Curve3d as _;
1013 let seat = revolved_seat(model, solid, edge, rim, tol)?;
1014
1015 let tube_rho = seat.sigma.mul_add(-(seat.tau * radius), seat.radius);
1016 if tube_rho <= tol.confusion() {
1017 ogeom_bail!(
1018 Construction,
1019 "a fillet of radius {radius} swallows the axis of a rim of \
1020 radius {}",
1021 seat.radius
1022 );
1023 }
1024 let tube_level = seat.centre - seat.up * (seat.tau * radius);
1025
1026 let seat_frame = seat.frame_at(seat.centre, tol)?;
1030 let (curve, crange) = edge_curve(model, edge, tol)?;
1031 let angle_of = |t: f64| -> OgeomResult<f64> {
1032 let local = seat_frame.to_local(curve.point_at(t, tol)?);
1033 Ok(local.y.atan2(local.x))
1034 };
1035 let tau_turn = core::f64::consts::TAU;
1036 let raw0 = angle_of(crange.0)?;
1037 let raw1 = angle_of(crange.1)?;
1038 let mid = angle_of(f64::midpoint(crange.0, crange.1))?;
1039 let (theta0, theta1) = {
1040 let span = (raw1 - raw0).rem_euclid(tau_turn);
1041 let inside = (mid - raw0).rem_euclid(tau_turn) <= span;
1042 if inside {
1043 (raw0, raw0 + span)
1044 } else {
1045 (raw1, raw1 + (tau_turn - span))
1046 }
1047 };
1048 if theta1 - theta0 <= tol.angular() {
1049 ogeom_bail!(Construction, "the rim arc has no length to blend along");
1050 }
1051 let sweep = theta1 - theta0;
1052
1053 let dir_at = |theta: f64| -> Vector {
1056 seat_frame.x().vector() * theta.cos() + seat_frame.y().vector() * theta.sin()
1057 };
1058 let wedge_frame_at = |origin: Point| -> OgeomResult<Frame> {
1062 Frame::new(
1063 origin,
1064 Direction::new(seat.up, tol)?,
1065 Direction::new(dir_at(theta0), tol)?,
1066 tol,
1067 )
1068 };
1069 let apex_at = |theta: f64| seat.centre + dir_at(theta) * seat.radius;
1070 let wall_at = |theta: f64| tube_level + dir_at(theta) * seat.radius;
1071 let cap_at = |theta: f64| seat.centre + dir_at(theta) * tube_rho;
1072
1073 let va0 = make_vertex(model, apex_at(theta0)).shape;
1076 let va1 = make_vertex(model, apex_at(theta1)).shape;
1077 let vw0 = make_vertex(model, wall_at(theta0)).shape;
1078 let vw1 = make_vertex(model, wall_at(theta1)).shape;
1079 let vc0 = make_vertex(model, cap_at(theta0)).shape;
1080 let vc1 = make_vertex(model, cap_at(theta1)).shape;
1081
1082 let ring_arc = |model: &mut Model,
1083 origin: Point,
1084 r: f64,
1085 from: &Shape,
1086 to: &Shape|
1087 -> OgeomResult<Shape> {
1088 let circle = Circle::new(wedge_frame_at(origin)?, r, tol)?;
1089 let curve = Curve::Circle(CircleCurve::new(circle));
1090 Ok(make_edge_between(model, curve, (0.0, sweep), from, to, tol)?.shape)
1091 };
1092 let quarter = |model: &mut Model, theta: f64, from: &Shape, to: &Shape| -> OgeomResult<Shape> {
1095 let dir = dir_at(theta);
1096 let centre_m = tube_level + dir * tube_rho;
1097 let x_dir = Direction::new(dir * (seat.sigma * seat.tau), tol)?;
1098 let z_dir = Direction::new(dir.cross(seat.up) * seat.sigma, tol)?;
1099 let frame = Frame::new(centre_m, z_dir, x_dir, tol)?;
1100 let circle = Circle::new(frame, radius, tol)?;
1101 let curve = Curve::Circle(CircleCurve::new(circle));
1102 Ok(make_edge_between(
1103 model,
1104 curve,
1105 (0.0, core::f64::consts::FRAC_PI_2),
1106 from,
1107 to,
1108 tol,
1109 )?
1110 .shape)
1111 };
1112
1113 let arc_apex = ring_arc(model, seat.centre, seat.radius, &va0, &va1)?;
1114 let arc_wall = ring_arc(model, tube_level, seat.radius, &vw0, &vw1)?;
1115 let arc_cap = ring_arc(model, seat.centre, tube_rho, &vc0, &vc1)?;
1116 let ruling0 = segment_between(model, (&va0, apex_at(theta0)), (&vw0, wall_at(theta0)), tol)?;
1117 let ruling1 = segment_between(model, (&vw1, wall_at(theta1)), (&va1, apex_at(theta1)), tol)?;
1118 let radial0 = segment_between(model, (&vc0, cap_at(theta0)), (&va0, apex_at(theta0)), tol)?;
1119 let radial1 = segment_between(model, (&va1, apex_at(theta1)), (&vc1, cap_at(theta1)), tol)?;
1120 let quarter0 = quarter(model, theta0, &vw0, &vc0)?;
1121 let quarter1 = quarter(model, theta1, &vw1, &vc1)?;
1122
1123 let wall_patch = {
1126 let below = radius + 1.0;
1127 let origin = seat.centre - seat.up * (seat.tau * below);
1128 let surface: SurfaceGeometry = CylinderSurface::new(
1129 Cylinder::new(wedge_frame_at(origin)?, seat.radius, tol)?,
1130 (0.0, 2.0 * below),
1131 )?
1132 .into();
1133 let edges = vec![
1134 arc_wall.clone(),
1135 ruling1.clone(),
1136 arc_apex.reversed(),
1137 ruling0.clone(),
1138 ];
1139 let face = face_from_edges(model, surface, &edges, tol)?;
1140 if seat.sigma * seat.tau < 0.0 {
1141 face.reversed()
1142 } else {
1143 face
1144 }
1145 };
1146
1147 let cap_sector = {
1149 let plane = Plane::through(seat.centre, Direction::new(seat.up * seat.tau, tol)?);
1150 let reach = (seat.radius + radius + 1.0) * 2.0;
1151 let surface: SurfaceGeometry =
1152 PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
1153 let edges = vec![
1154 arc_apex.clone(),
1155 radial1.clone(),
1156 arc_cap.reversed(),
1157 radial0.clone(),
1158 ];
1159 face_from_edges(model, surface, &edges, tol)?
1160 };
1161
1162 let blend_patch = {
1165 let surface: SurfaceGeometry = TorusSurface::new(Torus::new(
1166 wedge_frame_at(tube_level)?,
1167 tube_rho,
1168 radius,
1169 tol,
1170 )?)
1171 .into();
1172 let edges = vec![
1173 arc_wall.clone(),
1174 quarter1.clone(),
1175 arc_cap.reversed(),
1176 quarter0.reversed(),
1177 ];
1178 face_from_edges(model, surface, &edges, tol)?.reversed()
1179 };
1180
1181 let tangent_at = |theta: f64| seat.up.cross(dir_at(theta));
1184 let end_cap =
1185 |model: &mut Model, theta: f64, outward: Vector, edges: Vec<Shape>| -> OgeomResult<Shape> {
1186 let plane = Plane::through(apex_at(theta), Direction::new(outward, tol)?);
1187 let reach = (seat.radius + radius + 1.0) * 2.0;
1188 let surface: SurfaceGeometry =
1189 PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
1190 face_from_edges(model, surface, &edges, tol)
1191 };
1192 let cap_start = end_cap(
1193 model,
1194 theta0,
1195 -tangent_at(theta0),
1196 vec![ruling0.clone(), quarter0.clone(), radial0.clone()],
1197 )?;
1198 let cap_end = end_cap(
1199 model,
1200 theta1,
1201 tangent_at(theta1),
1202 vec![ruling1.clone(), radial1.clone(), quarter1.reversed()],
1203 )?;
1204
1205 let faces = [wall_patch, cap_sector, blend_patch, cap_start, cap_end];
1206 apply_wedge(model, solid, Some(edge), &faces, seat.additive(), tol)
1207}