1use crate::chamfer_sphere::{SphereSeat, sphere_bevel, sphere_seat};
22use crate::support::{
23 Blamed, RevolvedSeat, Seat, apply_wedge, edge_curve, planar_face, planar_seat, revolved_flanks,
24 revolved_seat,
25};
26use ogeom_algo::{Built, make_revolution_band};
27use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
28use ogeom_geom::{ConeSurface, Curve, SurfaceGeometry};
29use ogeom_math::{Cone, Direction, Frame};
30use ogeom_topo::{Model, Shape};
31
32pub fn chamfer_edge(
55 model: &mut Model,
56 solid: &Shape,
57 edge: &Shape,
58 distance: f64,
59 tol: Tolerances,
60) -> OgeomResult<Built> {
61 ogeom_algo::on_own_nodes(
62 model,
63 solid,
64 std::slice::from_ref(edge),
65 |model, solid, parts| chamfer_edge_own(model, solid, &parts[0], distance, tol),
66 )
67}
68
69fn chamfer_edge_own(
71 model: &mut Model,
72 solid: &Shape,
73 edge: &Shape,
74 distance: f64,
75 tol: Tolerances,
76) -> OgeomResult<Built> {
77 if crate::engine::is_seam(model, solid, edge, tol)? {
78 return Ok(unchanged(solid));
79 }
80 let specs = [(edge.clone(), Chamfer::Symmetric(distance))];
81 if let Some(built) = engine_chamfer(model, solid, &specs, tol)? {
82 return Ok(built);
83 }
84 wedge_for(model, solid, edge, &specs[0].1, tol)?.apply(model, solid, edge, tol)
85}
86
87pub fn chamfer_edge_distances(
101 model: &mut Model,
102 solid: &Shape,
103 edge: &Shape,
104 face: &Shape,
105 on_face: f64,
106 on_other: f64,
107 tol: Tolerances,
108) -> OgeomResult<Built> {
109 ogeom_algo::on_own_nodes(
110 model,
111 solid,
112 &[edge.clone(), face.clone()],
113 |model, solid, parts| {
114 chamfer_edge_distances_own(model, solid, &parts[0], &parts[1], on_face, on_other, tol)
115 },
116 )
117}
118
119fn chamfer_edge_distances_own(
121 model: &mut Model,
122 solid: &Shape,
123 edge: &Shape,
124 face: &Shape,
125 on_face: f64,
126 on_other: f64,
127 tol: Tolerances,
128) -> OgeomResult<Built> {
129 let spec = Chamfer::Distances {
130 face: face.clone(),
131 on_face,
132 on_other,
133 };
134 if crate::engine::is_seam(model, solid, edge, tol)? {
135 return Ok(unchanged(solid));
136 }
137 let specs = [(edge.clone(), spec)];
138 if let Some(built) = engine_chamfer(model, solid, &specs, tol)? {
139 return Ok(built);
140 }
141 wedge_for(model, solid, edge, &specs[0].1, tol)?.apply(model, solid, edge, tol)
142}
143
144pub fn chamfer_edge_angle(
159 model: &mut Model,
160 solid: &Shape,
161 edge: &Shape,
162 face: &Shape,
163 distance: f64,
164 angle: f64,
165 tol: Tolerances,
166) -> OgeomResult<Built> {
167 ogeom_algo::on_own_nodes(
168 model,
169 solid,
170 &[edge.clone(), face.clone()],
171 |model, solid, parts| {
172 chamfer_edge_angle_own(model, solid, &parts[0], &parts[1], distance, angle, tol)
173 },
174 )
175}
176
177fn chamfer_edge_angle_own(
179 model: &mut Model,
180 solid: &Shape,
181 edge: &Shape,
182 face: &Shape,
183 distance: f64,
184 angle: f64,
185 tol: Tolerances,
186) -> OgeomResult<Built> {
187 let spec = Chamfer::Angle {
188 face: face.clone(),
189 distance,
190 angle,
191 };
192 if crate::engine::is_seam(model, solid, edge, tol)? {
193 return Ok(unchanged(solid));
194 }
195 let specs = [(edge.clone(), spec)];
196 if let Some(built) = engine_chamfer(model, solid, &specs, tol)? {
197 return Ok(built);
198 }
199 wedge_for(model, solid, edge, &specs[0].1, tol)?.apply(model, solid, edge, tol)
200}
201
202fn chamfer_parts(specs: &[(Shape, Chamfer)]) -> Vec<Shape> {
205 let mut parts = Vec::with_capacity(specs.len() * 2);
206 for (edge, spec) in specs {
207 parts.push(edge.clone());
208 match spec {
209 Chamfer::Symmetric(_) => {}
210 Chamfer::Distances { face, .. } | Chamfer::Angle { face, .. } => {
211 parts.push(face.clone());
212 }
213 }
214 }
215 parts
216}
217
218fn chamfer_specs(specs: &[(Shape, Chamfer)], parts: &[Shape]) -> Vec<(Shape, Chamfer)> {
221 let mut parts = parts.iter().cloned();
222 let mut next = |was: &Shape| parts.next().unwrap_or_else(|| was.clone());
223 specs
224 .iter()
225 .map(|(edge, spec)| {
226 let edge = next(edge);
227 let spec = match spec {
228 Chamfer::Symmetric(d) => Chamfer::Symmetric(*d),
229 Chamfer::Distances {
230 face,
231 on_face,
232 on_other,
233 } => Chamfer::Distances {
234 face: next(face),
235 on_face: *on_face,
236 on_other: *on_other,
237 },
238 Chamfer::Angle {
239 face,
240 distance,
241 angle,
242 } => Chamfer::Angle {
243 face: next(face),
244 distance: *distance,
245 angle: *angle,
246 },
247 };
248 (edge, spec)
249 })
250 .collect()
251}
252
253#[derive(Debug, Clone)]
255pub enum Chamfer {
256 Symmetric(f64),
258 Distances {
261 face: Shape,
263 on_face: f64,
265 on_other: f64,
267 },
268 Angle {
271 face: Shape,
273 distance: f64,
275 angle: f64,
277 },
278}
279
280pub fn chamfer_edges(
288 model: &mut Model,
289 solid: &Shape,
290 edges: &[Shape],
291 distance: f64,
292 tol: Tolerances,
293) -> OgeomResult<Built> {
294 let specs: Vec<(Shape, Chamfer)> = edges
295 .iter()
296 .map(|e| (e.clone(), Chamfer::Symmetric(distance)))
297 .collect();
298 chamfer_edges_with(model, solid, &specs, tol)
299}
300
301pub fn chamfer_edges_with(
347 model: &mut Model,
348 solid: &Shape,
349 specs: &[(Shape, Chamfer)],
350 tol: Tolerances,
351) -> OgeomResult<Built> {
352 chamfer_edges_blamed(model, solid, specs, tol).map_err(|blamed| blamed.error)
353}
354
355pub(crate) fn chamfer_edges_blamed(
361 model: &mut Model,
362 solid: &Shape,
363 specs: &[(Shape, Chamfer)],
364 tol: Tolerances,
365) -> Result<Built, Blamed> {
366 if specs.is_empty() {
367 return Err(
368 ogeom_core::ogeom_err!(Construction, "a chain of no edges bevels nothing").into(),
369 );
370 }
371 let (specs, index) = without_seams(model, solid, specs, tol)?;
372 if specs.is_empty() {
373 return Ok(unchanged(solid));
374 }
375 if let Some(built) = engine_chamfer(model, solid, &specs, tol)? {
376 return Ok(built);
377 }
378 chamfer_edges_mitred_blamed(model, solid, &specs, tol).map_err(|mut blamed| {
379 for i in &mut blamed.edges {
380 *i = index[*i];
381 }
382 blamed
383 })
384}
385
386pub(crate) type Specs = Vec<(Shape, Chamfer)>;
388
389pub(crate) fn without_seams(
393 model: &Model,
394 solid: &Shape,
395 specs: &[(Shape, Chamfer)],
396 tol: Tolerances,
397) -> OgeomResult<(Specs, Vec<usize>)> {
398 let mut kept = Vec::with_capacity(specs.len());
399 let mut index = Vec::with_capacity(specs.len());
400 for (i, spec) in specs.iter().enumerate() {
401 if !crate::engine::is_seam(model, solid, &spec.0, tol)? {
402 kept.push(spec.clone());
403 index.push(i);
404 }
405 }
406 Ok((kept, index))
407}
408
409pub(crate) fn unchanged(solid: &Shape) -> Built {
411 let mut built = Built::from_nothing(solid.clone());
412 built.history.modify(solid, solid.clone());
413 built
414}
415
416pub(crate) fn engine_chamfer(
421 model: &mut Model,
422 solid: &Shape,
423 specs: &[(Shape, Chamfer)],
424 tol: Tolerances,
425) -> OgeomResult<Option<Built>> {
426 use crate::engine::Dissolved;
427 match crate::engine::dissolve(model, solid, tol)? {
428 Dissolved::Plain => {
429 let Some(request) = crate::engine::Request::from_chamfers(model, solid, specs, tol)?
430 else {
431 return Ok(None);
432 };
433 crate::engine::run(model, solid, &request, tol)
434 }
435 Dissolved::Outside => Ok(None),
436 Dissolved::Into(seamless) => {
437 let Some(specs) = seamless.specs(specs) else {
438 return Ok(None);
439 };
440 let Some(request) =
441 crate::engine::Request::from_chamfers(model, &seamless.solid, &specs, tol)?
442 else {
443 return Ok(None);
444 };
445 let Some(built) = crate::engine::run(model, &seamless.solid, &request, tol)? else {
446 return Ok(None);
447 };
448 Ok(Some(Built::new(
449 built.shape,
450 seamless.carry(&built.history),
451 )))
452 }
453 }
454}
455
456pub(crate) fn chamfer_edges_mitred(
460 model: &mut Model,
461 solid: &Shape,
462 specs: &[(Shape, Chamfer)],
463 tol: Tolerances,
464) -> OgeomResult<Built> {
465 chamfer_edges_mitred_blamed(model, solid, specs, tol).map_err(|blamed| blamed.error)
466}
467
468fn chamfer_edges_mitred_blamed(
470 model: &mut Model,
471 solid: &Shape,
472 specs: &[(Shape, Chamfer)],
473 tol: Tolerances,
474) -> Result<Built, Blamed> {
475 let mut blame = Vec::new();
476 ogeom_algo::on_own_nodes(
477 model,
478 solid,
479 &chamfer_parts(specs),
480 |model, solid, parts| {
481 chamfer_edges_with_own(model, solid, &chamfer_specs(specs, parts), tol).map_err(
482 |blamed| {
483 blame = blamed.edges;
484 blamed.error
485 },
486 )
487 },
488 )
489 .map_err(|error| Blamed {
490 edges: blame,
491 error,
492 })
493}
494
495fn chamfer_edges_with_own(
501 model: &mut Model,
502 solid: &Shape,
503 specs: &[(Shape, Chamfer)],
504 tol: Tolerances,
505) -> Result<Built, Blamed> {
506 if specs.is_empty() {
507 return Err(
508 ogeom_core::ogeom_err!(Construction, "a chain of no edges bevels nothing").into(),
509 );
510 }
511 let mut wedges = Vec::with_capacity(specs.len());
512 for (i, (edge, spec)) in specs.iter().enumerate() {
513 wedges.push(
514 wedge_for(model, solid, edge, spec, tol).map_err(|error| Blamed {
515 edges: vec![i],
516 error,
517 })?,
518 );
519 }
520 let marched = specs
524 .iter()
525 .any(|(edge, _)| matches!(seat_kind(model, edge, tol), Ok(SeatKind::Marched)));
526 let given = if marched {
527 let mut given = Vec::with_capacity(specs.len());
528 for (edge, _) in specs {
529 given.push(faces_holding(model, solid, edge)?);
530 }
531 given
532 } else {
533 Vec::new()
534 };
535 let mut built = Built::from_nothing(solid.clone());
536 let mut rings: Vec<(usize, WholeRing)> = Vec::new();
537 for (i, ((edge, _), wedge)) in specs.iter().zip(wedges).enumerate() {
538 if let Some(ring) = wedge.whole_ring {
542 if let Some((j, first)) = rings.iter().find(|(_, r)| r.same_circle(&ring, tol)) {
543 if first.same_legs(&ring, tol) {
544 continue;
545 }
546 return Err(Blamed {
547 edges: vec![i, *j],
548 error: ogeom_core::ogeom_err!(
549 Construction,
550 "two arcs of one sphere rim ask for different chamfers \
551 of its whole ring"
552 ),
553 });
554 }
555 rings.push((i, ring));
556 }
557 let applied = match &wedge.marched {
558 Some(spec) => marched_step(model, &built, edge, spec, &given[i], &specs[..i], tol),
559 None => wedge.apply(model, &built.shape, edge, tol),
560 };
561 let step = match applied {
562 Ok(step) => step,
563 Err(error) => {
564 let mut edges = vec![i];
565 let ends = |e: &Shape| -> OgeomResult<Vec<ogeom_topo::TShapeId>> {
566 Ok(ogeom_algo::edge_vertices(model, e)?
567 .map(|(a, b)| vec![a.node(), b.node()])
568 .unwrap_or_default())
569 };
570 let own = ends(edge)?;
571 for (j, (other, _)) in specs[..i].iter().enumerate() {
572 if ends(other)?.iter().any(|v| own.contains(v)) {
573 edges.push(j);
574 }
575 }
576 return Err(Blamed { edges, error });
577 }
578 };
579 built = Built {
580 shape: step.shape.clone(),
581 history: built.history.then(&step.history),
582 };
583 }
584 Ok(built)
585}
586
587enum RimSeat {
590 Revolved(RevolvedSeat),
591 Sphere(SphereSeat),
592}
593
594fn rim_seat(
595 model: &Model,
596 solid: &Shape,
597 edge: &Shape,
598 rim: &ogeom_geom::CircleCurve,
599 tol: Tolerances,
600) -> OgeomResult<RimSeat> {
601 match revolved_seat(model, solid, edge, rim, tol) {
602 Ok(seat) => Ok(RimSeat::Revolved(seat)),
603 Err(refused) => match sphere_seat(model, solid, edge, rim, tol)? {
604 Some(seat) => Ok(RimSeat::Sphere(seat)),
605 None => Err(refused),
606 },
607 }
608}
609
610fn wall_depth(seat: &RevolvedSeat, on_wall: f64) -> f64 {
614 if seat.slope == 0.0 {
615 on_wall
616 } else {
617 on_wall / seat.slope.hypot(1.0)
618 }
619}
620
621fn rim_corner(seat: &RevolvedSeat) -> f64 {
625 (seat.sigma * seat.slope / seat.slope.hypot(1.0)).acos()
628}
629
630fn wedge_for(
632 model: &mut Model,
633 solid: &Shape,
634 edge: &Shape,
635 spec: &Chamfer,
636 tol: Tolerances,
637) -> OgeomResult<Wedge> {
638 match legs(model, solid, edge, spec, tol)? {
639 Legs::Straight(seat, distances) => bevel(model, &seat, distances, tol),
640 Legs::Rim(seat, on_wall, on_cap) => {
641 revolved_bevel(model, solid, edge, &seat, on_wall, on_cap, tol)
642 }
643 Legs::Marched => Ok(Wedge {
644 faces: Vec::new(),
645 solid: None,
646 additive: false,
647 whole_ring: None,
648 marched: Some(spec.clone()),
649 }),
650 Legs::Sphere(seat, on_wall, on_cap) => {
651 let per = |face: &Shape| sphere_leg(&seat, face, on_wall, on_cap);
654 if crate::fits::legs_largest(model, solid, edge, 1.0, per, tol)? < 1.0 {
655 ogeom_bail!(
656 Construction,
657 "a chamfer on the rim between a plane and a sphere runs past \
658 one of its faces"
659 );
660 }
661 let closed =
662 ogeom_algo::edge_vertices(model, edge)?.is_some_and(|(a, b)| a.is_same(&b));
663 let Curve::Circle(rim) = edge_curve(model, edge, tol)?.0 else {
664 ogeom_bail!(Construction, "a sphere's rim is not a circle");
665 };
666 let whole = closed
667 || crate::chamfer_sphere::rim_closes(model, solid, &seat, &rim.circle(), tol)?;
668 let span = if whole {
671 None
672 } else {
673 let frame =
674 Frame::new(seat.centre, Direction::new(seat.up, tol)?, seat.x_ref, tol)?;
675 Some(arc_window(model, edge, &frame, tol)?)
676 };
677 Ok(Wedge {
678 faces: sphere_bevel(model, &seat, on_wall, on_cap, span, tol)?,
679 solid: None,
680 additive: seat.tau < 0.0,
681 whole_ring: whole.then_some(WholeRing {
682 centre: seat.centre,
683 radius: seat.radius,
684 up: seat.up,
685 on_wall,
686 on_cap,
687 }),
688 marched: None,
689 })
690 }
691 }
692}
693
694fn sphere_leg(seat: &SphereSeat, face: &Shape, on_wall: f64, on_cap: f64) -> f64 {
697 if face.is_same(&seat.wall_face) {
698 on_wall
699 } else if face.is_same(&seat.cap_face) {
700 on_cap
701 } else {
702 0.0
703 }
704}
705
706pub fn largest_chamfer_distance(
731 model: &Model,
732 solid: &Shape,
733 edge: &Shape,
734 spec: &Chamfer,
735 up_to: f64,
736 tol: Tolerances,
737) -> OgeomResult<Option<f64>> {
738 let conf = tol.confusion();
739 if !up_to.is_finite() || up_to <= conf {
740 ogeom_bail!(
741 Construction,
742 "a largest distance up to {up_to} asks for nothing"
743 );
744 }
745 let unit = match spec {
747 Chamfer::Symmetric(_) => Chamfer::Symmetric(1.0),
748 Chamfer::Distances {
749 face,
750 on_face,
751 on_other,
752 } => Chamfer::Distances {
753 face: face.clone(),
754 on_face: *on_face,
755 on_other: *on_other,
756 },
757 Chamfer::Angle { face, angle, .. } => Chamfer::Angle {
758 face: face.clone(),
759 distance: 1.0,
760 angle: *angle,
761 },
762 };
763 let mut largest = up_to;
764 let shortest = match legs(model, solid, edge, &unit, tol)? {
765 Legs::Straight(seat, per) => {
766 if seat.convex {
767 for (i, leg) in per.into_iter().enumerate() {
768 if leg > 0.0 {
769 let along = seat.leg(i, tol)?;
770 let reach = crate::support::face_reach(
771 model,
772 &seat.faces[i],
773 seat.start,
774 along,
775 tol,
776 )?;
777 largest = largest.min(reach / leg);
778 }
779 }
780 }
781 per[0].min(per[1])
782 }
783 Legs::Rim(seat, on_wall, on_cap) => {
784 let inward = seat.sigma * seat.tau * on_cap;
787 if inward > 0.0 {
788 largest = largest.min((seat.radius - 2.0 * conf) / inward);
789 }
790 let narrowing = seat.slope * seat.tau * wall_depth(&seat, on_wall);
793 if narrowing > 0.0 {
794 largest = largest.min((seat.radius - 2.0 * conf) / narrowing);
795 }
796 let per = |face: &Shape| {
797 if face.is_same(&seat.wall_face) {
798 on_wall
799 } else if face.is_same(&seat.cap_face) {
800 on_cap
801 } else {
802 0.0
803 }
804 };
805 largest = largest.min(crate::fits::legs_largest(
806 model, solid, edge, largest, per, tol,
807 )?);
808 on_wall.min(on_cap)
809 }
810 Legs::Marched => ogeom_bail!(
811 NotDone,
812 "the largest distance of a chamfer on a marched seat is not read \
813 from the seat"
814 ),
815 Legs::Sphere(seat, on_wall, on_cap) => {
816 if matches!(spec, Chamfer::Angle { .. }) {
819 ogeom_bail!(
820 NotDone,
821 "the largest distance of a chamfer at an angle on a sphere's \
822 rim is not read from the seat"
823 );
824 }
825 let inward = seat.sigma * seat.tau * on_cap;
826 if inward > 0.0 {
827 largest = largest.min((seat.radius - 2.0 * conf) / inward);
828 }
829 largest = largest.min((seat.to_pole() - 2.0 * conf) / on_wall);
830 let per = |face: &Shape| sphere_leg(&seat, face, on_wall, on_cap);
831 largest = largest.min(crate::fits::legs_largest(
832 model, solid, edge, largest, per, tol,
833 )?);
834 on_wall.min(on_cap)
835 }
836 };
837 Ok((largest * shortest > conf).then_some(largest))
838}
839
840pub(crate) enum Legs {
842 Straight(Seat, [f64; 2]),
845 Rim(RevolvedSeat, f64, f64),
848 Sphere(SphereSeat, f64, f64),
851 Marched,
854}
855
856pub(crate) fn legs(
858 model: &Model,
859 solid: &Shape,
860 edge: &Shape,
861 spec: &Chamfer,
862 tol: Tolerances,
863) -> OgeomResult<Legs> {
864 match spec {
865 Chamfer::Symmetric(distance) => match seat_kind(model, edge, tol)? {
866 SeatKind::Straight => {
867 let seat = planar_seat(model, solid, edge, tol)?;
868 Ok(Legs::Straight(seat, [*distance, *distance]))
869 }
870 SeatKind::Marched => Ok(Legs::Marched),
871 SeatKind::Rim(rim) => match rim_seat(model, solid, edge, &rim, tol)? {
872 RimSeat::Revolved(seat) => Ok(Legs::Rim(seat, *distance, *distance)),
873 RimSeat::Sphere(seat) => Ok(Legs::Sphere(seat, *distance, *distance)),
874 },
875 },
876 Chamfer::Distances {
877 face,
878 on_face,
879 on_other,
880 } => match seat_kind(model, edge, tol)? {
881 SeatKind::Straight => {
882 let seat = planar_seat(model, solid, edge, tol)?;
883 let i = seat_side(&seat, face)?;
884 let mut distances = [0.0; 2];
885 distances[i] = *on_face;
886 distances[1 - i] = *on_other;
887 Ok(Legs::Straight(seat, distances))
888 }
889 SeatKind::Marched => Ok(Legs::Marched),
890 SeatKind::Rim(rim) => match rim_seat(model, solid, edge, &rim, tol)? {
891 RimSeat::Revolved(seat) => {
892 let (on_wall, on_cap) =
893 revolved_side(&seat.wall_face, &seat.cap_face, face, *on_face, *on_other)?;
894 Ok(Legs::Rim(seat, on_wall, on_cap))
895 }
896 RimSeat::Sphere(seat) => {
897 let (on_wall, on_cap) =
898 revolved_side(&seat.wall_face, &seat.cap_face, face, *on_face, *on_other)?;
899 Ok(Legs::Sphere(seat, on_wall, on_cap))
900 }
901 },
902 },
903 Chamfer::Angle {
904 face,
905 distance,
906 angle,
907 } => {
908 let (distance, angle) = (*distance, *angle);
909 if !angle.is_finite() || angle <= tol.angular() {
910 ogeom_bail!(
911 Construction,
912 "a chamfer at an angle of {angle} cuts nothing"
913 );
914 }
915 match seat_kind(model, edge, tol)? {
916 SeatKind::Straight => {
917 let seat = planar_seat(model, solid, edge, tol)?;
918 let i = seat_side(&seat, face)?;
919 let sign = if seat.convex { 1.0 } else { -1.0 };
925 let a = seat.leg(i, tol)? * sign;
926 let b = seat.leg(1 - i, tol)? * sign;
927 let inward = -seat.normals[i] * sign;
928 let denominator = angle.sin().mul_add(b.dot(a), angle.cos() * b.dot(inward));
929 if denominator <= tol.angular() {
930 ogeom_bail!(
931 Construction,
932 "the bevel at that angle never meets the edge's other face"
933 );
934 }
935 let derived = distance * angle.sin() / denominator;
936 let mut distances = [0.0; 2];
937 distances[i] = distance;
938 distances[1 - i] = derived;
939 Ok(Legs::Straight(seat, distances))
940 }
941 SeatKind::Marched => Ok(Legs::Marched),
942 SeatKind::Rim(rim) => {
943 let seat = match rim_seat(model, solid, edge, &rim, tol)? {
950 RimSeat::Revolved(seat) => seat,
951 RimSeat::Sphere(seat) => {
952 let on_cap_face = if seat.cap_face.is_same(face) {
955 true
956 } else if seat.wall_face.is_same(face) {
957 false
958 } else {
959 ogeom_bail!(
960 Construction,
961 "the named face does not meet the edge being chamfered"
962 )
963 };
964 let (on_wall, on_cap) =
965 seat.angle_legs(on_cap_face, distance, angle, tol)?;
966 return Ok(Legs::Sphere(seat, on_wall, on_cap));
967 }
968 };
969 let corner = rim_corner(&seat);
970 let past = if seat.slope == 0.0 {
971 angle >= core::f64::consts::FRAC_PI_2 - tol.angular()
972 } else {
973 angle + corner >= core::f64::consts::PI - tol.angular()
974 };
975 if past {
976 ogeom_bail!(
977 Construction,
978 "the bevel at that angle never meets the edge's other face"
979 );
980 }
981 let derived = if seat.slope == 0.0 {
982 distance * angle.tan()
983 } else {
984 distance * angle.sin() / (angle + corner).sin()
985 };
986 let (on_wall, on_cap) =
987 revolved_side(&seat.wall_face, &seat.cap_face, face, distance, derived)?;
988 Ok(Legs::Rim(seat, on_wall, on_cap))
989 }
990 }
991 }
992 }
993}
994
995struct Wedge {
998 faces: Vec<Shape>,
999 solid: Option<Shape>,
1001 additive: bool,
1002 whole_ring: Option<WholeRing>,
1005 marched: Option<Chamfer>,
1008}
1009
1010#[derive(Debug, Clone, Copy)]
1012struct WholeRing {
1013 centre: ogeom_math::Point,
1014 radius: f64,
1015 up: ogeom_math::Vector,
1016 on_wall: f64,
1017 on_cap: f64,
1018}
1019
1020impl WholeRing {
1021 fn same_circle(&self, other: &Self, tol: Tolerances) -> bool {
1023 self.centre.distance(other.centre) <= tol.confusion()
1024 && (self.radius - other.radius).abs() <= tol.confusion()
1025 && self.up.dot(other.up) >= 1.0 - tol.angular()
1026 }
1027
1028 fn same_legs(&self, other: &Self, tol: Tolerances) -> bool {
1030 (self.on_wall - other.on_wall).abs() <= tol.confusion()
1031 && (self.on_cap - other.on_cap).abs() <= tol.confusion()
1032 }
1033}
1034
1035impl Wedge {
1036 fn apply(
1037 self,
1038 model: &mut Model,
1039 solid: &Shape,
1040 edge: &Shape,
1041 tol: Tolerances,
1042 ) -> OgeomResult<Built> {
1043 let mut built = match self.solid {
1044 Some(wedge) => {
1045 let mut result = if self.additive {
1046 ogeom_bool::fuse(model, solid, &wedge, tol)?
1047 } else {
1048 ogeom_bool::cut(model, solid, &wedge, tol)?
1049 };
1050 result.history.delete(edge);
1051 result
1052 }
1053 None => apply_wedge(model, solid, Some(edge), &self.faces, self.additive, tol)?,
1054 };
1055 crate::support::credit_new_faces(model, solid, edge, &mut built)?;
1056 Ok(built)
1057 }
1058}
1059
1060enum SeatKind {
1062 Straight,
1064 Rim(ogeom_geom::CircleCurve),
1066 Marched,
1068}
1069
1070fn seat_kind(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<SeatKind> {
1071 let (curve, _) = edge_curve(model, edge, tol)?;
1072 match curve {
1073 Curve::Line(_) => Ok(SeatKind::Straight),
1074 Curve::Circle(c) => Ok(SeatKind::Rim(c)),
1075 _ => Ok(SeatKind::Marched),
1076 }
1077}
1078
1079fn revolved_side(
1084 wall_face: &Shape,
1085 cap_face: &Shape,
1086 face: &Shape,
1087 on_face: f64,
1088 on_other: f64,
1089) -> OgeomResult<(f64, f64)> {
1090 if wall_face.is_same(face) {
1091 Ok((on_face, on_other))
1092 } else if cap_face.is_same(face) {
1093 Ok((on_other, on_face))
1094 } else {
1095 ogeom_bail!(
1096 Construction,
1097 "the named face does not meet the edge being chamfered"
1098 )
1099 }
1100}
1101
1102fn revolved_bevel(
1106 model: &mut Model,
1107 solid: &Shape,
1108 edge: &Shape,
1109 seat: &RevolvedSeat,
1110 on_wall: f64,
1111 on_cap: f64,
1112 tol: Tolerances,
1113) -> OgeomResult<Wedge> {
1114 for distance in [on_wall, on_cap] {
1115 if !distance.is_finite() || distance <= tol.confusion() {
1116 ogeom_bail!(Construction, "a chamfer of {distance} cuts nothing");
1117 }
1118 }
1119 let cap_rho = seat.sigma.mul_add(-(seat.tau * on_cap), seat.radius);
1120 if cap_rho <= tol.confusion() {
1121 ogeom_bail!(
1122 Construction,
1123 "a chamfer of {on_cap} along the cap swallows the axis of a rim \
1124 of radius {}",
1125 seat.radius
1126 );
1127 }
1128 let depth = wall_depth(seat, on_wall);
1131 let wall_rho = seat.radius_at(-seat.tau * depth);
1132 if wall_rho <= tol.confusion() {
1133 ogeom_bail!(
1134 Construction,
1135 "a chamfer of {on_wall} down the wall runs past the apex of its cone"
1136 );
1137 }
1138 if let Some((start, sweep)) = arc_span(model, edge, seat, tol)? {
1139 let legs = (depth, cap_rho, on_wall.max(on_cap));
1140 return sector_bevel(model, solid, edge, seat, legs, (start, sweep), tol);
1141 }
1142 let flanks = revolved_flanks(model, seat, depth, cap_rho, tol)?;
1143
1144 let bevel_band = {
1152 let slope = (cap_rho - wall_rho) / (seat.tau * depth);
1153 let rows = (
1157 0.0_f64.min(-seat.tau * depth),
1158 0.0_f64.max(-seat.tau * depth),
1159 );
1160 let frame = seat.frame_at(seat.centre, tol)?;
1161 let surface: SurfaceGeometry = if slope.atan().abs() <= tol.angular() {
1162 let pad = 0.1 * depth;
1163 ogeom_geom::CylinderSurface::new(
1164 ogeom_math::Cylinder::new(frame, cap_rho, tol)?,
1165 (rows.0 - pad, rows.1 + pad),
1166 )?
1167 .into()
1168 } else {
1169 let pad = if seat.slope == 0.0 {
1172 0.1 * depth
1173 } else {
1174 let apex = -cap_rho / slope;
1175 let clear = (apex - rows.0).abs().min((apex - rows.1).abs());
1176 (0.1 * depth).min(0.5 * clear)
1177 };
1178 let cone = Cone::new(frame, cap_rho, slope.atan(), tol)?;
1179 ConeSurface::new(cone, (rows.0 - pad, rows.1 + pad))?.into()
1180 };
1181 let band = make_revolution_band(model, &surface, &flanks.wall_ring, &flanks.cap_ring, tol)?;
1182 if seat.sigma * seat.tau > 0.0 {
1183 band.reversed()
1184 } else {
1185 band
1186 }
1187 };
1188
1189 Ok(Wedge {
1190 faces: vec![flanks.wall_band, flanks.annulus, bevel_band],
1191 solid: None,
1192 additive: seat.additive(),
1193 whole_ring: None,
1194 marched: None,
1195 })
1196}
1197
1198fn arc_span(
1203 model: &Model,
1204 edge: &Shape,
1205 seat: &RevolvedSeat,
1206 tol: Tolerances,
1207) -> OgeomResult<Option<(f64, f64)>> {
1208 let (curve, range) = edge_curve(model, edge, tol)?;
1209 let closed = ogeom_algo::edge_vertices(model, edge)?.is_some_and(|(a, b)| a.is_same(&b));
1210 let Curve::Circle(arc) = &curve else {
1211 return Ok(None);
1212 };
1213 if closed || crate::support::runs_whole_turn(model, seat, arc, range, tol)? {
1214 return Ok(None);
1215 }
1216 let frame = seat.frame_at(seat.centre, tol)?;
1217 Ok(Some(arc_window(model, edge, &frame, tol)?))
1218}
1219
1220fn arc_window(
1223 model: &Model,
1224 edge: &Shape,
1225 frame: &Frame,
1226 tol: Tolerances,
1227) -> OgeomResult<(f64, f64)> {
1228 use ogeom_geom::Curve3d as _;
1229 let (curve, range) = edge_curve(model, edge, tol)?;
1230 let centre = frame.origin();
1231 let angle = |t: f64| -> OgeomResult<f64> {
1232 let d = curve.point_at(t, tol)? - centre;
1233 Ok(d.dot(frame.y().vector()).atan2(d.dot(frame.x().vector())))
1234 };
1235 let turn = core::f64::consts::TAU;
1236 let (a, b, m) = (
1237 angle(range.0)?,
1238 angle(range.1)?,
1239 angle(0.5 * (range.0 + range.1))?,
1240 );
1241 let forward = (b - a).rem_euclid(turn);
1244 let start = if (m - a).rem_euclid(turn) <= forward {
1245 a
1246 } else {
1247 b
1248 };
1249 let sweep = if start == a { forward } else { turn - forward };
1250 Ok((start, sweep))
1251}
1252
1253fn sector_bevel(
1267 model: &mut Model,
1268 solid: &Shape,
1269 edge: &Shape,
1270 seat: &RevolvedSeat,
1271 (depth, cap_rho, leg): (f64, f64, f64),
1272 (start, sweep): (f64, f64),
1273 tol: Tolerances,
1274) -> OgeomResult<Wedge> {
1275 let frame = seat.frame_at(seat.centre, tol)?;
1276 let radial_at =
1277 |theta: f64| frame.x().vector() * theta.cos() + frame.y().vector() * theta.sin();
1278 let section_at = |theta: f64| -> [ogeom_math::Point; 3] {
1279 let radial = radial_at(theta);
1280 let rim = seat.centre + radial * seat.radius;
1281 let wall = if seat.slope == 0.0 {
1282 rim - seat.up * (seat.tau * depth)
1283 } else {
1284 let height = -seat.tau * depth;
1285 seat.centre + seat.up * height + radial * seat.radius_at(height)
1286 };
1287 [rim, wall, seat.centre + radial * cap_rho]
1288 };
1289 let mut window = [start, start + sweep];
1290 let mut trims = Vec::new();
1291 let hosts = [&seat.cap_face, &seat.wall_face];
1292 for (slot, sign) in [(0_usize, -1.0), (1, 1.0)] {
1293 let end = window[slot];
1294 let at = seat.centre + radial_at(end) * seat.radius;
1295 let ahead = seat.up.cross(radial_at(end)) * sign;
1296 if !crate::marched::crease_terminates_at(model, solid, edge, hosts, at, tol)? {
1297 continue;
1298 }
1299 let Some(face) = crate::support::oblique_end_face(model, solid, at, ahead, tol)? else {
1300 continue;
1301 };
1302 let step = (leg / seat.radius) / 8.0;
1303 let reach = 4.0 * leg / seat.radius;
1304 let section = |t: f64| Ok(section_at(end + sign * t));
1305 if let Some(past) =
1306 crate::support::run_out_past(model, solid, face, step, reach, §ion, tol)?
1307 {
1308 window[slot] = end + sign * past;
1309 trims.push(face);
1310 }
1311 }
1312 if window[1] - window[0] >= core::f64::consts::TAU - tol.angular() {
1314 window = [start, start + sweep];
1315 trims.clear();
1316 }
1317 let [rim, wall, cap] = section_at(window[0]);
1318 let across = seat.up.cross(radial_at(window[0]));
1319 let profile = crate::support::planar_face(model, &[rim, wall, cap], across, tol)?;
1320 let axis = ogeom_math::Axis::new(seat.centre, frame.z());
1321 let turn = window[1] - window[0];
1322 let mut wedge = ogeom_algo::make_revolution(model, &profile, axis, turn, tol)?.shape;
1323 let span = (seat.radius + leg) * 4.0;
1324 for plane in trims {
1325 wedge = crate::support::trimmed_wedge(model, &wedge, plane, span, tol)?;
1326 }
1327 Ok(Wedge {
1328 faces: Vec::new(),
1329 solid: Some(wedge),
1330 additive: seat.additive(),
1331 whole_ring: None,
1332 marched: None,
1333 })
1334}
1335
1336fn seat_side(seat: &Seat, face: &Shape) -> OgeomResult<usize> {
1338 if seat.faces[0].is_same(face) {
1339 Ok(0)
1340 } else if seat.faces[1].is_same(face) {
1341 Ok(1)
1342 } else {
1343 ogeom_bail!(
1344 Construction,
1345 "the named face does not meet the edge being chamfered"
1346 )
1347 }
1348}
1349
1350fn bevel(
1353 model: &mut Model,
1354 seat: &Seat,
1355 distances: [f64; 2],
1356 tol: Tolerances,
1357) -> OgeomResult<Wedge> {
1358 for distance in distances {
1359 if !distance.is_finite() || distance <= tol.confusion() {
1360 ogeom_bail!(Construction, "a chamfer of {distance} cuts nothing");
1361 }
1362 }
1363 let sign = if seat.convex { 1.0 } else { -1.0 };
1368 let a = seat.leg(0, tol)? * sign;
1369 let b = seat.leg(1, tol)? * sign;
1370 if seat.convex {
1373 for (i, leg) in [a, b].into_iter().enumerate() {
1374 let reach = crate::support::face_reach(model, &seat.faces[i], seat.start, leg, tol)?;
1375 if distances[i] > reach + tol.confusion() {
1376 ogeom_bail!(
1377 Construction,
1378 "a chamfer of {} on the edge from {:?} to {:?} runs past its face, \
1379 which reaches {reach} back from the edge",
1380 distances[i],
1381 seat.start,
1382 seat.end
1383 );
1384 }
1385 }
1386 }
1387
1388 let travel = seat.end - seat.start;
1389 let apex0 = seat.start;
1390 let apex1 = seat.end;
1391 let a0 = apex0 + a * distances[0];
1392 let b0 = apex0 + b * distances[1];
1393 let a1 = a0 + travel;
1394 let b1 = b0 + travel;
1395
1396 let bevel_out = {
1400 let across = b0 - a0;
1401 let mut n = seat.along.cross(across);
1402 let m = n.magnitude();
1403 if m <= tol.confusion() {
1404 ogeom_bail!(Construction, "the chamfer's cut line has no direction");
1405 }
1406 n /= m;
1407 if n.dot(a0 - apex0) < 0.0 {
1408 n = -n;
1409 }
1410 n
1411 };
1412
1413 let faces = [
1420 planar_face(model, &[apex0, a0, b0], -seat.along, tol)?,
1421 planar_face(model, &[apex1, a1, b1], seat.along, tol)?,
1422 planar_face(model, &[apex0, a0, a1, apex1], seat.normals[0] * sign, tol)?,
1423 planar_face(model, &[apex0, b0, b1, apex1], seat.normals[1] * sign, tol)?,
1424 planar_face(model, &[a0, b0, b1, a1], bevel_out, tol)?,
1425 ];
1426 Ok(Wedge {
1427 faces: faces.to_vec(),
1428 solid: None,
1429 additive: !seat.convex,
1430 whole_ring: None,
1431 marched: None,
1432 })
1433}
1434
1435fn faces_holding(model: &Model, solid: &Shape, edge: &Shape) -> Result<Vec<Shape>, Blamed> {
1437 let mut found = Vec::new();
1438 for face in ogeom_topo::explore_unique(model, solid, ogeom_topo::ShapeType::Face)? {
1439 if ogeom_topo::explore_unique(model, &face, ogeom_topo::ShapeType::Edge)?
1440 .iter()
1441 .any(|e| e.is_same(edge))
1442 {
1443 found.push(face);
1444 }
1445 }
1446 Ok(found)
1447}
1448
1449fn marched_step(
1461 model: &mut Model,
1462 so_far: &Built,
1463 edge: &Shape,
1464 spec: &Chamfer,
1465 given: &[Shape],
1466 earlier: &[(Shape, Chamfer)],
1467 tol: Tolerances,
1468) -> OgeomResult<Built> {
1469 let solid = &so_far.shape;
1470 let faces_now = ogeom_topo::explore_unique(model, solid, ogeom_topo::ShapeType::Face)?;
1471 let now = |named: &Shape| -> Shape {
1472 if faces_now.iter().any(|f| f.is_same(named)) {
1473 return named.clone();
1474 }
1475 so_far
1476 .history
1477 .modified(named)
1478 .iter()
1479 .find(|f| faces_now.iter().any(|g| g.is_same(f)))
1480 .cloned()
1481 .unwrap_or_else(|| named.clone())
1482 };
1483 let asked = spec;
1484 let spec = &match spec {
1485 Chamfer::Symmetric(d) => Chamfer::Symmetric(*d),
1486 Chamfer::Distances {
1487 face,
1488 on_face,
1489 on_other,
1490 } => Chamfer::Distances {
1491 face: now(face),
1492 on_face: *on_face,
1493 on_other: *on_other,
1494 },
1495 Chamfer::Angle {
1496 face,
1497 distance,
1498 angle,
1499 } => Chamfer::Angle {
1500 face: now(face),
1501 distance: *distance,
1502 angle: *angle,
1503 },
1504 };
1505 let pieces = match crate::fillet::refind_edges(model, solid, edge, given, tol) {
1507 Ok(pieces) => pieces,
1508 Err(ogeom_core::OgeomError::Construction(_)) => Vec::new(),
1509 Err(e) => return Err(e),
1510 };
1511 if pieces.is_empty() {
1512 let ends = |e: &Shape| -> OgeomResult<Vec<ogeom_topo::TShapeId>> {
1513 Ok(ogeom_algo::edge_vertices(model, e)?
1514 .map(|(a, b)| vec![a.node(), b.node()])
1515 .unwrap_or_default())
1516 };
1517 let own = ends(edge)?;
1518 for (other, theirs) in earlier {
1519 if same_spelling(theirs, asked, tol) && ends(other)?.iter().any(|v| own.contains(v)) {
1520 let mut done = Built::from_nothing(solid.clone());
1521 done.history.delete(edge);
1522 return Ok(done);
1523 }
1524 }
1525 ogeom_bail!(
1526 Construction,
1527 "a bevel on a marched seat whose edge an earlier bevel of the request, \
1528 of another spelling, took away is not built"
1529 );
1530 }
1531 let mut built = Built::from_nothing(solid.clone());
1532 for piece in pieces {
1533 let mut step =
1534 crate::chamfer_marched::marched_bevel(model, &built.shape, &piece, spec, given, tol)?;
1535 crate::support::credit_new_faces(model, &built.shape, &piece, &mut step)?;
1536 step.history.delete(edge);
1537 built = Built {
1538 shape: step.shape.clone(),
1539 history: built.history.then(&step.history),
1540 };
1541 }
1542 Ok(built)
1543}
1544
1545fn same_spelling(a: &Chamfer, b: &Chamfer, tol: Tolerances) -> bool {
1548 let near = |x: f64, y: f64| (x - y).abs() <= tol.confusion();
1549 match (a, b) {
1550 (Chamfer::Symmetric(x), Chamfer::Symmetric(y)) => near(*x, *y),
1551 (
1552 Chamfer::Distances {
1553 face: fa,
1554 on_face: oa,
1555 on_other: ra,
1556 },
1557 Chamfer::Distances {
1558 face: fb,
1559 on_face: ob,
1560 on_other: rb,
1561 },
1562 ) => fa.is_same(fb) && near(*oa, *ob) && near(*ra, *rb),
1563 (
1564 Chamfer::Angle {
1565 face: fa,
1566 distance: da,
1567 angle: aa,
1568 },
1569 Chamfer::Angle {
1570 face: fb,
1571 distance: db,
1572 angle: ab,
1573 },
1574 ) => fa.is_same(fb) && near(*da, *db) && (aa - ab).abs() <= tol.angular(),
1575 _ => false,
1576 }
1577}