1use crate::asym::Section;
15use crate::law::{Profile, RadiusLaw};
16use crate::support::{
17 Seat, apply_wedge, apply_wedge_solid, edge_curve, face_from_edges, planar_face, planar_seat,
18 revolved_flanks, revolved_seat, segment_between, trimmed_wedge, wedge_solid,
19};
20use ogeom_algo::{Built, make_edge_between, make_revolution_band, make_vertex};
21use ogeom_core::{OgeomError, OgeomResult, Tolerances, ogeom_bail};
22use ogeom_geom::{
23 CircleCurve, Curve, CylinderSurface, PlaneSurface, SurfaceGeometry, TorusSurface,
24};
25use ogeom_math::{Circle, Cylinder, Direction, Frame, Plane, Point, Torus, Vector};
26use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
27
28pub fn fillet_edge(
78 model: &mut Model,
79 solid: &Shape,
80 edge: &Shape,
81 radius: f64,
82 tol: Tolerances,
83) -> OgeomResult<Built> {
84 ogeom_algo::on_own_nodes(
85 model,
86 solid,
87 std::slice::from_ref(edge),
88 |model, solid, parts| {
89 cut_off_bands(
90 model,
91 solid,
92 |model| fillet_edge_own(model, solid, &parts[0], radius, tol),
93 tol,
94 )
95 },
96 )
97}
98
99fn cut_off_bands(
105 model: &mut Model,
106 solid: &Shape,
107 build: impl FnOnce(&mut Model) -> OgeomResult<Built>,
108 tol: Tolerances,
109) -> OgeomResult<Built> {
110 let (built, cuts) = crate::support::with_cut_offs(|| build(model));
111 let mut built = built?;
112 for cut in cuts {
113 let reach = 2.0 * cut.radius;
114 let (mut lo, mut hi) = (cut.points[0], cut.points[0]);
115 for p in &cut.points {
116 lo = Point::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
117 hi = Point::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
118 }
119 let (lo, hi) = (
120 lo - Vector::new(reach, reach, reach),
121 hi + Vector::new(reach, reach, reach),
122 );
123 let walls = [
124 (cut.normal, cut.origin),
125 (Vector::new(1.0, 0.0, 0.0), lo),
126 (Vector::new(0.0, 1.0, 0.0), lo),
127 (Vector::new(0.0, 0.0, 1.0), lo),
128 (Vector::new(-1.0, 0.0, 0.0), hi),
129 (Vector::new(0.0, -1.0, 0.0), hi),
130 (Vector::new(0.0, 0.0, -1.0), hi),
131 ];
132 let tool = crate::corner::convex_block(model, &walls, tol)?;
133 let shared = ogeom_bool::common(model, solid, &tool, tol)?.shape;
136 let held = if explore_unique(model, &shared, ShapeType::Face)?.is_empty() {
137 0.0
138 } else {
139 ogeom_algo::volume_properties(model, &shared, ogeom_mesh::Deflection::default(), tol)?
140 .mass
141 .abs()
142 };
143 if held > tol.confusion() {
144 ogeom_bail!(
145 Construction,
146 "a concave blend of radius {} runs past the side of the face it rolls on \
147 where the solid stands beyond that side, so its fill cannot be cut off \
148 there; see docs/CAPABILITIES.md, fillet.edge-blends",
149 cut.radius
150 );
151 }
152 let trimmed = ogeom_bool::cut(model, &built.shape, &tool, tol)?;
153 built = Built::new(trimmed.shape, built.history.then(&trimmed.history));
154 }
155 Ok(built)
156}
157
158fn fillet_edge_own(
160 model: &mut Model,
161 solid: &Shape,
162 edge: &Shape,
163 radius: f64,
164 tol: Tolerances,
165) -> OgeomResult<Built> {
166 if !radius.is_finite() || radius <= tol.confusion() {
167 ogeom_bail!(Construction, "a fillet of radius {radius} rounds nothing");
168 }
169 if let Err(refusal) = crate::support::ball_fits(model, solid, edge, radius, false, tol) {
175 return consuming(model, solid, edge, radius, refusal, tol);
176 }
177 let edges = std::slice::from_ref(edge);
178 or_rim_consumed(
179 model,
180 solid,
181 edge,
182 radius,
183 |model| {
184 with_fitted_rims(
185 model,
186 solid,
187 edges,
188 |model, rims| fillet_edge_meeting(model, solid, edge, radius, None, rims, tol),
189 tol,
190 )
191 },
192 tol,
193 )
194}
195
196fn consuming(
199 model: &mut Model,
200 solid: &Shape,
201 edge: &Shape,
202 radius: f64,
203 refusal: OgeomError,
204 tol: Tolerances,
205) -> OgeomResult<Built> {
206 if !matches!(refusal, OgeomError::Construction(_)) {
207 return Err(refusal);
208 }
209 let Some(seat) = crate::consumed::consumed_seat(model, solid, edge, radius, tol)? else {
210 let rolled = match crate::rolled::rolled_fillet(model, solid, edge, radius, tol)? {
211 Some(built) => Some(built),
212 None => crate::consumed::consumed_slanted(model, solid, edge, radius, tol)?,
213 };
214 let Some(mut built) = rolled else {
215 return match crate::consumed_rim::consumed_rim(model, solid, edge, radius, tol)? {
216 Some(rim) => rim_consuming(model, solid, edge, rim, radius, tol),
217 None => crate::consumed_marched::consumed_marched(model, solid, edge, radius, tol)?
218 .ok_or(refusal),
219 };
220 };
221 crate::support::credit_new_faces(model, solid, edge, &mut built)?;
222 return Ok(built);
223 };
224 let mut built = seated_fillet(model, solid, &seat, radius, Some(edge), &[], tol)?;
225 crate::support::credit_new_faces(model, solid, edge, &mut built)?;
226 Ok(built)
227}
228
229fn rim_consuming(
232 model: &mut Model,
233 solid: &Shape,
234 edge: &Shape,
235 rim: crate::support::RevolvedSeat,
236 radius: f64,
237 tol: Tolerances,
238) -> OgeomResult<Built> {
239 let mut built = revolved_fillet_on(model, solid, edge, rim, radius, tol)?;
240 crate::support::credit_new_faces(model, solid, edge, &mut built)?;
241 Ok(built)
242}
243
244fn or_rim_consumed(
251 model: &mut Model,
252 solid: &Shape,
253 edge: &Shape,
254 radius: f64,
255 round: impl FnOnce(&mut Model) -> OgeomResult<Built>,
256 tol: Tolerances,
257) -> OgeomResult<Built> {
258 let mark = model.note_widened();
259 match round(model) {
260 Err(refusal @ OgeomError::Construction(_)) => {
261 match crate::consumed_rim::consumed_rim(model, solid, edge, radius, tol)? {
262 Some(rim) => {
263 model.undo_widened(mark);
264 rim_consuming(model, solid, edge, rim, radius, tol)
265 }
266 None => {
267 model.keep_widened(mark);
270 crate::consumed_marched::consumed_marched(model, solid, edge, radius, tol)?
271 .ok_or(refusal)
272 }
273 }
274 }
275 Err(refusal @ OgeomError::NotDone(_)) => {
278 model.keep_widened(mark);
279 crate::consumed_marched::consumed_marched(model, solid, edge, radius, tol)?
280 .ok_or(refusal)
281 }
282 other => {
283 model.keep_widened(mark);
284 other
285 }
286 }
287}
288
289#[derive(Debug, Clone, Copy)]
293pub(crate) struct Mate {
294 pub ends: [(Point, Vector); 2],
296 pub settled: [bool; 2],
300 pub meets: [bool; 2],
304 pub alone: [bool; 2],
308}
309
310impl Mate {
311 pub(crate) fn settled_at(mates: &[Self], index: usize, at: Point, tol: Tolerances) -> bool {
321 let other = mates.iter().any(|mate| {
322 mate.ends
323 .iter()
324 .zip(mate.settled)
325 .any(|((p, _), settled)| settled && p.distance(at) <= tol.confusion() * 1e3)
326 });
327 let own = mates.get(index).is_some_and(|mate| {
328 let nearer = usize::from(mate.ends[1].0.distance(at) < mate.ends[0].0.distance(at));
329 mate.settled[nearer]
330 });
331 other || own
332 }
333
334 pub(crate) fn alone_at(mates: &[Self], index: usize, at: Point, tol: Tolerances) -> bool {
338 let near = |p: &Point| p.distance(at) <= tol.confusion() * 1e3;
339 let shared = mates
340 .iter()
341 .enumerate()
342 .any(|(i, mate)| i != index && mate.ends.iter().any(|(p, _)| near(p)));
343 let crossed = mates.get(index).is_some_and(|mate| {
344 mate.ends
345 .iter()
346 .zip(mate.alone)
347 .any(|((p, _), alone)| alone && near(p))
348 });
349 !shared || crossed
350 }
351
352 pub(crate) fn meets_at(mates: &[Self], index: usize, at: Point, tol: Tolerances) -> bool {
355 mates.get(index).is_some_and(|mate| {
356 mate.ends
357 .iter()
358 .zip(mate.meets)
359 .any(|((p, _), meets)| meets && p.distance(at) <= tol.confusion() * 1e3)
360 })
361 }
362}
363
364fn fillet_edge_meeting(
377 model: &mut Model,
378 solid: &Shape,
379 edge: &Shape,
380 radius: f64,
381 mates: Option<(usize, &[Mate])>,
382 rims: Rims,
383 tol: Tolerances,
384) -> OgeomResult<Built> {
385 let mut built = fillet_edge_seat(model, solid, edge, radius, mates, rims, tol)?;
386 crate::support::credit_new_faces(model, solid, edge, &mut built)?;
387 Ok(built)
388}
389
390fn fillet_edge_seat(
393 model: &mut Model,
394 solid: &Shape,
395 edge: &Shape,
396 radius: f64,
397 mates: Option<(usize, &[Mate])>,
398 rims: Rims,
399 tol: Tolerances,
400) -> OgeomResult<Built> {
401 if !radius.is_finite() || radius <= tol.confusion() {
402 ogeom_bail!(Construction, "a fillet of radius {radius} rounds nothing");
403 }
404 match route(model, solid, edge, rims, tol)? {
405 Route::Planar => planar_fillet(model, solid, edge, Section::Ball(radius), mates, tol),
406 Route::Ruled(seat) => {
407 crate::ruled::ruled_fillet(model, solid, edge, radius, mates, seat, tol)
408 }
409 Route::Revolved(_, _, seat)
412 if seat.slope != 0.0 && seat.ball(radius).rho - radius <= tol.confusion() =>
413 {
414 crate::marched::marched_fillet(model, solid, edge, radius, mates, tol)
415 }
416 Route::Revolved(c, crange, seat) => {
417 let closed =
418 ogeom_algo::edge_vertices(model, edge)?.is_some_and(|(a, b)| a.is_same(&b));
419 if closed {
420 return revolved_fillet(model, solid, edge, &c, radius, tol);
421 }
422 let whole_turn = crate::support::runs_whole_turn(model, &seat, &c, crange, tol)?;
423 if whole_turn {
424 revolved_fillet(model, solid, edge, &c, radius, tol)
425 } else {
426 revolved_arc_fillet(model, solid, edge, &c, radius, None, mates, tol)
427 }
428 }
429 Route::Marched => crate::marched::marched_fillet(model, solid, edge, radius, mates, tol),
430 }
431}
432
433#[allow(clippy::too_many_arguments, reason = "one question, all its data")]
444fn rim_end_stops_short(
445 model: &Model,
446 solid: &Shape,
447 edge: &Shape,
448 seat: &crate::support::RevolvedSeat,
449 mates: Option<(usize, &[Mate])>,
450 at: Point,
451 ahead: Vector,
452 tol: Tolerances,
453) -> OgeomResult<Option<(Point, Vector)>> {
454 let hosts = [&seat.cap_face, &seat.wall_face];
455 let met = mates.is_some_and(|(index, mates)| {
456 mates.iter().enumerate().any(|(i, mate)| {
457 i != index
458 && mate
459 .ends
460 .iter()
461 .any(|(p, _)| p.distance(at) <= tol.confusion() * 1e3)
462 })
463 });
464 if met || !crate::marched::crease_terminates_at(model, solid, edge, hosts, at, tol)? {
465 return Ok(None);
466 }
467 for face in explore_unique(model, solid, ShapeType::Face)? {
468 if hosts.iter().any(|h| h.is_same(&face)) {
469 continue;
470 }
471 let Some(data) = model.node(&face).and_then(|n| n.data().as_face()) else {
472 continue;
473 };
474 let Some(SurfaceGeometry::Plane(plane)) = model.geometry().surface(data.surface) else {
475 continue;
476 };
477 use ogeom_geom::Transformable as _;
478 let SurfaceGeometry::Plane(placed) =
479 SurfaceGeometry::Plane(*plane).transformed(&face.transform(model.datums())?, tol)?
480 else {
481 continue;
482 };
483 let plane = placed.plane();
484 if plane.signed_distance_to(at).abs() > tol.confusion() * 10.0 {
485 continue;
486 }
487 let meets = explore_unique(model, &face, ShapeType::Vertex)?
488 .iter()
489 .any(|v| {
490 model
491 .node(v)
492 .and_then(|n| n.data().as_vertex())
493 .zip(v.transform(model.datums()).ok())
494 .is_some_and(|(d, place)| {
495 place.apply(d.point).distance(at) <= tol.confusion() * 1e3
496 })
497 });
498 let normal = plane.normal().vector();
499 let lean = normal.dot(ahead).abs();
500 if meets && lean > 1e-6 && lean < 1.0 - 1e-6 {
501 let back = if normal.dot(ahead) > 0.0 {
503 -normal
504 } else {
505 normal
506 };
507 return Ok(Some((plane.origin(), back)));
508 }
509 }
510 Ok(None)
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515enum Rims {
516 Exact,
518 Fitted,
520}
521
522enum Route {
525 Planar,
527 Ruled(crate::ruled::RuledSeat),
529 Revolved(CircleCurve, (f64, f64), crate::support::RevolvedSeat),
532 Marched,
534}
535
536fn route(
541 model: &Model,
542 solid: &Shape,
543 edge: &Shape,
544 rims: Rims,
545 tol: Tolerances,
546) -> OgeomResult<Route> {
547 let (curve, crange) = edge_curve(model, edge, tol)?;
548 Ok(match curve {
549 Curve::Line(_) if crate::support::hosts_planar(model, solid, edge, tol)? => Route::Planar,
550 Curve::Line(_) => match crate::ruled::ruled_seat(model, solid, edge, tol)? {
551 Some(seat) => Route::Ruled(seat),
552 None => Route::Marched,
553 },
554 Curve::Circle(c) => match revolved_seat(model, solid, edge, &c, tol) {
555 Ok(seat) if seat.slope != 0.0 && rims == Rims::Fitted => Route::Marched,
556 Ok(seat) => Route::Revolved(c, crange, seat),
557 Err(OgeomError::Construction(_)) => Route::Marched,
558 Err(e) => return Err(e),
559 },
560 _ => Route::Marched,
561 })
562}
563
564pub fn fillet_edges(
666 model: &mut Model,
667 solid: &Shape,
668 edges: &[Shape],
669 radius: f64,
670 tol: Tolerances,
671) -> OgeomResult<Built> {
672 let asked: Vec<(Shape, f64)> = edges.iter().map(|e| (e.clone(), radius)).collect();
673 if let Some(built) = rounds_by_engine(model, solid, &asked, tol)? {
674 return Ok(built);
675 }
676 let radii = vec![radius; edges.len()];
677 ogeom_algo::on_own_nodes(model, solid, edges, |model, solid, parts| {
678 cut_off_bands(
679 model,
680 solid,
681 |model| fillet_edges_meeting(model, solid, parts, &radii, tol),
682 tol,
683 )
684 })
685}
686
687pub fn fillet_edges_with(
713 model: &mut Model,
714 solid: &Shape,
715 edges: &[(Shape, f64)],
716 tol: Tolerances,
717) -> OgeomResult<Built> {
718 let shapes: Vec<Shape> = edges.iter().map(|(edge, _)| edge.clone()).collect();
719 let radii: Vec<f64> = edges.iter().map(|&(_, radius)| radius).collect();
720 for (i, (edge, radius)) in edges.iter().enumerate() {
721 if !radius.is_finite() || *radius <= tol.confusion() {
722 ogeom_bail!(Construction, "a fillet of radius {radius} rounds nothing");
723 }
724 if edges[..i]
725 .iter()
726 .any(|(other, r)| other.is_same(edge) && r != radius)
727 {
728 ogeom_bail!(
729 Construction,
730 "an edge asked at two radii, {radius} and another, rounds neither"
731 );
732 }
733 }
734 if let Some(built) = rounds_by_engine(model, solid, edges, tol)? {
735 return Ok(built);
736 }
737 ogeom_algo::on_own_nodes(model, solid, &shapes, |model, solid, parts| {
738 cut_off_bands(
739 model,
740 solid,
741 |model| fillet_edges_meeting(model, solid, parts, &radii, tol),
742 tol,
743 )
744 })
745}
746
747fn rounds_by_engine(
752 model: &mut Model,
753 solid: &Shape,
754 edges: &[(Shape, f64)],
755 tol: Tolerances,
756) -> OgeomResult<Option<Built>> {
757 let mut once: Vec<(Shape, f64)> = Vec::with_capacity(edges.len());
758 for (edge, radius) in edges {
759 if !once.iter().any(|(e, _)| e.is_same(edge)) {
760 once.push((edge.clone(), *radius));
761 }
762 }
763 if once.is_empty()
766 || once
767 .iter()
768 .any(|(_, r)| !(r.is_finite() && *r > tol.confusion()))
769 {
770 return Ok(None);
771 }
772 let request = crate::engine::Request::from_rounds(&once, tol)?;
773 crate::engine::run(model, solid, &request, tol)
774}
775
776fn fillet_edges_meeting(
780 model: &mut Model,
781 solid: &Shape,
782 edges: &[Shape],
783 radii: &[f64],
784 tol: Tolerances,
785) -> OgeomResult<Built> {
786 let request: Vec<(Shape, f64)> = edges.iter().cloned().zip(radii.iter().copied()).collect();
790 let signed = crate::setback::signed_corners(model, solid, &request, tol)?;
791 let cusps = crate::setback::cusp_corners(model, solid, &request, tol)?;
792 let (edges, radii): (Vec<Shape>, Vec<f64>) = if signed.is_empty() && cusps.is_empty() {
793 (edges.to_vec(), radii.to_vec())
794 } else {
795 let mut sorted = request.clone();
796 sorted.sort_by_key(|(edge, _)| edge.node());
797 sorted.into_iter().unzip()
798 };
799 let (edges, radii) = (edges.as_slice(), radii.as_slice());
800 let round = |model: &mut Model| {
801 with_fitted_rims(
802 model,
803 solid,
804 edges,
805 |model, rims| fillet_edges_meeting_with(model, solid, edges, radii, rims, tol),
806 tol,
807 )
808 };
809 let mut mixed = mixed_corners(model, edges, radii)?;
810 mixed.retain(|(vertex, _)| {
811 !signed.iter().any(|c| c.vertex.is_same(vertex))
812 && !cusps.iter().any(|c| c.vertex.is_same(vertex))
813 });
814 let mut built = match (edges, radii) {
817 ([edge], [radius]) => or_rim_consumed(model, solid, edge, *radius, round, tol)?,
818 _ => round(model)?,
819 };
820 for (vertex, request) in &mixed {
821 built = crate::setback::close_mixed_corner(model, solid, built, vertex, request, tol)?;
822 }
823 for corner in &signed {
824 built = crate::setback::close_signed_corner(model, solid, built, corner, tol)?;
825 }
826 for corner in &cusps {
827 built = crate::cusp::close_cusp(model, solid, built, corner, tol)?;
828 }
829 Ok(built)
830}
831
832type Corner = (Shape, Vec<(Shape, f64)>);
834
835fn mixed_corners(model: &Model, edges: &[Shape], radii: &[f64]) -> OgeomResult<Vec<Corner>> {
838 let mut corners: Vec<Corner> = Vec::new();
839 for (edge, &radius) in edges.iter().zip(radii) {
840 let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? else {
841 continue;
842 };
843 for v in [a, b] {
844 if let Some(slot) = corners.iter_mut().find(|(held, _)| held.is_same(&v)) {
845 slot.1.push((edge.clone(), radius));
846 } else {
847 corners.push((v, vec![(edge.clone(), radius)]));
848 }
849 }
850 }
851 corners.retain(|(_, at)| at.len() >= 3 && at.iter().any(|&(_, r)| r != at[0].1));
852 Ok(corners)
853}
854
855fn with_fitted_rims(
862 model: &mut Model,
863 solid: &Shape,
864 edges: &[Shape],
865 mut round: impl FnMut(&mut Model, Rims) -> OgeomResult<Built>,
866 tol: Tolerances,
867) -> OgeomResult<Built> {
868 let mark = model.note_widened();
869 let exact = round(model, Rims::Exact);
870 if matches!(
871 exact,
872 Ok(_) | Err(OgeomError::Cancelled | OgeomError::Dangling(_))
873 ) || !has_cone_rim(model, solid, edges, tol)?
874 {
875 model.keep_widened(mark);
876 return exact;
877 }
878 model.undo_widened(mark);
879 round(model, Rims::Fitted)
880}
881
882fn has_cone_rim(
885 model: &Model,
886 solid: &Shape,
887 edges: &[Shape],
888 tol: Tolerances,
889) -> OgeomResult<bool> {
890 for edge in edges {
891 if let Route::Revolved(_, _, seat) = route(model, solid, edge, Rims::Exact, tol)?
892 && seat.slope != 0.0
893 {
894 return Ok(true);
895 }
896 }
897 Ok(false)
898}
899
900fn fillet_edges_meeting_with(
907 model: &mut Model,
908 solid: &Shape,
909 edges: &[Shape],
910 radii: &[f64],
911 rims: Rims,
912 tol: Tolerances,
913) -> OgeomResult<Built> {
914 let mut ends: Vec<Shape> = Vec::new();
915 let mut shared = false;
916 for edge in edges {
917 if let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? {
918 for v in [a, b] {
919 if ends.iter().any(|w| w.is_same(&v)) {
920 shared = true;
921 }
922 ends.push(v);
923 }
924 }
925 }
926 if !shared {
927 return fillet_edges_own(model, solid, edges, radii, None, rims, None, tol);
928 }
929 let mark = model.note_widened();
934 for crossed in [true, false] {
938 let mut taken = Taken {
939 crossed,
940 ..Taken::default()
941 };
942 match fillet_edges_own(
943 model,
944 solid,
945 edges,
946 radii,
947 Some(&mut taken),
948 rims,
949 None,
950 tol,
951 ) {
952 Ok(built) => {
953 model.keep_widened(mark);
954 return Ok(built);
955 }
956 Err(e @ (OgeomError::Cancelled | OgeomError::Dangling(_))) => {
960 model.keep_widened(mark);
961 return Err(e);
962 }
963 Err(e) if !taken.any => {
966 model.keep_widened(mark);
967 return Err(e);
968 }
969 Err(_) => {
970 model.undo_widened(mark);
971 for (id, surface) in taken.surfaces {
972 if let Some(held) = model.geometry_mut().surface_mut(id) {
973 *held = surface;
974 }
975 }
976 if !taken.any_crossed {
979 break;
980 }
981 }
982 }
983 }
984 fillet_edges_own(model, solid, edges, radii, None, rims, None, tol)
985}
986
987pub(crate) fn request_fits(
998 model: &Model,
999 solid: &Shape,
1000 edges: &[Shape],
1001 radii: &[f64],
1002 tol: Tolerances,
1003) -> Result<Vec<bool>, crate::support::Blamed> {
1004 use crate::support::Blamed;
1005 let blame = |i: usize| {
1006 move |error: OgeomError| Blamed {
1007 edges: vec![i],
1008 error,
1009 }
1010 };
1011 let mut contacts = Vec::with_capacity(edges.len());
1015 let mut touched: Vec<(Vec<ogeom_topo::TShapeId>, Vec<ogeom_topo::TShapeId>)> =
1019 Vec::with_capacity(edges.len());
1020 for edge in edges {
1021 let faces = ogeom_topo::ancestors_of(model, solid, edge, ShapeType::Face)?
1022 .iter()
1023 .map(Shape::node)
1024 .collect();
1025 let ends = ogeom_algo::edge_vertices(model, edge)?
1026 .map(|(a, b)| vec![a.node(), b.node()])
1027 .unwrap_or_default();
1028 touched.push((faces, ends));
1029 }
1030 let alone = |i: usize| {
1031 let (faces, ends) = &touched[i];
1032 touched.iter().enumerate().all(|(j, (f, e))| {
1033 j == i || (!f.iter().any(|x| faces.contains(x)) && !e.iter().any(|x| ends.contains(x)))
1034 })
1035 };
1036 let mut rolled = vec![false; edges.len()];
1039 crate::support::keeping_face_meshes(|| -> Result<(), Blamed> {
1040 for (i, edge) in edges.iter().enumerate() {
1043 match crate::support::ball_fits(model, solid, edge, radii[i], !alone(i), tol) {
1044 Ok(found) => contacts.push(found),
1045 Err(refusal @ OgeomError::Construction(_)) if edges.len() > 1 && alone(i) => {
1046 if crate::rolled::rolled_run(model, solid, edge, radii[i], tol)
1047 .map_err(blame(i))?
1048 .is_none()
1049 {
1050 return Err(blame(i)(refusal));
1051 }
1052 rolled[i] = true;
1053 contacts.push(Vec::new());
1054 }
1055 Err(other) => return Err(blame(i)(other)),
1056 }
1057 }
1058 let reach =
1065 |found: &[crate::support::Contact]| found.iter().map(|c| c.setback).reduce(f64::max);
1066 let mut boxes = Vec::with_capacity(edges.len());
1067 for edge in edges {
1068 boxes.push(ogeom_algo::shape_bounds(model, edge, tol)?);
1069 }
1070 let reaches: Vec<Option<f64>> = contacts.iter().map(|found| reach(found)).collect();
1071 for (i, edge) in edges.iter().enumerate() {
1072 let Some(own) = reaches[i].filter(|_| alone(i)) else {
1073 continue;
1074 };
1075 let near = reaches.iter().enumerate().any(|(j, other)| {
1076 j != i
1077 && other.is_some_and(|other| boxes[i].distance_to_box(&boxes[j]) < own + other)
1078 });
1079 if near {
1080 contacts[i] = crate::support::ball_fits(model, solid, edge, radii[i], true, tol)
1081 .map_err(blame(i))?;
1082 }
1083 }
1084 Ok(())
1085 })?;
1086 crate::support::bands_clear(model, edges, &contacts, tol)?;
1087 Ok(rolled)
1088}
1089
1090#[derive(Default)]
1094struct Taken {
1095 crossed: bool,
1096 any: bool,
1097 any_crossed: bool,
1098 surfaces: Vec<(ogeom_topo::SurfaceId, SurfaceGeometry)>,
1099}
1100
1101#[allow(clippy::too_many_arguments, reason = "one request's settings")]
1104fn fillet_edges_own(
1105 model: &mut Model,
1106 solid: &Shape,
1107 edges: &[Shape],
1108 radii: &[f64],
1109 meeting: Option<&mut Taken>,
1110 cones: Rims,
1111 laws: Option<&[Profile]>,
1112 tol: Tolerances,
1113) -> OgeomResult<Built> {
1114 if edges.is_empty() {
1115 ogeom_bail!(Construction, "a chain of no edges rounds nothing");
1116 }
1117 let rolled = match request_fits(model, solid, edges, radii, tol) {
1118 Ok(rolled) => rolled,
1119 Err(crate::support::Blamed { error: refusal, .. }) if laws.is_some() => {
1120 return Err(refusal);
1121 }
1122 Err(crate::support::Blamed { error: refusal, .. }) => {
1125 return match edges {
1126 [edge] if meeting.is_none() => {
1127 consuming(model, solid, edge, radii[0], refusal, tol)
1128 }
1129 _ => Err(refusal),
1130 };
1131 }
1132 };
1133 use ogeom_geom::Curve3d as _;
1134 let mut corners: Vec<(Shape, Vec<f64>)> = Vec::new();
1138 for (edge, &radius) in edges.iter().zip(radii) {
1139 let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? else {
1140 continue;
1141 };
1142 for v in [a, b] {
1143 if let Some(slot) = corners.iter_mut().find(|(held, _)| held.is_same(&v)) {
1144 slot.1.push(radius);
1145 } else {
1146 corners.push((v, vec![radius]));
1147 }
1148 }
1149 }
1150 corners.retain(|(_, at)| at.len() >= 3);
1151 if laws.is_some() && (!corners.is_empty() || rolled.iter().any(|&r| r)) {
1152 ogeom_bail!(
1153 Construction,
1154 "a variable radius over a chain three of whose edges meet at a \
1155 vertex, or one of whose balls rests on a narrow face's far edge, \
1156 is still owed; see docs/CAPABILITIES.md, fillet.variable"
1157 );
1158 }
1159 corners.retain(|(_, at)| at.iter().all(|&r| r == at[0]));
1164 let round =
1167 |model: &mut Model, built: Built, vertex: &Shape, radius: f64| -> OgeomResult<Built> {
1168 match crate::corner::round_vertex_with(model, &built.shape, vertex, radius, None, tol) {
1169 Ok(rounded) => Ok(Built {
1170 shape: rounded.shape,
1171 history: built.history.then(&rounded.history),
1172 }),
1173 Err(OgeomError::Construction(_)) => Ok(built),
1174 Err(other) => Err(other),
1175 }
1176 };
1177 let mut built: Option<Built> = None;
1182 let mut rounded: Vec<Point> = Vec::new();
1183 let together = if corners.len() > 1 && corners.iter().all(|(_, at)| at[0] == corners[0].1[0]) {
1185 let vertices: Vec<Shape> = corners.iter().map(|(v, _)| v.clone()).collect();
1186 crate::corner::round_vertices_together(model, solid, &vertices, corners[0].1[0], tol)?
1187 } else {
1188 None
1189 };
1190 let one_by_one: &[(Shape, Vec<f64>)] = if together.is_some() { &[] } else { &corners };
1191 if let Some(all) = together {
1192 for (vertex, _) in &corners {
1193 if let Some(point) = model
1194 .node(vertex)
1195 .and_then(|n| n.data().as_vertex())
1196 .map(|d| d.point)
1197 {
1198 rounded.push(vertex.transform(model.datums())?.apply(point));
1199 }
1200 }
1201 built = Some(all);
1202 }
1203 for (vertex, at) in one_by_one {
1204 let start = built
1205 .take()
1206 .unwrap_or_else(|| Built::from_nothing(solid.clone()));
1207 let before = start.shape.clone();
1208 let after = round(model, start, vertex, at[0])?;
1209 if !after.shape.is_same(&before)
1210 && let Some(point) = model
1211 .node(vertex)
1212 .and_then(|n| n.data().as_vertex())
1213 .map(|d| d.point)
1214 {
1215 rounded.push(vertex.transform(model.datums())?.apply(point));
1216 }
1217 built = Some(after);
1218 }
1219 let mut mates: Vec<Mate> = edges
1220 .iter()
1221 .map(|e| -> OgeomResult<Mate> {
1222 let (curve, range) = edge_curve(model, e, tol)?;
1223 let end_of = |t: f64, leaving: f64| -> OgeomResult<(Point, Vector)> {
1224 let d = curve.d1_at(t, tol)?;
1225 let m = d.magnitude();
1226 let unit = if m > tol.confusion() {
1227 d / m
1228 } else {
1229 Vector::ZERO
1230 };
1231 Ok((curve.point_at(t, tol)?, unit * leaving))
1232 };
1233 let ends = [end_of(range.0, 1.0)?, end_of(range.1, -1.0)?];
1234 let settled = ends.map(|(p, _)| {
1235 rounded
1236 .iter()
1237 .any(|q| q.distance(p) <= tol.confusion() * 1e3)
1238 });
1239 Ok(Mate {
1240 ends,
1241 settled,
1242 meets: [false, false],
1243 alone: [false, false],
1244 })
1245 })
1246 .collect::<OgeomResult<_>>()?;
1247 let mut round_rims: Vec<ogeom_math::Circle> = Vec::new();
1254 let on_round_rim = |model: &Model, edge: &Shape, rims: &[ogeom_math::Circle]| {
1255 rim_circle(model, edge, tol).is_some_and(|c| rims.iter().any(|r| same_circle(r, &c, tol)))
1256 };
1257 let mut host_faces: ogeom_core::FastMap<ogeom_topo::TShapeId, Vec<Shape>> =
1263 ogeom_core::FastMap::default();
1264 for face in explore_unique(model, solid, ShapeType::Face)? {
1265 for e in explore_unique(model, &face, ShapeType::Edge)? {
1266 let held = host_faces.entry(e.node()).or_default();
1267 if !held.iter().any(|f| f.is_same(&face)) {
1268 held.push(face.clone());
1269 }
1270 }
1271 }
1272 let junctions = if meeting.is_some() {
1277 let given: Vec<Vec<Shape>> = edges
1278 .iter()
1279 .map(|e| host_faces.get(&e.node()).cloned().unwrap_or_default())
1280 .collect();
1281 let mut found = crate::junction::junctions(model, solid, edges, &given, &mates, radii, tol)
1282 .unwrap_or_default();
1283 if meeting.as_ref().is_some_and(|taken| !taken.crossed) {
1284 found.retain(|junction| !junction.crossed);
1285 }
1286 joinable(model, edges, found)?
1287 } else {
1288 Vec::new()
1289 };
1290 if let Some(taken) = meeting
1291 && !junctions.is_empty()
1292 {
1293 taken.any = true;
1294 taken.any_crossed = junctions.iter().any(|junction| junction.crossed);
1295 for face in explore_unique(model, solid, ShapeType::Face)? {
1296 let Some(data) = model.node(&face).and_then(|n| n.data().as_face()) else {
1297 continue;
1298 };
1299 if taken.surfaces.iter().any(|(id, _)| *id == data.surface) {
1300 continue;
1301 }
1302 if let Some(surface) = model.geometry().surface(data.surface) {
1303 taken.surfaces.push((data.surface, surface.clone()));
1304 }
1305 }
1306 }
1307 for junction in &junctions {
1310 for &index in &junction.pair {
1311 let mate = &mut mates[index];
1312 let end = usize::from(
1313 mate.ends[1].0.distance(junction.at) < mate.ends[0].0.distance(junction.at),
1314 );
1315 if junction.reentrant || !junction.crossed {
1316 mate.meets[end] = true;
1317 } else {
1318 mate.alone[end] = true;
1319 }
1320 }
1321 }
1322 let hosts_of = |edge: &Shape| -> Vec<ogeom_topo::TShapeId> {
1323 let mut out: Vec<_> = host_faces
1324 .get(&edge.node())
1325 .map(|faces| faces.iter().map(Shape::node).collect())
1326 .unwrap_or_default();
1327 out.sort_unstable();
1328 out
1329 };
1330 let mut occurrences: Option<(Shape, ogeom_core::FastMap<ogeom_topo::SameKey, Shape>)> = None;
1334 let mut refind =
1335 |model: &Model, on: &Shape, edge: &Shape, hosts: &[Shape]| -> OgeomResult<Vec<Shape>> {
1336 if !occurrences
1337 .as_ref()
1338 .is_some_and(|(held, _)| held.is_same(on))
1339 {
1340 let mut by_name = ogeom_core::FastMap::default();
1341 for e in explore_unique(model, on, ShapeType::Edge)? {
1342 by_name.entry(ogeom_topo::SameKey(e.clone())).or_insert(e);
1343 }
1344 occurrences = Some((on.clone(), by_name));
1345 }
1346 let named = occurrences
1347 .as_ref()
1348 .and_then(|(_, by_name)| by_name.get(&ogeom_topo::SameKey(edge.clone())));
1349 match named {
1350 Some(own) => Ok(vec![own.clone()]),
1351 None => refind_edges(model, on, edge, hosts, tol),
1352 }
1353 };
1354 let mut blended_creases: Vec<Vec<ogeom_topo::TShapeId>> = Vec::new();
1355 let rims: Vec<Option<Circle>> = edges.iter().map(|e| rim_circle(model, e, tol)).collect();
1365 let mut ends_of: Vec<Vec<Shape>> = Vec::with_capacity(edges.len());
1366 for edge in edges {
1367 let mut ends: Vec<Shape> = Vec::new();
1368 if let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? {
1369 for v in [a, b] {
1370 if !ends.iter().any(|held| held.is_same(&v)) {
1371 ends.push(v);
1372 }
1373 }
1374 }
1375 ends_of.push(ends);
1376 }
1377 let meet = |i: usize, j: usize| {
1378 ends_of[i]
1379 .iter()
1380 .any(|v| ends_of[j].iter().any(|w| w.is_same(v)))
1381 && !matches!((&rims[i], &rims[j]), (Some(a), Some(b)) if same_circle(a, b, tol))
1382 && !junctions
1385 .iter()
1386 .any(|junction| junction.slot(i).is_some() && junction.slot(j).is_some())
1387 };
1388 let mut marched: Vec<Option<bool>> = vec![None; edges.len()];
1398 for i in 0..edges.len() {
1399 if (0..edges.len()).any(|j| j != i && meet(i, j)) {
1400 marched[i] = Some(matches!(
1401 route(model, solid, &edges[i], cones, tol)?,
1402 Route::Marched
1403 ));
1404 }
1405 }
1406 let first: Vec<bool> = (0..edges.len())
1407 .map(|i| {
1408 marched[i] == Some(true)
1409 && (0..edges.len()).any(|j| meet(i, j) && marched[j] == Some(false))
1410 })
1411 .collect();
1412 let mut taken: Vec<usize> = (0..edges.len()).collect();
1413 taken.sort_by_key(|&i| (!first[i], i));
1414 let mut group: Vec<usize> = (0..edges.len()).collect();
1418 for junction in &junctions {
1419 let [a, b] = junction.pair;
1420 let (from, to) = (group[a], group[b]);
1421 for g in &mut group {
1422 if *g == from {
1423 *g = to;
1424 }
1425 }
1426 }
1427 let mut round_of: Vec<Option<usize>> = vec![None; edges.len()];
1428 for &i in &taken {
1429 if round_of[i].is_some() {
1430 continue;
1431 }
1432 let members: Vec<usize> = (0..edges.len()).filter(|&m| group[m] == group[i]).collect();
1433 let mut round = 0;
1434 while members
1435 .iter()
1436 .any(|&m| (0..edges.len()).any(|j| round_of[j] == Some(round) && meet(m, j)))
1437 {
1438 round += 1;
1439 }
1440 for m in members {
1441 round_of[m] = Some(round);
1442 }
1443 }
1444 let round_of: Vec<usize> = round_of.into_iter().map(|r| r.unwrap_or(0)).collect();
1445 let rank: Vec<usize> = {
1446 let mut rank = vec![0; edges.len()];
1447 for (k, &i) in taken.iter().enumerate() {
1448 rank[i] = k;
1449 }
1450 rank
1451 };
1452 let mut order: Vec<usize> = (0..edges.len()).collect();
1453 order.sort_by_key(|&i| (round_of[i], rank[i]));
1454 let mut set_aside: Vec<(Vec<usize>, Shape, crate::support::Wedge)> = Vec::new();
1455 let mut this_round = 0;
1456 for index in order {
1457 let edge = &edges[index];
1458 if round_of[index] != this_round {
1460 this_round = round_of[index];
1461 if let Some(b) = built.take() {
1462 let halves = with_corners(model, &mut set_aside, &junctions, &b.shape, false, tol)?;
1463 let joined = join_flush(
1464 model,
1465 core::mem::take(&mut set_aside),
1466 &junctions,
1467 &halves,
1468 tol,
1469 )?;
1470 built = Some(apply_set_aside(model, b, joined, tol)?);
1471 }
1472 }
1473 if on_round_rim(model, edge, &round_rims) {
1475 if let Some(b) = built.as_mut() {
1476 b.history.delete(edge);
1477 }
1478 continue;
1479 }
1480 let hosts: Vec<Shape> = host_faces.get(&edge.node()).cloned().unwrap_or_default();
1483 let (current, targets): (Shape, Vec<Shape>) = match &built {
1492 None => (solid.clone(), vec![edge.clone()]),
1493 Some(b) => {
1494 let gone = b.history.trace(edge).is_empty();
1495 let on_blended_crease = blended_creases.contains(&hosts_of(edge))
1499 || mates[index].settled == [true, true];
1500 let rounded = on_blended_crease || on_round_rim(model, edge, &round_rims);
1501 if gone && rounded {
1502 continue;
1503 }
1504 if gone {
1505 ogeom_bail!(
1506 Construction,
1507 "an earlier blend in the chain consumed this edge; \
1508 the chain's members interfere"
1509 );
1510 }
1511 let found = match refind(model, &b.shape, edge, &hosts) {
1512 Ok(live) => live,
1513 Err(_) if rounded => Vec::new(),
1514 Err(e) => return Err(e),
1515 };
1516 (b.shape.clone(), found)
1517 }
1518 };
1519 if targets.is_empty() {
1522 if let Some(b) = built.as_mut() {
1523 b.history.delete(edge);
1524 }
1525 continue;
1526 }
1527 let mut current = current;
1528 for target in &targets {
1529 let live = match refind(model, ¤t, target, &hosts) {
1532 Ok(live) => live,
1533 Err(_) if on_round_rim(model, target, &round_rims) => continue,
1534 Err(e) => return Err(e),
1535 };
1536 let [target] = live.as_slice() else {
1537 ogeom_bail!(
1538 Construction,
1539 "a blend in the chain split a piece of a later edge again; \
1540 the chain's members interfere"
1541 );
1542 };
1543 let whole_rim = match rim_circle(model, target, tol) {
1544 Some(rim) if rounds_whole_turn(model, ¤t, target, tol)? => Some(rim),
1545 _ => None,
1546 };
1547 let mut step = {
1548 let (step, wedges) = crate::support::collecting_wedges(|| {
1549 if rolled[index] {
1550 let Some(mut built) = crate::rolled::rolled_fillet(
1551 model,
1552 ¤t,
1553 target,
1554 radii[index],
1555 tol,
1556 )?
1557 else {
1558 ogeom_bail!(
1559 Construction,
1560 "the ball no longer rests on the far edge beside an edge \
1561 of the chain"
1562 );
1563 };
1564 crate::support::credit_new_faces(model, ¤t, target, &mut built)?;
1565 return Ok(built);
1566 }
1567 if let Some(laws) = laws {
1568 return law_piece(
1569 model,
1570 ¤t,
1571 &edges[index],
1572 target,
1573 &laws[index],
1574 (index, &mates),
1575 tol,
1576 );
1577 }
1578 fillet_edge_meeting(
1579 model,
1580 ¤t,
1581 target,
1582 radii[index],
1583 Some((index, &mates)),
1584 cones,
1585 tol,
1586 )
1587 });
1588 let step = step?;
1589 for mut wedge in wedges {
1590 if !wedge.additive {
1591 for junction in &junctions {
1592 let Some(slot) = junction.slot(index) else {
1593 continue;
1594 };
1595 if junction.crossed {
1599 continue;
1600 }
1601 let plane = junction.keeps(slot);
1602 if junction.reentrant {
1610 let Some(cap) = crate::junction::cap_at(
1611 model,
1612 &wedge.solid,
1613 junction.at,
1614 junction.outward[slot],
1615 tol,
1616 )?
1617 else {
1618 ogeom_bail!(
1619 Construction,
1620 "a band meeting another across a re-entrant turn has \
1621 no cap at the vertex to carry on from"
1622 );
1623 };
1624 wedge.solid = crate::junction::carried_into(
1625 model,
1626 &wedge.solid,
1627 &cap,
1628 (edge, &hosts),
1629 junction.outward[slot],
1630 plane,
1631 tol,
1632 )?;
1633 } else {
1634 wedge.solid = crate::support::trimmed_wedge(
1635 model,
1636 &wedge.solid,
1637 plane,
1638 radii[index] * 2.0,
1639 tol,
1640 )?;
1641 }
1642 }
1643 }
1644 set_aside.push((vec![index], edge.clone(), wedge));
1645 }
1646 step
1647 };
1648 if let Some(rim) = whole_rim {
1649 round_rims.push(rim);
1650 }
1651 let hosts = hosts_of(edge);
1652 if hosts.len() == 2 && !blended_creases.contains(&hosts) {
1653 blended_creases.push(hosts);
1654 }
1655 if !target.is_same(edge) {
1658 step.history.delete(edge);
1659 }
1660 current = step.shape.clone();
1661 built = Some(match built {
1662 None => step,
1663 Some(prev) => Built {
1664 shape: step.shape.clone(),
1665 history: prev.history.then(&step.history),
1666 },
1667 });
1668 }
1669 }
1670 let built = built.unwrap_or_else(|| unreachable!());
1671 let halves = with_corners(model, &mut set_aside, &junctions, &built.shape, false, tol)?;
1672 let joined = join_flush(model, set_aside, &junctions, &halves, tol)?;
1673 apply_set_aside(model, built, joined, tol)
1674}
1675
1676fn joinable(
1682 model: &Model,
1683 edges: &[Shape],
1684 found: Vec<crate::junction::Junction>,
1685) -> OgeomResult<Vec<crate::junction::Junction>> {
1686 let mut ends: Vec<Vec<Shape>> = Vec::with_capacity(edges.len());
1687 for edge in edges {
1688 ends.push(
1689 ogeom_algo::edge_vertices(model, edge)?
1690 .map(|(a, b)| vec![a, b])
1691 .unwrap_or_default(),
1692 );
1693 }
1694 let (flush, crossed): (Vec<_>, Vec<_>) = found.into_iter().partition(|j| !j.crossed);
1697 let mut found = flush.clone();
1698 found.extend(crossed);
1699 let pending: Vec<[usize; 2]> = found.iter().map(|j| j.pair).collect();
1700 let mut group: Vec<usize> = (0..edges.len()).collect();
1701 let mut kept: Vec<crate::junction::Junction> = Vec::new();
1702 for junction in found {
1703 let [a, b] = junction.pair;
1704 if group[a] == group[b] {
1705 kept.push(junction);
1710 continue;
1711 }
1712 let joined = |m: usize, n: usize| {
1716 kept.iter()
1717 .chain(std::iter::once(&junction))
1718 .any(|j| j.slot(m).is_some() && j.slot(n).is_some())
1719 || pending
1720 .iter()
1721 .any(|pair| pair.contains(&m) && pair.contains(&n))
1722 };
1723 let of_a: Vec<usize> = (0..edges.len()).filter(|&m| group[m] == group[a]).collect();
1724 let of_b: Vec<usize> = (0..edges.len()).filter(|&m| group[m] == group[b]).collect();
1725 let clash = of_a.iter().any(|&m| {
1726 of_b.iter().any(|&n| {
1727 !joined(m, n) && ends[m].iter().any(|v| ends[n].iter().any(|w| w.is_same(v)))
1728 })
1729 });
1730 if clash {
1731 continue;
1732 }
1733 let (from, to) = (group[a], group[b]);
1734 for g in &mut group {
1735 if *g == from {
1736 *g = to;
1737 }
1738 }
1739 kept.push(junction);
1740 }
1741 let meets_unjoined = (0..edges.len()).any(|m| {
1745 (m + 1..edges.len()).any(|n| {
1746 group[m] == group[n]
1747 && ends[m].iter().any(|v| ends[n].iter().any(|w| w.is_same(v)))
1748 && !kept
1749 .iter()
1750 .any(|j| j.slot(m).is_some() && j.slot(n).is_some())
1751 })
1752 });
1753 if meets_unjoined && kept.iter().any(|j| j.crossed) {
1754 return joinable(model, edges, flush);
1755 }
1756 Ok(kept)
1757}
1758
1759fn with_corners(
1773 model: &mut Model,
1774 set_aside: &mut Vec<(Vec<usize>, Shape, crate::support::Wedge)>,
1775 junctions: &[crate::junction::Junction],
1776 solid: &Shape,
1777 law: bool,
1778 tol: Tolerances,
1779) -> OgeomResult<Vec<(usize, (Point, Vector))>> {
1780 let crossed: Vec<&crate::junction::Junction> = junctions
1781 .iter()
1782 .filter(|j| j.crossed && j.reentrant)
1783 .collect();
1784 let holding = |set_aside: &[(Vec<usize>, Shape, crate::support::Wedge)], index: usize| {
1785 (0..set_aside.len())
1786 .filter(|&k| set_aside[k].0.contains(&index))
1787 .collect::<Vec<usize>>()
1788 };
1789 let mut halves = Vec::new();
1790 let indices: Vec<usize> = crossed.iter().flat_map(|j| j.pair).collect();
1791 for &index in &indices {
1792 let ends: Vec<Point> = crossed
1793 .iter()
1794 .filter(|j| j.slot(index).is_some())
1795 .map(|j| j.at)
1796 .collect();
1797 if ends.len() != 2 || halves.iter().any(|(i, _)| *i == index) {
1798 continue;
1799 }
1800 let held = holding(set_aside, index);
1801 let [k] = held.as_slice() else {
1802 continue;
1803 };
1804 let k = *k;
1805 let along = ends[1] - ends[0];
1806 let length = along.magnitude();
1807 if length <= tol.confusion() {
1808 ogeom_bail!(Construction, "a band crossed at both ends has no length");
1809 }
1810 let toward = along / length;
1811 let middle = ends[0].midpoint(ends[1]);
1812 let span = length + crate::marched::band_fit_target(tol).max(length);
1813 let (indices_k, edge, wedge) = set_aside.remove(k);
1814 for keep in [-toward, toward] {
1815 let half =
1816 crate::support::trimmed_wedge(model, &wedge.solid, (middle, keep), span, tol)?;
1817 set_aside.push((
1818 indices_k.clone(),
1819 edge.clone(),
1820 crate::support::Wedge {
1821 solid: half,
1822 additive: wedge.additive,
1823 edges: wedge.edges.clone(),
1824 melt_refusal: wedge.melt_refusal,
1825 credit: Vec::new(),
1826 },
1827 ));
1828 }
1829 halves.push((index, (middle, toward)));
1830 }
1831 for junction in crossed {
1832 let mut sides = [None, None];
1835 for (slot, side) in sides.iter_mut().enumerate() {
1836 let held = holding(set_aside, junction.pair[slot])
1838 .into_iter()
1839 .filter(|&k| set_aside[k].0.len() == 1);
1840 let mut here = Vec::new();
1841 for k in held {
1842 if crate::junction::cap_at(
1843 model,
1844 &set_aside[k].2.solid,
1845 junction.at,
1846 junction.outward[slot],
1847 tol,
1848 )?
1849 .is_some()
1850 {
1851 here.push(k);
1852 }
1853 }
1854 *side = match here.as_slice() {
1855 [] => None,
1856 [k] => Some(*k),
1857 _ => ogeom_bail!(
1858 Construction,
1859 "a crossed junction's band is more than one cut wedge in its round"
1860 ),
1861 };
1862 }
1863 let (a, b) = match sides {
1865 [None, None] => continue,
1866 [Some(a), Some(b)] if a != b => (a, b),
1867 _ => ogeom_bail!(
1868 Construction,
1869 "a crossed junction's bands are not one cut wedge each in their round"
1870 ),
1871 };
1872 if set_aside[a].2.additive || set_aside[b].2.additive {
1873 ogeom_bail!(Construction, "a crossed junction's band is a fill");
1874 }
1875 if !set_aside[a].2.credit.is_empty() || !set_aside[b].2.credit.is_empty() {
1876 ogeom_bail!(Construction, "a crossed junction's band is already joined");
1877 }
1878 let (taken, of_second) = crate::junction::corner(
1879 model,
1880 [&set_aside[a].2.solid, &set_aside[b].2.solid],
1881 junction,
1882 solid,
1883 law,
1884 tol,
1885 )?;
1886 let second = set_aside[b].1.clone();
1887 let later = set_aside.remove(a.max(b));
1888 let earlier = set_aside.remove(a.min(b));
1889 let ((mut indices, edge, first), (others, other, last)) = if a < b {
1890 (earlier, later)
1891 } else {
1892 (later, earlier)
1893 };
1894 indices.extend(others);
1895 let mut rounded = first.edges;
1896 rounded.extend(last.edges);
1897 rounded.push(other);
1898 set_aside.push((
1899 indices,
1900 edge,
1901 crate::support::Wedge {
1902 solid: taken,
1903 additive: false,
1904 edges: rounded,
1905 melt_refusal: first.melt_refusal.or(last.melt_refusal),
1906 credit: of_second
1907 .into_iter()
1908 .map(|face| (face, second.clone()))
1909 .collect(),
1910 },
1911 ));
1912 }
1913 Ok(halves)
1914}
1915
1916fn join_flush(
1920 model: &mut Model,
1921 set_aside: Vec<(Vec<usize>, Shape, crate::support::Wedge)>,
1922 junctions: &[crate::junction::Junction],
1923 halves: &[(usize, (Point, Vector))],
1924 tol: Tolerances,
1925) -> OgeomResult<Vec<(Shape, crate::support::Wedge)>> {
1926 let mut held: Vec<Option<(Vec<usize>, Shape, crate::support::Wedge)>> =
1928 set_aside.into_iter().map(Some).collect();
1929 let meetings: Vec<((Point, Vector), Point, [usize; 2])> = junctions
1933 .iter()
1934 .filter(|j| !j.crossed)
1935 .map(|j| (j.flush, j.at, j.pair))
1936 .chain(
1937 halves
1938 .iter()
1939 .map(|&(index, (on, normal))| ((on, normal), on, [index, index])),
1940 )
1941 .collect();
1942 for ((on, normal), at, pair) in meetings {
1943 let mut sides = [None, None];
1946 for slot in 0..2 {
1947 for (k, entry) in held.iter().enumerate() {
1948 let Some((indices, _, wedge)) = entry else {
1949 continue;
1950 };
1951 if !indices.contains(&pair[slot]) || (slot == 1 && sides[0] == Some(k)) {
1952 continue;
1953 }
1954 let gap = crate::marched::band_fit_target(tol) * 2.0;
1955 if crate::junction::ends_in(model, &wedge.solid, (on, normal), at, gap, tol)? {
1956 sides[slot] = Some(k);
1957 if slot == 0 {
1958 break;
1959 }
1960 }
1961 }
1962 }
1963 let [Some(a), Some(b)] = sides else {
1964 continue;
1965 };
1966 if a == b {
1967 let Some((indices, edge, ring)) = held[a].take() else {
1970 continue;
1971 };
1972 let (solid, traced) = crate::junction::joined(
1973 model,
1974 [&ring.solid, &ring.solid],
1975 (on, normal),
1976 at,
1977 crate::marched::band_fit_target(tol) * 2.0,
1978 tol,
1979 )?;
1980 let mut credit = Vec::new();
1981 for (given, image) in &traced {
1982 let owner = ring
1983 .credit
1984 .iter()
1985 .find(|(f, _)| f.is_same(given))
1986 .map_or_else(|| edge.clone(), |(_, e)| e.clone());
1987 credit.push((image.clone(), owner));
1988 }
1989 held[a] = Some((
1990 indices,
1991 edge,
1992 crate::support::Wedge {
1993 solid,
1994 credit,
1995 ..ring
1996 },
1997 ));
1998 continue;
1999 }
2000 let (Some((mut indices, edge, first)), Some((others, other, second))) =
2001 (held[a].take(), held[b].take())
2002 else {
2003 continue;
2004 };
2005 let (solid, traced) = crate::junction::joined(
2006 model,
2007 [&first.solid, &second.solid],
2008 (on, normal),
2009 at,
2010 crate::marched::band_fit_target(tol) * 2.0,
2011 tol,
2012 )?;
2013 let mut credit: Vec<(Shape, Shape)> = Vec::new();
2016 for (from, rounded) in [(&first, &edge), (&second, &other)] {
2017 for face in ogeom_topo::explore_unique(model, &from.solid, ShapeType::Face)? {
2018 let owner = from
2019 .credit
2020 .iter()
2021 .find(|(f, _)| f.is_same(&face))
2022 .map_or_else(|| rounded.clone(), |(_, e)| e.clone());
2023 for (given, image) in &traced {
2024 if given.is_same(&face) {
2025 credit.push((image.clone(), owner.clone()));
2026 }
2027 }
2028 }
2029 }
2030 indices.extend(others);
2031 indices.dedup();
2032 let mut rounded: Vec<Shape> = first.edges;
2033 rounded.extend(second.edges);
2034 if !other.is_same(&edge) {
2035 rounded.push(other);
2036 }
2037 held[a] = Some((
2038 indices,
2039 edge,
2040 crate::support::Wedge {
2041 solid,
2042 additive: false,
2043 edges: rounded,
2044 melt_refusal: first.melt_refusal.or(second.melt_refusal),
2045 credit,
2046 },
2047 ));
2048 }
2049 Ok(held
2050 .into_iter()
2051 .flatten()
2052 .map(|(_, edge, wedge)| (edge, wedge))
2053 .collect())
2054}
2055
2056pub(crate) fn apply_set_aside(
2066 model: &mut Model,
2067 mut built: Built,
2068 set_aside: Vec<(Shape, crate::support::Wedge)>,
2069 tol: Tolerances,
2070) -> OgeomResult<Built> {
2071 if set_aside.is_empty() {
2072 return Ok(built);
2073 }
2074 let mut boxes = Vec::with_capacity(set_aside.len());
2075 for (_, wedge) in &set_aside {
2076 boxes.push(ogeom_algo::shape_bounds(model, &wedge.solid, tol)?);
2077 }
2078 let (fused, cut): (Vec<usize>, Vec<usize>) =
2079 (0..set_aside.len()).partition(|&i| set_aside[i].1.additive);
2080 let crossed = cut
2083 .iter()
2084 .any(|&i| fused.iter().any(|&j| boxes[i].intersects(&boxes[j])));
2085 let groups: Vec<Vec<usize>> = if crossed {
2086 (0..set_aside.len()).map(|i| vec![i]).collect()
2087 } else {
2088 let mut groups: Vec<Vec<usize>> = Vec::new();
2092 for side in [cut, fused] {
2093 let first = groups.len();
2094 for i in side {
2095 let slot = groups[first..]
2096 .iter()
2097 .position(|g| g.iter().all(|&j| !boxes[i].intersects(&boxes[j])));
2098 match slot {
2099 Some(k) => groups[first + k].push(i),
2100 None => groups.push(vec![i]),
2101 }
2102 }
2103 }
2104 groups
2105 };
2106 let given = built.clone();
2107 let mut groups: std::collections::VecDeque<Vec<usize>> = groups.into();
2108 while let Some(group) = groups.pop_front() {
2109 let additive = set_aside[group[0]].1.additive;
2110 let lumps: Vec<Shape> = group
2111 .iter()
2112 .map(|&i| set_aside[i].1.solid.clone())
2113 .collect();
2114 let tool = if let [one] = lumps.as_slice() {
2115 one.clone()
2116 } else {
2117 model.add_compound(&lumps)?
2118 };
2119 let applied = if additive {
2120 ogeom_bool::fuse_seated(model, &built.shape, &tool, tol)
2121 } else {
2122 ogeom_bool::cut_seated(model, &built.shape, &tool, tol)
2123 };
2124 if matches!(applied, Err(OgeomError::NotDone(_))) && group.len() > 1 {
2128 built = given.clone();
2129 groups = (0..set_aside.len()).map(|i| vec![i]).collect();
2130 continue;
2131 }
2132 let refusal = group.iter().find_map(|&i| set_aside[i].1.melt_refusal);
2133 let step = match (applied, refusal) {
2134 (Err(OgeomError::NotDone(cause)), Some(refusal)) => {
2135 ogeom_bail!(NotDone, "{refusal} ({cause})")
2136 }
2137 (applied, _) => applied?,
2138 };
2139 let mut history = built.history.then(&step.history);
2143 let result = ogeom_topo::explore_unique(model, &step.shape, ShapeType::Face)?;
2144 for &i in &group {
2145 let (edge, wedge) = &set_aside[i];
2146 for face in ogeom_topo::explore_unique(model, &wedge.solid, ShapeType::Face)? {
2147 let owner = wedge
2148 .credit
2149 .iter()
2150 .find(|(f, _)| f.is_same(&face))
2151 .map_or(edge, |(_, e)| e);
2152 for image in step.history.trace(&face) {
2153 if result.iter().any(|f| f.is_same(image)) {
2154 history.generate(owner, image.clone());
2155 }
2156 }
2157 }
2158 for rounded in &wedge.edges {
2159 history.delete(rounded);
2160 }
2161 history.delete(edge);
2162 }
2163 built = Built {
2164 shape: step.shape.clone(),
2165 history,
2166 };
2167 }
2168 Ok(built)
2169}
2170
2171pub fn fillet_edge_variable(
2188 model: &mut Model,
2189 solid: &Shape,
2190 edge: &Shape,
2191 start_radius: f64,
2192 end_radius: f64,
2193 tol: Tolerances,
2194) -> OgeomResult<Built> {
2195 ogeom_algo::on_own_nodes(
2196 model,
2197 solid,
2198 std::slice::from_ref(edge),
2199 |model, solid, parts| {
2200 fillet_edge_variable_own(model, solid, &parts[0], start_radius, end_radius, tol)
2201 },
2202 )
2203}
2204
2205fn fillet_edge_variable_own(
2207 model: &mut Model,
2208 solid: &Shape,
2209 edge: &Shape,
2210 start_radius: f64,
2211 end_radius: f64,
2212 tol: Tolerances,
2213) -> OgeomResult<Built> {
2214 for radius in [start_radius, end_radius] {
2215 if !radius.is_finite() || radius <= tol.confusion() {
2216 ogeom_bail!(Construction, "a fillet of radius {radius} rounds nothing");
2217 }
2218 }
2219 if (start_radius - end_radius).abs() <= tol.confusion() {
2220 return fillet_edge_own(model, solid, edge, start_radius, tol);
2221 }
2222 let law = Profile::new(
2223 &RadiusLaw::Linear {
2224 start: start_radius,
2225 end: end_radius,
2226 },
2227 false,
2228 tol,
2229 )?;
2230 if let Some(built) = consumed_law(model, solid, edge, &law, false, tol)? {
2231 return Ok(built);
2232 }
2233 law_ball_on_faces(model, solid, edge, &law, Some(false), tol)?;
2234 let seat = planar_seat(model, solid, edge, tol)?;
2235 let (first, second) = if seat
2236 .along
2237 .dot(seat.normals[0].cross(seat.normals[1]))
2238 .is_sign_positive()
2239 {
2240 (0, 1)
2241 } else {
2242 (1, 0)
2243 };
2244 let sign = if seat.convex { 1.0 } else { -1.0 };
2245 let n1 = seat.normals[first] * sign;
2246 let n2 = seat.normals[second] * sign;
2247 let a = seat.leg(first, tol)? * sign;
2248 let b = seat.leg(second, tol)? * sign;
2249 let bisector = {
2250 let u = a + b;
2251 let m = u.magnitude();
2252 if m <= tol.angular() {
2253 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
2254 }
2255 u / m
2256 };
2257 let depth = -n1.dot(bisector);
2258 if depth <= tol.angular() {
2259 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
2260 }
2261 let sweep = seat.along.dot(n1.cross(n2)).atan2(n1.dot(n2)) * sign;
2262 if sweep <= tol.angular() {
2263 ogeom_bail!(Construction, "the faces are parallel; there is no corner");
2264 }
2265
2266 let radii = [start_radius, end_radius];
2270 let apex = [seat.start, seat.end];
2271 let centre = [
2272 apex[0] + bisector * (radii[0] / depth),
2273 apex[1] + bisector * (radii[1] / depth),
2274 ];
2275 let contact_a = [centre[0] + n1 * radii[0], centre[1] + n1 * radii[1]];
2276 let contact_b = [centre[0] + n2 * radii[0], centre[1] + n2 * radii[1]];
2277
2278 let half_cos = (sweep / 2.0).cos();
2281 let arc_mid = (n1 + n2) / (1.0 + n1.dot(n2));
2282 let weights = [1.0, half_cos, 1.0];
2283
2284 let u_knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 1.0, 1.0], 1)?;
2288 let v_knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], 2)?;
2289 let mut net: Vec<ogeom_math::Weighted<Point>> = Vec::with_capacity(6);
2290 for i in 0..2 {
2291 let arms = [n1, arc_mid, n2];
2292 for (j, arm) in arms.iter().enumerate() {
2293 net.push(ogeom_math::Weighted::new(
2294 centre[i] + *arm * radii[i],
2295 weights[j],
2296 tol,
2297 )?);
2298 }
2299 }
2300 let grid = ogeom_math::ControlGrid::new(net, 2, 3)?;
2301 let surface: SurfaceGeometry =
2302 ogeom_geom::BSplineSurface::rational(u_knots, v_knots, grid)?.into();
2303 let surface_id = model.geometry_mut().add_surface(surface.clone());
2304
2305 let va = [
2307 make_vertex(model, contact_a[0]).shape,
2308 make_vertex(model, contact_a[1]).shape,
2309 ];
2310 let vb = [
2311 make_vertex(model, contact_b[0]).shape,
2312 make_vertex(model, contact_b[1]).shape,
2313 ];
2314
2315 let rail = |model: &mut Model,
2318 from: (&Shape, Point),
2319 to: (&Shape, Point),
2320 row: f64|
2321 -> OgeomResult<Shape> {
2322 let line = ogeom_geom::LineCurve::segment(from.1, to.1, tol)?;
2323 let curve = Curve::Line(line);
2324 let domain = ogeom_geom::Curve3d::domain(&curve);
2325 let built = make_edge_between(model, curve, domain, from.0, to.0, tol)?.shape;
2326 let knots = ogeom_math::KnotVector::new(vec![domain.0, domain.0, domain.1, domain.1], 1)?;
2327 let pcurve = ogeom_geom::BSpline2d::new(
2328 knots,
2329 vec![
2330 ogeom_math::Point2::new(0.0, row),
2331 ogeom_math::Point2::new(1.0, row),
2332 ],
2333 tol,
2334 )?;
2335 ogeom_algo::attach_pcurve(
2336 model,
2337 &built,
2338 pcurve.into(),
2339 surface_id,
2340 ogeom_topo::Location::identity(),
2341 domain,
2342 )?;
2343 Ok(built)
2344 };
2345 let rail_a = rail(model, (&va[0], contact_a[0]), (&va[1], contact_a[1]), 0.0)?;
2346 let rail_b = rail(model, (&vb[0], contact_b[0]), (&vb[1], contact_b[1]), 1.0)?;
2347
2348 let arc_edge = |model: &mut Model, i: usize| -> OgeomResult<Shape> {
2351 let knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], 2)?;
2352 let control = vec![
2353 ogeom_math::Weighted::new(contact_a[i], 1.0, tol)?,
2354 ogeom_math::Weighted::new(centre[i] + arc_mid * radii[i], half_cos, tol)?,
2355 ogeom_math::Weighted::new(contact_b[i], 1.0, tol)?,
2356 ];
2357 let curve = Curve::BSpline(ogeom_geom::BSplineCurve::rational(knots, control)?);
2358 let built = make_edge_between(model, curve, (0.0, 1.0), &va[i], &vb[i], tol)?.shape;
2359 #[allow(clippy::cast_precision_loss)]
2360 let column = ogeom_geom::Line2d::over(
2361 ogeom_math::Axis2::new(
2362 ogeom_math::Point2::new(i as f64, 0.0),
2363 ogeom_math::Direction2::Y,
2364 ),
2365 0.0,
2366 1.0,
2367 )?;
2368 ogeom_algo::attach_pcurve(
2369 model,
2370 &built,
2371 column.into(),
2372 surface_id,
2373 ogeom_topo::Location::identity(),
2374 (0.0, 1.0),
2375 )?;
2376 Ok(built)
2377 };
2378 let arc0 = arc_edge(model, 0)?;
2379 let arc1 = arc_edge(model, 1)?;
2380
2381 let blend = {
2385 let wire = ogeom_algo::make_wire(
2386 model,
2387 &[
2388 rail_a.clone(),
2389 arc1.clone(),
2390 rail_b.reversed(),
2391 arc0.reversed(),
2392 ],
2393 tol,
2394 )?
2395 .shape;
2396 let face =
2397 ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
2398 use ogeom_geom::Surface as _;
2399 let s_mid = surface.point_at(0.5, 0.5, tol)?;
2400 let (du, dv) = surface.d1_at(0.5, 0.5, tol)?;
2401 let apex_mid = apex[0].midpoint(apex[1]);
2402 let outward = s_mid - apex_mid.midpoint(s_mid);
2403 if du.cross(dv).dot(outward) >= 0.0 {
2404 face
2405 } else {
2406 face.reversed()
2407 }
2408 };
2409
2410 let leg_a = planar_face(
2414 model,
2415 &[apex[0], contact_a[0], contact_a[1], apex[1]],
2416 n1 * sign,
2417 tol,
2418 )?;
2419 let leg_b = planar_face(
2420 model,
2421 &[apex[0], contact_b[0], contact_b[1], apex[1]],
2422 n2 * sign,
2423 tol,
2424 )?;
2425 let cap = |model: &mut Model, i: usize, outward: Vector| -> OgeomResult<Built> {
2426 let plane = Plane::through(apex[i], Direction::new(outward, tol)?);
2427 let reach = (apex[i].distance(contact_a[i]) + apex[i].distance(contact_b[i]) + 1.0) * 2.0;
2428 let cap_surface: SurfaceGeometry =
2429 PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
2430 let apex_v = make_vertex(model, apex[i]).shape;
2431 let edges = vec![
2432 crate::support::segment_between(
2433 model,
2434 (&apex_v, apex[i]),
2435 (&va[i], contact_a[i]),
2436 tol,
2437 )?,
2438 if i == 0 { arc0.clone() } else { arc1.clone() },
2439 crate::support::segment_between(
2440 model,
2441 (&vb[i], contact_b[i]),
2442 (&apex_v, apex[i]),
2443 tol,
2444 )?,
2445 ];
2446 let turn = (contact_a[i] - apex[i]).cross(contact_b[i] - apex[i]);
2449 let edges: Vec<Shape> = if turn.dot(outward) < 0.0 {
2450 edges.iter().rev().map(Shape::reversed).collect()
2451 } else {
2452 edges
2453 };
2454 ogeom_algo::make_face_with_pcurves(model, cap_surface, &[edges], tol)
2455 };
2456 let cap0 = cap(model, 0, -seat.along)?.shape;
2457 let cap1 = cap(model, 1, seat.along)?.shape;
2458
2459 let faces = [leg_a, leg_b, cap0, cap1, blend];
2460 apply_wedge(model, solid, Some(edge), &faces, !seat.convex, tol)
2461}
2462
2463pub fn fillet_edges_variable(
2548 model: &mut Model,
2549 solid: &Shape,
2550 chain: &[Shape],
2551 law: &RadiusLaw,
2552 tol: Tolerances,
2553) -> OgeomResult<Built> {
2554 if chain.is_empty() {
2555 ogeom_bail!(
2556 Construction,
2557 "a variable radius over no edges rounds nothing"
2558 );
2559 }
2560 ogeom_algo::on_own_nodes(model, solid, chain, |model, solid, parts| {
2561 fillet_chain_law_own(model, solid, parts, law, tol)
2562 })
2563}
2564
2565fn closed_chain(model: &Model, solid: &Shape, chain: &[Shape]) -> OgeomResult<bool> {
2568 if !runs_between_one_pair(model, solid, chain)? {
2569 return Ok(false);
2570 }
2571 let (Some(first), Some(last)) = (
2572 ogeom_algo::edge_vertices(model, &chain[0])?,
2573 ogeom_algo::edge_vertices(model, &chain[chain.len() - 1])?,
2574 ) else {
2575 return Ok(false);
2576 };
2577 Ok(last.1.is_same(&first.0))
2578}
2579
2580fn one_curve_span(
2586 model: &Model,
2587 solid: &Shape,
2588 chain: &[Shape],
2589 tol: Tolerances,
2590) -> OgeomResult<Option<(f64, f64)>> {
2591 if !runs_between_one_pair(model, solid, chain)? {
2592 return Ok(None);
2593 }
2594 let mut pieces = Vec::with_capacity(chain.len());
2595 for edge in chain {
2596 let Some(ogeom_topo::EdgeRepr::Curve3d {
2597 curve,
2598 location,
2599 range,
2600 }) = model
2601 .node(edge)
2602 .and_then(|n| n.data().as_edge())
2603 .and_then(|d| d.curve3d())
2604 .cloned()
2605 else {
2606 return Ok(None);
2607 };
2608 if edge.location() != &ogeom_topo::Location::default() {
2609 return Ok(None);
2610 }
2611 pieces.push((curve, location, range, edge.orientation()));
2612 }
2613 let (curve, location, _, orientation) = pieces[0].clone();
2616 let Some(geometry) = model.geometry().curve(curve) else {
2617 return Ok(None);
2618 };
2619 if pieces.iter().any(|(c, l, _, o)| {
2620 *l != location || *o != orientation || model.geometry().curve(*c) != Some(geometry)
2621 }) {
2622 return Ok(None);
2623 }
2624 let period = {
2625 use ogeom_geom::Curve3d as _;
2626 geometry.is_periodic().then(|| {
2627 let (lo, hi) = geometry.domain();
2628 hi - lo
2629 })
2630 };
2631 let forward = orientation != ogeom_topo::Orientation::Reversed;
2634 let ordered: Vec<(f64, f64)> = if forward {
2635 pieces.iter().map(|p| p.2).collect()
2636 } else {
2637 pieces.iter().rev().map(|p| p.2).collect()
2638 };
2639 let reach = tol.parametric();
2640 let mut span = ordered[0];
2641 for &(lo, hi) in &ordered[1..] {
2642 let shift = match period {
2643 Some(period) if (lo + period - span.1).abs() <= reach => period,
2644 _ => 0.0,
2645 };
2646 if (lo + shift - span.1).abs() > reach {
2647 return Ok(None);
2648 }
2649 span.1 = hi + shift;
2650 }
2651 Ok(Some(span))
2652}
2653
2654fn runs_between_one_pair(model: &Model, solid: &Shape, chain: &[Shape]) -> OgeomResult<bool> {
2657 let faces_of = |edge: &Shape| -> OgeomResult<Vec<Shape>> {
2658 let mut faces: Vec<Shape> = Vec::new();
2659 for f in ogeom_topo::ancestors_of(model, solid, edge, ShapeType::Face)? {
2660 if !faces.iter().any(|g| g.is_same(&f)) {
2661 faces.push(f);
2662 }
2663 }
2664 Ok(faces)
2665 };
2666 let hosts = faces_of(&chain[0])?;
2667 let mut ends = Vec::with_capacity(chain.len());
2668 for (i, edge) in chain.iter().enumerate() {
2669 if chain[..i].iter().any(|e| e.is_same(edge)) {
2670 return Ok(false);
2671 }
2672 let faces = faces_of(edge)?;
2673 let same =
2674 faces.len() == hosts.len() && faces.iter().all(|f| hosts.iter().any(|h| h.is_same(f)));
2675 if !same {
2676 return Ok(false);
2677 }
2678 let Some(pair) = ogeom_algo::edge_vertices(model, edge)? else {
2679 return Ok(false);
2680 };
2681 ends.push(pair);
2682 }
2683 Ok((1..ends.len()).all(|i| ends[i - 1].1.is_same(&ends[i].0)))
2684}
2685
2686fn fillet_chain_law_own(
2688 model: &mut Model,
2689 solid: &Shape,
2690 chain: &[Shape],
2691 law: &RadiusLaw,
2692 tol: Tolerances,
2693) -> OgeomResult<Built> {
2694 let edge = &chain[0];
2695 let closed = closed_chain(model, solid, chain)?;
2696 let span = if chain.len() > 1 && !closed {
2700 let Some(span) = one_curve_span(model, solid, chain, tol)? else {
2701 return chain_law(model, solid, chain, law, tol);
2702 };
2703 Some(span)
2704 } else {
2705 None
2706 };
2707 let profile = Profile::new(law, closed, tol)?;
2708 if profile.level(tol) {
2709 let radii = vec![profile.largest(); chain.len()];
2710 return fillet_edges_meeting(model, solid, chain, &radii, tol);
2711 }
2712 let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
2713 if chain.len() == 1
2714 && let RadiusLaw::Linear { start, end } = law
2715 {
2716 let (from, to) = if reversed {
2717 (*end, *start)
2718 } else {
2719 (*start, *end)
2720 };
2721 return fillet_edge_variable_own(model, solid, edge, from, to, tol);
2722 }
2723 let mut planar = false;
2727 for e in chain {
2728 let (curve, _) = edge_curve(model, e, tol)?;
2729 if matches!(curve, Curve::Line(_)) {
2730 let seat = if chain.len() == 1 {
2731 Some(route(model, solid, e, Rims::Exact, tol)?)
2732 } else {
2733 None
2734 };
2735 planar = matches!(seat, Some(Route::Planar));
2736 if !planar && !matches!(seat, Some(Route::Ruled(_))) {
2737 ogeom_bail!(
2738 Construction,
2739 "a variable radius on a straight edge other than one \
2740 between two planes or along a drum's ruling, alone, is \
2741 still owed; see docs/CAPABILITIES.md, fillet.variable"
2742 );
2743 }
2744 }
2745 }
2746 if planar
2749 && chain.len() == 1
2750 && let Some(built) = consumed_law(model, solid, edge, &profile, reversed, tol)?
2751 {
2752 return Ok(built);
2753 }
2754 let stations = (chain.len() == 1 && span.is_none()).then_some(reversed);
2756 if closed && chain.len() > 1 && !law_loop_fits(model, solid, chain, &profile, tol)? {
2757 for e in chain {
2758 law_ball_on_faces(model, solid, e, &profile, None, tol)?;
2759 }
2760 } else if !(closed && chain.len() > 1) {
2761 for e in chain {
2762 law_ball_on_faces(model, solid, e, &profile, stations, tol)?;
2763 }
2764 }
2765 let mut built = if planar {
2766 straight_law_fillet(model, solid, edge, &profile, reversed, (0.0, 1.0), tol)?
2767 } else {
2768 crate::marched::marched_fillet_law(model, solid, edge, &profile, span, None, tol)?
2769 };
2770 crate::support::credit_new_faces(model, solid, edge, &mut built)?;
2771 if span.is_some() {
2773 for other in &chain[1..] {
2774 built.history.delete(other);
2775 }
2776 }
2777 Ok(built)
2778}
2779
2780fn law_piece(
2792 model: &mut Model,
2793 solid: &Shape,
2794 whole: &Shape,
2795 piece: &Shape,
2796 law: &Profile,
2797 (index, mates): (usize, &[Mate]),
2798 tol: Tolerances,
2799) -> OgeomResult<Built> {
2800 use ogeom_geom::Curve3d as _;
2801 let (curve, range) = edge_curve(model, whole, tol)?;
2802 let ruler = crate::law::Ruler::new(curve.clone(), range, false, tol)?;
2803 let whole_reversed = whole.orientation() == ogeom_topo::Orientation::Reversed;
2804 let fraction = |p: Point| -> OgeomResult<f64> {
2807 let t = ogeom_algo::project_on_curve(&curve, p, 64, tol)?.parameter;
2808 let s = ruler.fraction(t, tol)?.0;
2809 Ok(if whole_reversed { 1.0 - s } else { s })
2810 };
2811 let (piece_curve, (lo, hi)) = edge_curve(model, piece, tol)?;
2812 let reversed = piece.orientation() == ogeom_topo::Orientation::Reversed;
2813 let (start, end) = if reversed {
2814 (
2815 piece_curve.point_at(hi, tol)?,
2816 piece_curve.point_at(lo, tol)?,
2817 )
2818 } else {
2819 (
2820 piece_curve.point_at(lo, tol)?,
2821 piece_curve.point_at(hi, tol)?,
2822 )
2823 };
2824 let (from, to) = (fraction(start)?, fraction(end)?);
2825 if (to - from).abs() <= tol.parametric() {
2826 ogeom_bail!(
2827 Construction,
2828 "a piece of a chain's edge under a law has no length"
2829 );
2830 }
2831 let own = law.window(from, to);
2832 let planar = matches!(piece_curve, Curve::Line(_))
2833 && matches!(route(model, solid, piece, Rims::Exact, tol)?, Route::Planar);
2834 if !planar {
2835 return crate::marched::marched_fillet_law(
2836 model,
2837 solid,
2838 piece,
2839 &own,
2840 None,
2841 Some((index, mates)),
2842 tol,
2843 );
2844 }
2845 let (built, wedges) = crate::support::collecting_wedges(|| {
2846 straight_law_fillet(model, solid, piece, &own, reversed, (0.0, 1.0), tol)
2847 });
2848 built?;
2849 let [band] = <[crate::support::Wedge; 1]>::try_from(wedges).map_err(|w| {
2850 ogeom_core::ogeom_err!(
2851 Construction,
2852 "a piece of a chain's edge under a law built {} wedges, not one",
2853 w.len()
2854 )
2855 })?;
2856 let mut wedge = band.solid;
2857 let gap = crate::marched::band_fit_target(tol) * 2.0;
2858 let seat = planar_seat(model, solid, piece, tol)?;
2859 for (at, outward, radius) in [
2860 (
2861 seat.start,
2862 -seat.along,
2863 if reversed {
2864 own.at(1.0).0
2865 } else {
2866 own.at(0.0).0
2867 },
2868 ),
2869 (
2870 seat.end,
2871 seat.along,
2872 if reversed {
2873 own.at(0.0).0
2874 } else {
2875 own.at(1.0).0
2876 },
2877 ),
2878 ] {
2879 let (alone, wedges) = crate::support::collecting_wedges(|| {
2880 planar_fillet(
2881 model,
2882 solid,
2883 piece,
2884 Section::Ball(radius),
2885 Some((index, mates)),
2886 tol,
2887 )
2888 });
2889 alone?;
2890 let Some(alone) = wedges.into_iter().next() else {
2891 continue;
2892 };
2893 let mut past = f64::NEG_INFINITY;
2896 for vertex in explore_unique(model, &alone.solid, ShapeType::Vertex)? {
2897 if let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) {
2898 let p = vertex.transform(model.datums())?.apply(data.point);
2899 past = past.max((p - at).dot(outward));
2900 }
2901 }
2902 if past <= gap {
2903 continue;
2904 }
2905 let span = ogeom_algo::shape_bounds(model, &alone.solid, tol)?.diagonal() * 2.0;
2906 let run_out = crate::support::trimmed_wedge(model, &alone.solid, (at, outward), span, tol)?;
2907 wedge = crate::junction::joined(model, [&wedge, &run_out], (at, outward), at, gap, tol)?.0;
2908 }
2909 crate::support::apply_wedge_solid(model, solid, Some(piece), &wedge, band.additive, tol)
2910}
2911
2912fn consumed_law(
2924 model: &mut Model,
2925 solid: &Shape,
2926 edge: &Shape,
2927 law: &Profile,
2928 reversed: bool,
2929 tol: Tolerances,
2930) -> OgeomResult<Option<Built>> {
2931 let consumes = |radius: f64| -> OgeomResult<Option<crate::support::Seat>> {
2935 match crate::support::ball_fits(model, solid, edge, radius, false, tol) {
2936 Ok(_) => Ok(None),
2937 Err(OgeomError::Construction(_)) => {
2938 crate::consumed::consumed_seat(model, solid, edge, radius, tol)
2939 }
2940 Err(e) => Err(e),
2941 }
2942 };
2943 let (least, largest) = (consumes(law.least())?, consumes(law.largest())?);
2944 let seat = match (least, largest) {
2945 (Some(_), Some(seat)) => seat,
2946 (None, None) => return Ok(None),
2947 _ => ogeom_bail!(
2948 Construction,
2949 "a variable radius whose ball sits on a narrow face beside the \
2950 edge at some radii of the law and takes it off whole at others \
2951 is still owed: the constant round changes construction between \
2952 them; see docs/CAPABILITIES.md, fillet.variable"
2953 ),
2954 };
2955 let mut built = straight_law_on(model, solid, edge, seat, law, reversed, (0.0, 1.0), tol)?;
2956 crate::support::credit_new_faces(model, solid, edge, &mut built)?;
2957 Ok(Some(built))
2958}
2959
2960fn law_on_edge<'a>(
2964 model: &Model,
2965 edge: &Shape,
2966 law: &'a Profile,
2967 reversed: bool,
2968 tol: Tolerances,
2969) -> OgeomResult<Box<dyn Fn(f64) -> f64 + 'a>> {
2970 let (curve, range) = edge_curve(model, edge, tol)?;
2971 let flip = move |s: f64| if reversed { 1.0 - s } else { s };
2972 if matches!(curve, Curve::Line(_)) {
2973 return Ok(Box::new(move |f: f64| law.at(flip(f)).0));
2974 }
2975 let ruler = crate::law::Ruler::new(curve, range, false, tol)?;
2976 Ok(Box::new(move |f: f64| {
2977 let t = (range.1 - range.0).mul_add(f, range.0);
2978 let s = ruler.fraction(t, tol).map_or(f, |(s, _)| s);
2979 law.at(flip(s)).0
2980 }))
2981}
2982
2983fn law_loop_fits(
2991 model: &Model,
2992 solid: &Shape,
2993 chain: &[Shape],
2994 law: &Profile,
2995 tol: Tolerances,
2996) -> OgeomResult<bool> {
2997 use ogeom_geom::Curve3d as _;
2998 let (stored, (lo, hi)) = edge_curve(model, &chain[0], tol)?;
2999 if !stored.is_periodic() {
3000 return Ok(false);
3001 }
3002 let ruler = crate::law::Ruler::new(stored.clone(), stored.domain(), true, tol)?;
3003 let (start, heading) = if chain[0].orientation() == ogeom_topo::Orientation::Reversed {
3004 (stored.point_at(hi, tol)?, -stored.d1_at(hi, tol)?)
3005 } else {
3006 (stored.point_at(lo, tol)?, stored.d1_at(lo, tol)?)
3007 };
3008 let at = ogeom_algo::project_on_curve(&stored, start, 256, tol)?.parameter;
3009 let against = stored.d1_at(at, tol)?.dot(heading) < 0.0;
3010 let origin = ruler.fraction(at, tol)?.0;
3011 let fractions: Vec<f64> = (1..LAW_STATIONS)
3012 .map(|k| f64::from(k) / f64::from(LAW_STATIONS))
3013 .collect();
3014 for edge in chain {
3015 let (curve, (a, b)) = edge_curve(model, edge, tol)?;
3016 let radius_at = |f: f64| -> f64 {
3017 let read = || -> OgeomResult<f64> {
3018 let p = curve.point_at((b - a).mul_add(f, a), tol)?;
3019 let t = ogeom_algo::project_on_curve(&stored, p, 64, tol)?.parameter;
3020 let s = ruler.fraction(t, tol)?.0 - origin;
3021 Ok(law.at((if against { -s } else { s }).rem_euclid(1.0)).0)
3022 };
3023 read().unwrap_or_else(|_| law.largest())
3024 };
3025 crate::support::ball_fits_by(model, solid, edge, &radius_at, &fractions, false, tol)?;
3026 crate::support::ball_fits(model, solid, edge, law.largest(), false, tol)?;
3029 }
3030 Ok(true)
3031}
3032
3033fn law_chain_fits(
3046 model: &Model,
3047 solid: &Shape,
3048 chain: &[Shape],
3049 laws: &[&Profile],
3050 tol: Tolerances,
3051) -> OgeomResult<()> {
3052 let fractions: Vec<f64> = (1..LAW_STATIONS)
3053 .map(|k| f64::from(k) / f64::from(LAW_STATIONS))
3054 .collect();
3055 let mut contacts = Vec::with_capacity(chain.len());
3056 for (edge, law) in chain.iter().zip(laws) {
3057 let (curve, _) = edge_curve(model, edge, tol)?;
3058 let planar = matches!(curve, Curve::Line(_))
3059 && matches!(route(model, solid, edge, Rims::Exact, tol)?, Route::Planar);
3060 if planar {
3061 let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
3062 let radius_at = law_on_edge(model, edge, law, reversed, tol)?;
3063 contacts.push(crate::support::ball_fits_by(
3064 model,
3065 solid,
3066 edge,
3067 &*radius_at,
3068 &fractions,
3069 true,
3070 tol,
3071 )?);
3072 } else {
3073 law_ball_on_faces(model, solid, edge, law, None, tol)?;
3074 contacts.push(crate::support::ball_fits(
3075 model,
3076 solid,
3077 edge,
3078 law.largest(),
3079 true,
3080 tol,
3081 )?);
3082 }
3083 }
3084 crate::support::bands_clear(model, chain, &contacts, tol).map_err(|b| b.error)
3085}
3086
3087const LAW_STATIONS: u32 = 32;
3090
3091fn law_ball_on_faces(
3108 model: &Model,
3109 solid: &Shape,
3110 edge: &Shape,
3111 law: &Profile,
3112 stations: Option<bool>,
3113 tol: Tolerances,
3114) -> OgeomResult<()> {
3115 if let Some(reversed) = stations {
3116 let fractions: Vec<f64> = (1..LAW_STATIONS)
3117 .map(|k| f64::from(k) / f64::from(LAW_STATIONS))
3118 .collect();
3119 let radius_at = law_on_edge(model, edge, law, reversed, tol)?;
3120 crate::support::ball_fits_by(model, solid, edge, &*radius_at, &fractions, false, tol)?;
3121 crate::support::ball_fits(model, solid, edge, law.largest(), false, tol)?;
3127 return Ok(());
3128 }
3129 for radius in [law.least(), law.largest()] {
3130 let deflection = ogeom_mesh::Deflection {
3131 chord: (radius * 1e-3).max(tol.confusion() * 1e2),
3132 ..ogeom_mesh::Deflection::default()
3133 };
3134 for contact in crate::support::ball_fits(model, solid, edge, radius, false, tol)? {
3135 let on =
3136 ogeom_algo::classify_on_face(model, &contact.face, contact.at, deflection, tol)?;
3137 if on == ogeom_algo::Containment::Out {
3138 ogeom_bail!(
3139 Construction,
3140 "a variable radius whose ball at radius {radius} runs off a \
3141 face beside the edge (a narrow face's far edge) is still \
3142 owed; see docs/CAPABILITIES.md, fillet.variable"
3143 );
3144 }
3145 }
3146 }
3147 Ok(())
3148}
3149
3150fn chain_mates(model: &Model, chain: &[Shape], tol: Tolerances) -> OgeomResult<Vec<Mate>> {
3153 use ogeom_geom::Curve3d as _;
3154 chain
3155 .iter()
3156 .map(|e| -> OgeomResult<Mate> {
3157 let (curve, range) = edge_curve(model, e, tol)?;
3158 let end_of = |t: f64, leaving: f64| -> OgeomResult<(Point, Vector)> {
3159 let d = curve.d1_at(t, tol)?;
3160 let m = d.magnitude();
3161 let unit = if m > tol.confusion() {
3162 d / m
3163 } else {
3164 Vector::ZERO
3165 };
3166 Ok((curve.point_at(t, tol)?, unit * leaving))
3167 };
3168 Ok(Mate {
3169 ends: [end_of(range.0, 1.0)?, end_of(range.1, -1.0)?],
3170 settled: [false, false],
3171 meets: [false, false],
3172 alone: [false, false],
3173 })
3174 })
3175 .collect()
3176}
3177
3178fn chain_law(
3203 model: &mut Model,
3204 solid: &Shape,
3205 chain: &[Shape],
3206 law: &RadiusLaw,
3207 tol: Tolerances,
3208) -> OgeomResult<Built> {
3209 let mates = chain_mates(model, chain, tol)?;
3210 let runs: Vec<[Point; 2]> = chain
3212 .iter()
3213 .zip(&mates)
3214 .map(|(e, m)| {
3215 let [(a, _), (b, _)] = m.ends;
3216 if e.orientation() == ogeom_topo::Orientation::Reversed {
3217 [b, a]
3218 } else {
3219 [a, b]
3220 }
3221 })
3222 .collect();
3223 let near = |p: Point, q: Point| p.distance(q) <= tol.confusion() * 1e3;
3224 let mut walk: Vec<(usize, bool)> = vec![(0, true)];
3226 let mut used = vec![false; chain.len()];
3227 used[0] = true;
3228 let mut head = runs[0][1];
3229 while let Some(k) =
3230 (0..chain.len()).find(|&k| !used[k] && runs[k].iter().any(|p| near(*p, head)))
3231 {
3232 let with = near(runs[k][0], head);
3233 used[k] = true;
3234 head = if with { runs[k][1] } else { runs[k][0] };
3235 walk.push((k, with));
3236 }
3237 let closed = near(head, runs[0][0]);
3238 if !closed {
3239 let mut tail = runs[0][0];
3240 while let Some(k) =
3241 (0..chain.len()).find(|&k| !used[k] && runs[k].iter().any(|p| near(*p, tail)))
3242 {
3243 let with = near(runs[k][1], tail);
3244 used[k] = true;
3245 tail = if with { runs[k][0] } else { runs[k][1] };
3246 walk.insert(0, (k, with));
3247 }
3248 }
3249 if used.iter().any(|u| !u) {
3250 ogeom_bail!(
3251 Construction,
3252 "a variable radius over a chain whose edges do not run end to end \
3253 rounds nothing; see docs/CAPABILITIES.md, fillet.variable"
3254 );
3255 }
3256 let tangent = crate::junction::tangent_junctions(chain, &mates, tol);
3257 let profile = Profile::new(law, closed, tol)?;
3258 if profile.level(tol) {
3259 let radii = vec![profile.largest(); chain.len()];
3260 return fillet_edges_meeting(model, solid, chain, &radii, tol);
3261 }
3262 let mut lengths = Vec::with_capacity(walk.len());
3264 for &(k, _) in &walk {
3265 let (curve, range) = edge_curve(model, &chain[k], tol)?;
3266 lengths.push(crate::law::Ruler::new(curve, range, false, tol)?.length());
3267 }
3268 let total: f64 = lengths.iter().sum();
3269 let mut meetings: Vec<([usize; 2], Point, f64)> = Vec::new();
3272 let mut before = 0.0;
3273 for (step, &(k, with)) in walk.iter().enumerate() {
3274 before += lengths[step];
3275 let Some(&(next, _)) = walk
3276 .get(step + 1)
3277 .or(if closed { walk.first() } else { None })
3278 else {
3279 continue;
3280 };
3281 let at = if with { runs[k][1] } else { runs[k][0] };
3282 meetings.push(([k, next], at, profile.at(before / total).0));
3283 }
3284 let mut junctions: Vec<crate::junction::Junction> = Vec::new();
3288 let mut in_rounds = false;
3292 for &([i, j], at, radius) in &meetings {
3293 if let Some(found) = tangent
3294 .iter()
3295 .find(|t| t.slot(i).is_some() && t.slot(j).is_some() && near(t.at, at))
3296 {
3297 junctions.push(found.clone());
3298 continue;
3299 }
3300 let hosts: Vec<Vec<Shape>> = [i, j]
3301 .iter()
3302 .map(|&k| -> OgeomResult<Vec<Shape>> {
3303 let mut faces: Vec<Shape> = Vec::new();
3304 for f in ogeom_topo::ancestors_of(model, solid, &chain[k], ShapeType::Face)? {
3305 if !faces.iter().any(|g| g.is_same(&f)) {
3306 faces.push(f);
3307 }
3308 }
3309 Ok(faces)
3310 })
3311 .collect::<OgeomResult<_>>()?;
3312 let turning = crate::junction::junctions(
3313 model,
3314 solid,
3315 &[chain[i].clone(), chain[j].clone()],
3316 &hosts,
3317 &[mates[i], mates[j]],
3318 &[radius, radius],
3319 tol,
3320 )?
3321 .into_iter()
3322 .find(|t| near(t.at, at));
3323 let planar = |model: &Model, k: usize| -> OgeomResult<bool> {
3324 Ok(
3325 edge_curve(model, &chain[k], tol).is_ok_and(|(c, _)| matches!(c, Curve::Line(_)))
3326 && matches!(
3327 route(model, solid, &chain[k], Rims::Exact, tol)?,
3328 Route::Planar
3329 ),
3330 )
3331 };
3332 let straight = planar(model, i)? && planar(model, j)?;
3333 let mixed = planar(model, i)? != planar(model, j)?;
3341 let in_turn = |mixed: bool| -> OgeomResult<()> {
3342 if mixed {
3343 ogeom_bail!(
3344 Construction,
3345 "a variable radius over a chain turning where a marched \
3346 band meets a straight one between planes is still owed: \
3347 under a law the marched band is fitted, and its end meets \
3348 the straight band in what the boolean does not melt; see \
3349 docs/CAPABILITIES.md, fillet.variable"
3350 );
3351 }
3352 Ok(())
3353 };
3354 let Some(mut turning) = turning else {
3355 in_turn(mixed)?;
3358 in_rounds = true;
3359 continue;
3360 };
3361 turning.pair = [i, j];
3362 if !straight && turning.crossed && !turning.reentrant {
3363 in_turn(mixed)?;
3364 in_rounds = true;
3365 continue;
3366 }
3367 let convex = turning.crossed && !turning.reentrant;
3368 let reentrant = turning.reentrant;
3369 if !straight || !(convex || reentrant) {
3370 ogeom_bail!(
3371 Construction,
3372 "a variable radius over a chain turning where it meets is \
3373 rounded between straight creases across a convex or a \
3374 re-entrant turn, and a curved crease across a convex turn; \
3375 any other turn is still owed; see docs/CAPABILITIES.md, \
3376 fillet.variable"
3377 );
3378 }
3379 junctions.push(turning);
3380 }
3381 let mut owns: Vec<Profile> = Vec::with_capacity(walk.len());
3384 let mut before = 0.0;
3385 for (&(_, with), length) in walk.iter().zip(&lengths) {
3386 let (from, to) = (before / total, (before + length) / total);
3387 before += length;
3388 owns.push(if with {
3389 profile.window(from, to)
3390 } else {
3391 profile.window(to, from)
3392 });
3393 }
3394 if in_rounds {
3398 if !junctions.is_empty() {
3399 ogeom_bail!(
3400 Construction,
3401 "a variable radius over a chain that turns where a curved \
3402 crease meets and also joins bands across another junction is \
3403 still owed; see docs/CAPABILITIES.md, fillet.variable"
3404 );
3405 }
3406 let mut laws: Vec<Profile> = owns.clone();
3407 let mut radii: Vec<f64> = vec![0.0; chain.len()];
3408 for (&(k, _), own) in walk.iter().zip(&owns) {
3409 laws[k] = own.clone();
3410 radii[k] = own.largest();
3411 }
3412 for (edge, own) in chain.iter().zip(&laws) {
3413 law_ball_on_faces(model, solid, edge, own, None, tol)?;
3414 }
3415 return fillet_edges_own(
3416 model,
3417 solid,
3418 chain,
3419 &radii,
3420 None,
3421 Rims::Exact,
3422 Some(&laws),
3423 tol,
3424 );
3425 }
3426 {
3431 let mut edges: Vec<Shape> = Vec::with_capacity(walk.len());
3432 let mut radii: Vec<f64> = Vec::with_capacity(walk.len());
3433 for (&(k, _), own) in walk.iter().zip(&owns) {
3434 edges.push(chain[k].clone());
3435 radii.push(own.largest());
3436 }
3437 let rests = request_fits(model, solid, &edges, &radii, tol).map_err(|b| b.error)?;
3438 if rests.iter().any(|&r| r) {
3439 ogeom_bail!(
3440 Construction,
3441 "a variable radius over a chain one of whose balls rests on \
3442 the far edge of a narrow face beside it is still owed; see \
3443 docs/CAPABILITIES.md, fillet.variable"
3444 );
3445 }
3446 }
3447 let mut group: Vec<usize> = (0..chain.len()).collect();
3456 for junction in junctions.iter().filter(|j| !j.crossed) {
3457 let [a, b] = junction.pair;
3458 let (from, to) = (group[a], group[b]);
3459 for g in &mut group {
3460 if *g == from {
3461 *g = to;
3462 }
3463 }
3464 }
3465 let mut halved: Vec<usize> = Vec::new();
3466 for junction in junctions.iter().filter(|j| j.crossed && !j.reentrant) {
3467 let [a, b] = junction.pair;
3468 if group[a] != group[b] || halved.iter().any(|&h| group[h] == group[a]) {
3469 continue;
3470 }
3471 let between = (0..chain.len()).find(|&m| {
3472 group[m] == group[a]
3473 && junctions
3474 .iter()
3475 .filter(|j| !j.crossed && j.slot(m).is_some())
3476 .count()
3477 == 2
3478 && edge_curve(model, &chain[m], tol).is_ok_and(|(c, _)| matches!(c, Curve::Line(_)))
3479 });
3480 let Some(m) = between else {
3481 ogeom_bail!(
3482 Construction,
3483 "a variable radius over a loop joined end to end round a convex \
3484 turn with no straight edge between two of its junctions to \
3485 open it at is still owed; see docs/CAPABILITIES.md, \
3486 fillet.variable"
3487 );
3488 };
3489 halved.push(m);
3490 }
3491 {
3492 let mut laws: Vec<Option<&Profile>> = vec![None; chain.len()];
3493 for (&(k, _), own) in walk.iter().zip(&owns) {
3494 laws[k] = Some(own);
3495 }
3496 let laws: Vec<&Profile> = laws.into_iter().flatten().collect();
3497 if laws.len() != chain.len() {
3498 ogeom_bail!(Invariant, "a chain's walk takes every edge once");
3499 }
3500 law_chain_fits(model, solid, chain, &laws, tol)?;
3501 }
3502 let mut set_aside: Vec<(Vec<usize>, Shape, crate::support::Wedge)> = Vec::new();
3503 for ((&(k, _), own), _) in walk.iter().zip(&owns).zip(&lengths) {
3504 let edge = &chain[k];
3505 let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
3506 let (curve, _) = edge_curve(model, edge, tol)?;
3507 if halved.contains(&k) {
3508 for (part, (from, to)) in [((0.0, 0.5), (0.0, 0.5)), ((0.5, 1.0), (0.5, 1.0))] {
3511 let law = if reversed {
3512 own.window(1.0 - to, 1.0 - from)
3513 } else {
3514 own.window(from, to)
3515 };
3516 let (built, wedges) = crate::support::collecting_wedges(|| {
3517 straight_law_fillet(model, solid, edge, &law, reversed, part, tol)
3518 });
3519 built?;
3520 let [wedge] = <[crate::support::Wedge; 1]>::try_from(wedges).map_err(|w| {
3521 ogeom_core::ogeom_err!(
3522 Construction,
3523 "a half band of a chain under a law built {} wedges, not one",
3524 w.len()
3525 )
3526 })?;
3527 set_aside.push((vec![k], edge.clone(), wedge));
3528 }
3529 continue;
3530 }
3531 let (built, wedges) = crate::support::collecting_wedges(|| -> OgeomResult<Built> {
3532 if matches!(curve, Curve::Line(_)) {
3533 if !matches!(route(model, solid, edge, Rims::Exact, tol)?, Route::Planar) {
3534 ogeom_bail!(
3535 Construction,
3536 "a variable radius on a straight edge of a chain other \
3537 than one between two planes is still owed; see \
3538 docs/CAPABILITIES.md, fillet.variable"
3539 );
3540 }
3541 straight_law_fillet(model, solid, edge, own, reversed, (0.0, 1.0), tol)
3542 } else {
3543 crate::marched::marched_fillet_law(
3544 model,
3545 solid,
3546 edge,
3547 own,
3548 None,
3549 Some((k, &mates)),
3550 tol,
3551 )
3552 }
3553 });
3554 built?;
3555 let [wedge] = <[crate::support::Wedge; 1]>::try_from(wedges).map_err(|w| {
3556 ogeom_core::ogeom_err!(
3557 Construction,
3558 "a band of a chain under a law built {} wedges, not one",
3559 w.len()
3560 )
3561 })?;
3562 set_aside.push((vec![k], edge.clone(), wedge));
3563 }
3564 let gap = crate::marched::band_fit_target(tol) * 2.0;
3571 let run_on = run_on_past_turns(
3572 model,
3573 solid,
3574 chain,
3575 (&walk, &owns, &halved),
3576 (&junctions, &meetings),
3577 &mut set_aside,
3578 tol,
3579 )?;
3580 for (junction, &(_, _, radius)) in junctions.iter().zip(&meetings) {
3581 if junction.tangent {
3582 continue;
3583 }
3584 for slot in 0..2 {
3585 let index = junction.pair[slot];
3586 let edge = chain[index].clone();
3587 let Some(entry) = set_aside
3588 .iter_mut()
3589 .find(|(held, _, _)| held.contains(&index))
3590 else {
3591 continue;
3592 };
3593 let outward = junction.outward[slot];
3594 if junction.crossed && junction.reentrant {
3598 continue;
3599 }
3600 if junction.reentrant {
3601 let Some(cap) =
3602 crate::junction::cap_at(model, &entry.2.solid, junction.at, outward, tol)?
3603 else {
3604 ogeom_bail!(
3605 Construction,
3606 "a band meeting another across a re-entrant turn has no \
3607 cap at the vertex to carry on from"
3608 );
3609 };
3610 let hosts: Vec<Shape> =
3611 ogeom_topo::ancestors_of(model, solid, &edge, ShapeType::Face)?;
3612 entry.2.solid = crate::junction::carried_into(
3613 model,
3614 &entry.2.solid,
3615 &cap,
3616 (&edge, &hosts),
3617 outward,
3618 junction.keeps(slot),
3619 tol,
3620 )?;
3621 continue;
3622 }
3623 if !edge_curve(model, &edge, tol).is_ok_and(|(c, _)| matches!(c, Curve::Line(_)))
3626 || run_on
3627 .iter()
3628 .any(|&(k, at)| k == index && at.distance(junction.at) <= tol.confusion())
3629 {
3630 continue;
3631 }
3632 let (built, wedges) = crate::support::collecting_wedges(|| {
3635 planar_fillet(model, solid, &edge, Section::Ball(radius), None, tol)
3636 });
3637 built?;
3638 let Some(alone) = wedges.into_iter().next() else {
3639 continue;
3640 };
3641 let mut past = f64::NEG_INFINITY;
3644 for vertex in explore_unique(model, &alone.solid, ShapeType::Vertex)? {
3645 if let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) {
3646 let p = vertex.transform(model.datums())?.apply(data.point);
3647 past = past.max((p - junction.at).dot(outward));
3648 }
3649 }
3650 if past <= gap {
3651 continue;
3652 }
3653 let run_out = crate::support::trimmed_wedge(
3654 model,
3655 &alone.solid,
3656 (junction.at, outward),
3657 ogeom_algo::shape_bounds(model, &alone.solid, tol)?.diagonal() * 2.0,
3658 tol,
3659 )?;
3660 let (solid_joined, _) = crate::junction::joined(
3661 model,
3662 [&entry.2.solid, &run_out],
3663 (junction.at, outward),
3664 junction.at,
3665 gap,
3666 tol,
3667 )?;
3668 entry.2.solid = solid_joined;
3669 }
3670 }
3671 let halves = with_corners(model, &mut set_aside, &junctions, solid, true, tol)?;
3672 let joined = join_flush(model, set_aside, &junctions, &halves, tol)?;
3673 let mut built = apply_set_aside(model, Built::from_nothing(solid.clone()), joined, tol)?;
3674 for edge in chain {
3675 built.history.delete(edge);
3676 }
3677 Ok(built)
3678}
3679
3680type Meeting = ([usize; 2], Point, f64);
3683
3684fn run_on_past_turns(
3697 model: &mut Model,
3698 solid: &Shape,
3699 chain: &[Shape],
3700 (walk, owns, halved): (&[(usize, bool)], &[Profile], &[usize]),
3701 (junctions, meetings): (&[crate::junction::Junction], &[Meeting]),
3702 set_aside: &mut [(Vec<usize>, Shape, crate::support::Wedge)],
3703 tol: Tolerances,
3704) -> OgeomResult<Vec<(usize, Point)>> {
3705 let gap = crate::marched::band_fit_target(tol) * 2.0;
3706 let mut ends: Vec<[f64; 2]> = vec![[0.0; 2]; chain.len()];
3709 let mut alone_at: Vec<(usize, Point)> = Vec::new();
3710 for (junction, &(_, _, radius)) in junctions.iter().zip(meetings) {
3711 if junction.tangent || junction.reentrant || !junction.crossed {
3712 continue;
3713 }
3714 let (meet, bisector) = junction.flush;
3715 let reflect = |v: Vector| v - bisector * (2.0 * v.dot(bisector));
3716 let miss = radius.mul_add(
3717 (reflect(junction.sides[0]) - junction.sides[1]).magnitude(),
3718 2.0 * (junction.at - meet).dot(bisector).abs(),
3719 );
3720 if miss <= crate::marched::band_fit_target(tol) {
3721 continue;
3722 }
3723 for slot in 0..2 {
3724 let index = junction.pair[slot];
3725 let edge = &chain[index];
3726 let (curve, range) = edge_curve(model, edge, tol)?;
3727 if !matches!(curve, Curve::Line(_)) || halved.contains(&index) {
3728 continue;
3729 }
3730 let outward = junction.outward[slot];
3731 let (built, wedges) = crate::support::collecting_wedges(|| {
3732 planar_fillet(model, solid, edge, Section::Ball(radius), None, tol)
3733 });
3734 built?;
3735 let Some(alone) = wedges.into_iter().next() else {
3736 continue;
3737 };
3738 let mut past = f64::NEG_INFINITY;
3739 for vertex in explore_unique(model, &alone.solid, ShapeType::Vertex)? {
3740 if let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) {
3741 let p = vertex.transform(model.datums())?.apply(data.point);
3742 past = past.max((p - junction.at).dot(outward));
3743 }
3744 }
3745 let far = junction.at + outward * past;
3750 if past <= gap
3751 || !crate::junction::ends_in(model, &alone.solid, (far, outward), far, gap, tol)?
3752 {
3753 continue;
3754 }
3755 let first = ogeom_geom::Curve3d::point_at(&curve, range.0, tol)?;
3756 let last = ogeom_geom::Curve3d::point_at(&curve, range.1, tol)?;
3757 let start = if edge.orientation() == ogeom_topo::Orientation::Reversed {
3758 last
3759 } else {
3760 first
3761 };
3762 let end = usize::from(start.distance(junction.at) > first.distance(last) / 2.0);
3763 ends[index][end] = ends[index][end].max(past / first.distance(last));
3764 alone_at.push((index, junction.at));
3765 }
3766 }
3767 let mut run_on = Vec::new();
3768 for (index, &[before, after]) in ends.iter().enumerate() {
3769 if before == 0.0 && after == 0.0 {
3770 continue;
3771 }
3772 let Some(slot) = walk.iter().position(|&(k, _)| k == index) else {
3773 continue;
3774 };
3775 let edge = &chain[index];
3776 let law = owns[slot].stretched(before, after);
3777 let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
3778 let part = if reversed {
3779 (-after, 1.0 + before)
3780 } else {
3781 (-before, 1.0 + after)
3782 };
3783 let (built, wedges) = crate::support::collecting_wedges(|| {
3784 straight_law_fillet(model, solid, edge, &law, reversed, part, tol)
3785 });
3786 built?;
3787 let [wedge] = <[crate::support::Wedge; 1]>::try_from(wedges).map_err(|w| {
3788 ogeom_core::ogeom_err!(
3789 Construction,
3790 "a band run on past its turns built {} wedges, not one",
3791 w.len()
3792 )
3793 })?;
3794 let Some(entry) = set_aside
3795 .iter_mut()
3796 .find(|(held, _, _)| held.contains(&index))
3797 else {
3798 continue;
3799 };
3800 entry.2 = wedge;
3801 for (_, at) in alone_at.iter().filter(|(k, _)| *k == index) {
3802 run_on.push((index, *at));
3803 }
3804 }
3805 Ok(run_on)
3806}
3807
3808fn straight_law_fillet(
3821 model: &mut Model,
3822 solid: &Shape,
3823 edge: &Shape,
3824 law: &Profile,
3825 reversed: bool,
3826 part: (f64, f64),
3827 tol: Tolerances,
3828) -> OgeomResult<Built> {
3829 let seat = planar_seat(model, solid, edge, tol)?;
3830 straight_law_on(model, solid, edge, seat, law, reversed, part, tol)
3831}
3832
3833#[allow(clippy::too_many_arguments, reason = "one construction, all its data")]
3836#[allow(clippy::too_many_lines, reason = "one wedge, assembled end to end")]
3837fn straight_law_on(
3838 model: &mut Model,
3839 solid: &Shape,
3840 edge: &Shape,
3841 mut seat: crate::support::Seat,
3842 law: &Profile,
3843 reversed: bool,
3844 part: (f64, f64),
3845 tol: Tolerances,
3846) -> OgeomResult<Built> {
3847 let (whole_start, whole_end) = (seat.start, seat.end);
3848 seat.start = whole_start + (whole_end - whole_start) * part.0;
3849 seat.end = whole_start + (whole_end - whole_start) * part.1;
3850 let (first, second) = if seat
3851 .along
3852 .dot(seat.normals[0].cross(seat.normals[1]))
3853 .is_sign_positive()
3854 {
3855 (0, 1)
3856 } else {
3857 (1, 0)
3858 };
3859 let sign = if seat.convex { 1.0 } else { -1.0 };
3860 let n1 = seat.normals[first] * sign;
3861 let n2 = seat.normals[second] * sign;
3862 let a = seat.leg(first, tol)? * sign;
3863 let b = seat.leg(second, tol)? * sign;
3864 let bisector = {
3865 let u = a + b;
3866 let m = u.magnitude();
3867 if m <= tol.angular() {
3868 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
3869 }
3870 u / m
3871 };
3872 let depth = -n1.dot(bisector);
3873 if depth <= tol.angular() {
3874 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
3875 }
3876 let sweep = seat.along.dot(n1.cross(n2)).atan2(n1.dot(n2)) * sign;
3877 if sweep <= tol.angular() {
3878 ogeom_bail!(Construction, "the faces are parallel; there is no corner");
3879 }
3880
3881 let (knots, radii) = {
3885 let (knots, values) = law.spline(tol);
3886 if reversed {
3887 (
3888 knots.iter().rev().map(|k| 1.0 - k).collect::<Vec<f64>>(),
3889 values.into_iter().rev().collect::<Vec<f64>>(),
3890 )
3891 } else {
3892 (knots, values)
3893 }
3894 };
3895 let greville: Vec<f64> = (0..radii.len())
3896 .map(|i| (knots[i + 1] + knots[i + 2] + knots[i + 3]) / 3.0)
3897 .collect();
3898 let apex_at = |u: f64| seat.start + (seat.end - seat.start) * u;
3899 let apex = [seat.start, seat.end];
3900 let end_radii = [radii[0], radii[radii.len() - 1]];
3901 let centre = [
3902 apex[0] + bisector * (end_radii[0] / depth),
3903 apex[1] + bisector * (end_radii[1] / depth),
3904 ];
3905 let contact_a = [centre[0] + n1 * end_radii[0], centre[1] + n1 * end_radii[1]];
3906 let contact_b = [centre[0] + n2 * end_radii[0], centre[1] + n2 * end_radii[1]];
3907
3908 let half_cos = (sweep / 2.0).cos();
3909 let arc_mid = (n1 + n2) / (1.0 + n1.dot(n2));
3910 let weights = [1.0, half_cos, 1.0];
3911 let arms = [n1, arc_mid, n2];
3912 let lift = bisector / depth;
3913
3914 let u_knots = ogeom_math::KnotVector::new(knots.clone(), 3)?;
3915 let v_knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], 2)?;
3916 let mut net: Vec<ogeom_math::Weighted<Point>> = Vec::with_capacity(radii.len() * 3);
3917 for (g, r) in greville.iter().zip(&radii) {
3918 for (arm, w) in arms.iter().zip(weights) {
3919 net.push(ogeom_math::Weighted::new(
3920 apex_at(*g) + (lift + *arm) * *r,
3921 w,
3922 tol,
3923 )?);
3924 }
3925 }
3926 let grid = ogeom_math::ControlGrid::new(net, radii.len(), 3)?;
3927 let surface: SurfaceGeometry =
3928 ogeom_geom::BSplineSurface::rational(u_knots, v_knots, grid)?.into();
3929 let surface_id = model.geometry_mut().add_surface(surface.clone());
3930
3931 let va = [
3932 make_vertex(model, contact_a[0]).shape,
3933 make_vertex(model, contact_a[1]).shape,
3934 ];
3935 let vb = [
3936 make_vertex(model, contact_b[0]).shape,
3937 make_vertex(model, contact_b[1]).shape,
3938 ];
3939
3940 let rail = |model: &mut Model,
3943 arm: Vector,
3944 ends: [&Shape; 2],
3945 row: f64|
3946 -> OgeomResult<Shape> {
3947 let control: Vec<Point> = greville
3948 .iter()
3949 .zip(&radii)
3950 .map(|(g, r)| apex_at(*g) + (lift + arm) * *r)
3951 .collect();
3952 let knot_vector = ogeom_math::KnotVector::new(knots.clone(), 3)?;
3953 let curve = Curve::BSpline(ogeom_geom::BSplineCurve::new(knot_vector, control, tol)?);
3954 let built = make_edge_between(model, curve, (0.0, 1.0), ends[0], ends[1], tol)?.shape;
3955 let image = ogeom_geom::Line2d::over(
3956 ogeom_math::Axis2::new(ogeom_math::Point2::new(0.0, row), ogeom_math::Direction2::X),
3957 0.0,
3958 1.0,
3959 )?;
3960 ogeom_algo::attach_pcurve(
3961 model,
3962 &built,
3963 image.into(),
3964 surface_id,
3965 ogeom_topo::Location::identity(),
3966 (0.0, 1.0),
3967 )?;
3968 Ok(built)
3969 };
3970 let rail_a = rail(model, n1, [&va[0], &va[1]], 0.0)?;
3971 let rail_b = rail(model, n2, [&vb[0], &vb[1]], 1.0)?;
3972
3973 let arc_edge = |model: &mut Model, i: usize| -> OgeomResult<Shape> {
3974 let knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], 2)?;
3975 let control = vec![
3976 ogeom_math::Weighted::new(contact_a[i], 1.0, tol)?,
3977 ogeom_math::Weighted::new(centre[i] + arc_mid * end_radii[i], half_cos, tol)?,
3978 ogeom_math::Weighted::new(contact_b[i], 1.0, tol)?,
3979 ];
3980 let curve = Curve::BSpline(ogeom_geom::BSplineCurve::rational(knots, control)?);
3981 let built = make_edge_between(model, curve, (0.0, 1.0), &va[i], &vb[i], tol)?.shape;
3982 #[allow(clippy::cast_precision_loss)]
3983 let column = ogeom_geom::Line2d::over(
3984 ogeom_math::Axis2::new(
3985 ogeom_math::Point2::new(i as f64, 0.0),
3986 ogeom_math::Direction2::Y,
3987 ),
3988 0.0,
3989 1.0,
3990 )?;
3991 ogeom_algo::attach_pcurve(
3992 model,
3993 &built,
3994 column.into(),
3995 surface_id,
3996 ogeom_topo::Location::identity(),
3997 (0.0, 1.0),
3998 )?;
3999 Ok(built)
4000 };
4001 let arc0 = arc_edge(model, 0)?;
4002 let arc1 = arc_edge(model, 1)?;
4003
4004 let blend = {
4005 let wire = ogeom_algo::make_wire(
4006 model,
4007 &[
4008 rail_a.clone(),
4009 arc1.clone(),
4010 rail_b.reversed(),
4011 arc0.reversed(),
4012 ],
4013 tol,
4014 )?
4015 .shape;
4016 let face =
4017 ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
4018 use ogeom_geom::Surface as _;
4019 let s_mid = surface.point_at(0.5, 0.5, tol)?;
4020 let (du, dv) = surface.d1_at(0.5, 0.5, tol)?;
4021 let apex_mid = apex[0].midpoint(apex[1]);
4022 let outward = s_mid - apex_mid.midpoint(s_mid);
4023 if du.cross(dv).dot(outward) >= 0.0 {
4024 face
4025 } else {
4026 face.reversed()
4027 }
4028 };
4029
4030 let plane_face = |model: &mut Model,
4033 corners: [Point; 3],
4034 edges: Vec<Shape>,
4035 outward: Vector|
4036 -> OgeomResult<Shape> {
4037 let plane = Plane::through(corners[0], Direction::new(outward, tol)?);
4038 let reach = corners
4039 .iter()
4040 .fold(1.0_f64, |m, p| m.max(p.distance(corners[0])))
4041 * 2.0
4042 + seat.start.distance(seat.end) * 2.0;
4043 let surface: SurfaceGeometry =
4044 PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
4045 let turn = (corners[1] - corners[0]).cross(corners[2] - corners[0]);
4046 let edges: Vec<Shape> = if turn.dot(outward) < 0.0 {
4047 edges.iter().rev().map(Shape::reversed).collect()
4048 } else {
4049 edges
4050 };
4051 Ok(ogeom_algo::make_face_with_pcurves(model, surface, &[edges], tol)?.shape)
4052 };
4053 let apex_v = [
4054 make_vertex(model, apex[0]).shape,
4055 make_vertex(model, apex[1]).shape,
4056 ];
4057 let leg = |model: &mut Model,
4058 rail: &Shape,
4059 ends: &[Shape; 2],
4060 contact: [Point; 2],
4061 outward: Vector|
4062 -> OgeomResult<Shape> {
4063 let edges = vec![
4064 segment_between(model, (&apex_v[0], apex[0]), (&ends[0], contact[0]), tol)?,
4065 rail.clone(),
4066 segment_between(model, (&ends[1], contact[1]), (&apex_v[1], apex[1]), tol)?,
4067 segment_between(model, (&apex_v[1], apex[1]), (&apex_v[0], apex[0]), tol)?,
4068 ];
4069 plane_face(model, [apex[0], contact[0], contact[1]], edges, outward)
4070 };
4071 let leg_a = leg(model, &rail_a, &va, contact_a, n1 * sign)?;
4072 let leg_b = leg(model, &rail_b, &vb, contact_b, n2 * sign)?;
4073 let cap = |model: &mut Model, i: usize, outward: Vector| -> OgeomResult<Shape> {
4074 let edges = vec![
4075 segment_between(model, (&apex_v[i], apex[i]), (&va[i], contact_a[i]), tol)?,
4076 if i == 0 { arc0.clone() } else { arc1.clone() },
4077 segment_between(model, (&vb[i], contact_b[i]), (&apex_v[i], apex[i]), tol)?,
4078 ];
4079 plane_face(model, [apex[i], contact_a[i], contact_b[i]], edges, outward)
4080 };
4081 let cap0 = cap(model, 0, -seat.along)?;
4082 let cap1 = cap(model, 1, seat.along)?;
4083
4084 let faces = [leg_a, leg_b, cap0, cap1, blend];
4085 apply_wedge(model, solid, Some(edge), &faces, !seat.convex, tol)
4086}
4087
4088pub(crate) fn planar_fillet(
4090 model: &mut Model,
4091 solid: &Shape,
4092 edge: &Shape,
4093 section: Section,
4094 mates: Option<(usize, &[Mate])>,
4095 tol: Tolerances,
4096) -> OgeomResult<Built> {
4097 let radius = section.size();
4098 let mut seat = planar_seat(model, solid, edge, tol)?;
4099 let mut trims: Vec<(Point, Vector)> = Vec::new();
4106 let mut trimmed = [false; 2];
4107 for (slot, end) in [true, false].into_iter().enumerate() {
4108 if !seat.convex {
4109 break;
4110 }
4111 let at = if end { seat.start } else { seat.end };
4112 if mates.is_some_and(|(index, mates)| Mate::meets_at(mates, index, at, tol)) {
4114 continue;
4115 }
4116 let outward = if end { -seat.along } else { seat.along };
4117 let hosts = [&seat.faces[0], &seat.faces[1]];
4118 let Some((origin, normal)) =
4119 crate::support::overhang_at(model, solid, edge, hosts, at, outward, tol)?
4120 else {
4121 continue;
4122 };
4123 let Some(inward) = section.centre(&seat, tol)? else {
4124 continue;
4125 };
4126 let rate = -outward.dot(normal);
4129 let crossings = section
4130 .points(&seat, at, inward, tol)?
4131 .map(|p| (origin - p).dot(normal) / rate);
4132 if crossings.iter().all(|c| c.abs() <= tol.confusion() * 1e3) {
4133 continue;
4134 }
4135 let reach = crossings.iter().fold(0.0_f64, |m, c| m.max(-*c)) + radius * 0.25;
4136 if end {
4137 seat.start -= seat.along * reach;
4138 } else {
4139 seat.end += seat.along * reach;
4140 }
4141 trims.push((at - normal * (at - origin).dot(normal), normal));
4142 trimmed[slot] = true;
4143 }
4144 let mut probes = crate::support::LazyProbe::default();
4157 for (slot, end) in [true, false].into_iter().enumerate() {
4158 if trimmed[slot] {
4160 continue;
4161 }
4162 let at = if end { seat.start } else { seat.end };
4163 let outward = if end { -seat.along } else { seat.along };
4164 if let Some((index, mates)) = mates {
4165 let chain = mates.iter().enumerate().any(|(i, mate)| {
4169 i != index
4170 && mate.ends.iter().any(|(p, leaving)| {
4171 p.distance(at) <= tol.confusion() * 1e3
4172 && leaving.cross(outward).magnitude() <= 1e-2
4173 && leaving.dot(outward) > 0.0
4174 })
4175 });
4176 if chain {
4177 continue;
4178 }
4179 if Mate::settled_at(mates, index, at, tol) || Mate::meets_at(mates, index, at, tol) {
4182 continue;
4183 }
4184 }
4185 let hosts = [&seat.faces[0], &seat.faces[1]];
4186 if !crate::marched::crease_terminates_at(model, solid, edge, hosts, at, tol)? {
4187 continue;
4188 }
4189 let through_blend =
4190 crate::marched::neighbour_blend_at(model, solid, hosts, at, seat.convex, radius, tol)?;
4191 if through_blend && mates.is_none() || !through_blend && !seat.convex {
4194 continue;
4195 }
4196 let Some(inward) = section.centre(&seat, tol)? else {
4198 continue;
4199 };
4200 let centre_at = |s: f64| at + outward * s + inward;
4201 let step = radius / 8.0;
4202 let mut reach: Option<f64> = None;
4203 if through_blend {
4204 let Some(probe) = probes.get(model, solid, tol)? else {
4205 continue;
4206 };
4207 for k in 1..=32 {
4208 let s = step * f64::from(k);
4209 let inside = probe.holds(model, centre_at(s), tol)? == ogeom_algo::Containment::In;
4210 if inside != seat.convex {
4211 reach = Some(s + radius * 0.25);
4212 break;
4213 }
4214 }
4215 } else {
4216 let points = section.points(&seat, at, inward, tol)?;
4224 let boss = mates.is_none_or(|(index, mates)| Mate::alone_at(mates, index, at, tol));
4225 reach = crate::support::run_out_reach(
4226 model,
4227 solid,
4228 edge,
4229 hosts,
4230 (at, outward),
4231 &points,
4232 (radius, boss),
4233 &mut probes,
4234 tol,
4235 )?;
4236 }
4237 if std::env::var_os("OGEOM_DEBUG_RUNOUT").is_some() {
4238 eprintln!("RUNOUT straight seat end {end}: reach {reach:?}");
4239 }
4240 let Some(mut reach) = reach else {
4241 continue;
4242 };
4243 if let Some(mate) = mates.and_then(|(index, mates)| mates.get(index)) {
4250 let own = mate
4251 .ends
4252 .iter()
4253 .map(|(p, _)| *p)
4254 .min_by(|p, q| p.distance(at).total_cmp(&q.distance(at)));
4255 let to_own = own.map(|own| (own - at).dot(outward));
4256 if let Some(to_own) = to_own.filter(|&t| t > tol.confusion() && t < reach)
4257 && let Some(probe) = probes.get(model, solid, tol)?
4258 && probe.holds(model, centre_at(to_own + step), tol)? == ogeom_algo::Containment::In
4259 {
4260 reach = to_own;
4261 }
4262 }
4263 if end {
4264 seat.start -= seat.along * reach;
4265 } else {
4266 seat.end += seat.along * reach;
4267 }
4268 }
4269 match section {
4270 Section::Ball(radius) => {
4271 seated_fillet(model, solid, &seat, radius, Some(edge), &trims, tol)
4272 }
4273 Section::Conic { .. } => {
4274 crate::asym::seated_conic(model, solid, &seat, section, Some(edge), &trims, tol)
4275 }
4276 }
4277}
4278
4279pub(crate) fn seat_centre(
4283 seat: &Seat,
4284 radius: f64,
4285 tol: Tolerances,
4286) -> OgeomResult<Option<Vector>> {
4287 let sign = if seat.convex { 1.0 } else { -1.0 };
4288 let bisector = seat.leg(0, tol)? * sign + seat.leg(1, tol)? * sign;
4289 let m = bisector.magnitude();
4290 if m <= tol.angular() {
4291 return Ok(None);
4292 }
4293 let depth = seat.normals[0].dot(bisector / m).abs();
4294 if depth <= tol.angular() {
4295 return Ok(None);
4296 }
4297 Ok(Some(bisector / m * (radius / depth)))
4298}
4299
4300pub(crate) fn section_at(seat: &Seat, at: Point, inward: Vector, radius: f64) -> [Point; 6] {
4304 let centre = at + inward;
4305 let sign = if seat.convex { 1.0 } else { -1.0 };
4306 let touch = [0, 1].map(|i| centre + seat.normals[i] * (radius * sign));
4307 let arc = centre - inward * (radius / inward.magnitude());
4308 [
4309 at,
4310 touch[0],
4311 touch[1],
4312 at.midpoint(touch[0]),
4313 at.midpoint(touch[1]),
4314 arc,
4315 ]
4316}
4317
4318#[derive(Debug, Clone, Copy)]
4325pub(crate) struct BallSeat {
4326 pub first: usize,
4328 pub second: usize,
4330 pub n1: Vector,
4332 pub n2: Vector,
4334 pub sign: f64,
4336 pub offset: Vector,
4338 pub sweep: f64,
4340}
4341
4342pub(crate) fn ball_seat(seat: &Seat, radius: f64, tol: Tolerances) -> OgeomResult<BallSeat> {
4345 let (first, second) = if seat
4349 .along
4350 .dot(seat.normals[0].cross(seat.normals[1]))
4351 .is_sign_positive()
4352 {
4353 (0, 1)
4354 } else {
4355 (1, 0)
4356 };
4357 let n1 = seat.normals[first];
4358 let n2 = seat.normals[second];
4359 let sign = if seat.convex { 1.0 } else { -1.0 };
4363 let a = seat.leg(first, tol)? * sign;
4364 let b = seat.leg(second, tol)? * sign;
4365
4366 let bisector = {
4369 let u = a + b;
4370 let m = u.magnitude();
4371 if m <= tol.angular() {
4372 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
4373 }
4374 u / m
4375 };
4376 let depth = (n1.dot(bisector)).abs();
4377 if depth <= tol.angular() {
4378 ogeom_bail!(Construction, "the faces meet too sharply to seat a fillet");
4379 }
4380 let offset = bisector * (radius / depth);
4381 let sweep = seat.along.dot(n1.cross(n2)).atan2(n1.dot(n2));
4382 if sweep <= tol.angular() {
4383 ogeom_bail!(Construction, "the faces are parallel; there is no corner");
4384 }
4385 Ok(BallSeat {
4386 first,
4387 second,
4388 n1,
4389 n2,
4390 sign,
4391 offset,
4392 sweep,
4393 })
4394}
4395
4396pub(crate) fn seated_fillet(
4404 model: &mut Model,
4405 solid: &Shape,
4406 seat: &Seat,
4407 radius: f64,
4408 edge: Option<&Shape>,
4409 trims: &[(Point, Vector)],
4410 tol: Tolerances,
4411) -> OgeomResult<Built> {
4412 let BallSeat {
4413 n1,
4414 n2,
4415 sign,
4416 offset,
4417 sweep,
4418 ..
4419 } = ball_seat(seat, radius, tol)?;
4420 let centre = seat.start + offset;
4421
4422 let travel = seat.end - seat.start;
4423 let length = travel.magnitude();
4424 let apex0 = seat.start;
4425 let apex1 = seat.end;
4426 let contact_a0 = centre + n1 * (radius * sign);
4427 let contact_b0 = centre + n2 * (radius * sign);
4428 let contact_a1 = contact_a0 + travel;
4429 let contact_b1 = contact_b0 + travel;
4430
4431 let along_dir = Direction::new(seat.along, tol)?;
4432 let radial_dir = Direction::new(n1 * sign, tol)?;
4433
4434 let arc = |model: &mut Model, h: f64, from: &Shape, to: &Shape| -> OgeomResult<Shape> {
4440 let frame = Frame::new(centre + seat.along * h, along_dir, radial_dir, tol)?;
4441 let circle = Circle::new(frame, radius, tol)?;
4442 let curve = Curve::Circle(CircleCurve::new(circle));
4443 Ok(make_edge_between(model, curve, (0.0, sweep), from, to, tol)?.shape)
4444 };
4445
4446 let leg_a = planar_face(
4450 model,
4451 &[apex0, contact_a0, contact_a1, apex1],
4452 n1 * sign,
4453 tol,
4454 )?;
4455 let leg_b = planar_face(
4456 model,
4457 &[apex0, contact_b0, contact_b1, apex1],
4458 n2 * sign,
4459 tol,
4460 )?;
4461
4462 let cap = |model: &mut Model,
4464 apex: Point,
4465 ca: Point,
4466 cb: Point,
4467 h: f64,
4468 outward: Vector|
4469 -> OgeomResult<Shape> {
4470 let plane = Plane::through(apex, Direction::new(outward, tol)?);
4471 let reach = (apex.distance(ca).max(apex.distance(cb)) + radius).max(1.0) * 2.0;
4472 let surface = PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?;
4473 let apex_v = make_vertex(model, apex).shape;
4474 let ca_v = make_vertex(model, ca).shape;
4475 let cb_v = make_vertex(model, cb).shape;
4476 let edges = vec![
4477 segment_between(model, (&apex_v, apex), (&ca_v, ca), tol)?,
4478 arc(model, h, &ca_v, &cb_v)?,
4479 segment_between(model, (&cb_v, cb), (&apex_v, apex), tol)?,
4480 ];
4481 let edges: Vec<Shape> = if (ca - apex).cross(cb - apex).dot(outward) < 0.0 {
4484 edges.iter().rev().map(Shape::reversed).collect()
4485 } else {
4486 edges
4487 };
4488 face_from_edges(model, surface.into(), &edges, tol)
4489 };
4490 let cap0 = cap(model, apex0, contact_a0, contact_b0, 0.0, -seat.along)?;
4491 let cap1 = cap(model, apex1, contact_a1, contact_b1, length, seat.along)?;
4492
4493 let blend = {
4497 let frame = Frame::new(centre, along_dir, radial_dir, tol)?;
4498 let surface = CylinderSurface::new(Cylinder::new(frame, radius, tol)?, (0.0, length))?;
4499 let a0_v = make_vertex(model, contact_a0).shape;
4500 let b0_v = make_vertex(model, contact_b0).shape;
4501 let a1_v = make_vertex(model, contact_a1).shape;
4502 let b1_v = make_vertex(model, contact_b1).shape;
4503 let edges = vec![
4504 arc(model, 0.0, &a0_v, &b0_v)?,
4505 segment_between(model, (&b0_v, contact_b0), (&b1_v, contact_b1), tol)?,
4506 arc(model, length, &a1_v, &b1_v)?.reversed(),
4507 segment_between(model, (&a0_v, contact_a0), (&a1_v, contact_a1), tol)?.reversed(),
4508 ];
4509 face_from_edges(model, surface.into(), &edges, tol)?
4510 };
4511
4512 let faces = [leg_a, leg_b, cap0, cap1, blend.reversed()];
4513 let mut wedge = wedge_solid(model, &faces, false, tol)?;
4514 let span = length + (centre.distance(apex0) + radius) * 4.0;
4516 for &plane in trims {
4517 wedge = trimmed_wedge(model, &wedge, plane, span, tol)?;
4518 }
4519 apply_wedge_solid(model, solid, edge, &wedge, !seat.convex, tol)
4520}
4521
4522fn revolved_fillet(
4537 model: &mut Model,
4538 solid: &Shape,
4539 edge: &Shape,
4540 rim: &CircleCurve,
4541 radius: f64,
4542 tol: Tolerances,
4543) -> OgeomResult<Built> {
4544 let seat = revolved_seat(model, solid, edge, rim, tol)?;
4545 revolved_fillet_on(model, solid, edge, seat, radius, tol)
4546}
4547
4548fn revolved_fillet_on(
4551 model: &mut Model,
4552 solid: &Shape,
4553 edge: &Shape,
4554 seat: crate::support::RevolvedSeat,
4555 radius: f64,
4556 tol: Tolerances,
4557) -> OgeomResult<Built> {
4558 let ball = seat.ball(radius);
4559 let tube_rho = ball.rho;
4560 if tube_rho <= tol.confusion() {
4561 ogeom_bail!(
4562 Construction,
4563 "a fillet of radius {radius} swallows the axis of a rim of \
4564 radius {}",
4565 seat.radius
4566 );
4567 }
4568 let tube_level = seat.centre + seat.up * ball.height;
4569 let flanks = revolved_flanks(model, &seat, -seat.tau * ball.wall_height, tube_rho, tol)?;
4570
4571 let blend_band = {
4574 let surface: SurfaceGeometry = TorusSurface::new(Torus::new(
4575 seat.frame_at(tube_level, tol)?,
4576 tube_rho,
4577 radius,
4578 tol,
4579 )?)
4580 .into();
4581 if seat.slope == 0.0 {
4582 make_revolution_band(model, &surface, &flanks.wall_ring, &flanks.cap_ring, tol)?
4583 } else {
4584 tube_band(model, &surface, &seat, &flanks, tol)?
4585 }
4586 .reversed()
4587 };
4588
4589 let faces = [flanks.wall_band, flanks.annulus, blend_band];
4590 apply_wedge(model, solid, Some(edge), &faces, seat.additive(), tol)
4591}
4592
4593fn tube_band(
4602 model: &mut Model,
4603 surface: &SurfaceGeometry,
4604 seat: &crate::support::RevolvedSeat,
4605 flanks: &crate::support::RevolvedFlanks,
4606 tol: Tolerances,
4607) -> OgeomResult<Shape> {
4608 use core::f64::consts::{PI, TAU};
4609 let cap_row = seat.tau.atan2(0.0);
4610 let lean = seat.sigma * seat.tau;
4611 let wall_row = {
4612 let row = (-lean * seat.slope).atan2(lean);
4613 row + ((cap_row - row) / TAU).round() * TAU
4614 };
4615 debug_assert!((cap_row - wall_row).abs() < PI);
4616 let row = |v: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
4617 Ok(ogeom_geom::Line2d::over(
4618 ogeom_math::Axis2::new(
4619 ogeom_math::Point2::new(0.0, v),
4620 ogeom_math::Direction2::new(ogeom_math::Vector2::new(1.0, 0.0), tol)?,
4621 ),
4622 0.0,
4623 TAU,
4624 )?
4625 .into())
4626 };
4627 ogeom_algo::make_band_between(
4628 model,
4629 surface,
4630 [
4631 (&flanks.wall_ring, row(wall_row)?),
4632 (&flanks.cap_ring, row(cap_row)?),
4633 ],
4634 tol,
4635 )
4636}
4637
4638fn rounds_whole_turn(
4643 model: &Model,
4644 solid: &Shape,
4645 edge: &Shape,
4646 tol: Tolerances,
4647) -> OgeomResult<bool> {
4648 let (Curve::Circle(c), range) = edge_curve(model, edge, tol)? else {
4649 return Ok(false);
4650 };
4651 if ogeom_algo::edge_vertices(model, edge)?.is_some_and(|(a, b)| a.is_same(&b)) {
4652 return Ok(true);
4653 }
4654 match revolved_seat(model, solid, edge, &c, tol) {
4655 Ok(seat) => crate::support::runs_whole_turn(model, &seat, &c, range, tol),
4656 Err(OgeomError::Construction(_)) => crate::marched::crease_closes(model, solid, edge, tol),
4657 Err(e) => Err(e),
4658 }
4659}
4660
4661fn rim_circle(model: &Model, edge: &Shape, tol: Tolerances) -> Option<ogeom_math::Circle> {
4663 match edge_curve(model, edge, tol).ok()?.0 {
4664 Curve::Circle(c) => Some(c.circle()),
4665 _ => None,
4666 }
4667}
4668
4669fn same_circle(a: &ogeom_math::Circle, b: &ogeom_math::Circle, tol: Tolerances) -> bool {
4671 let reach = tol.confusion() * 100.0;
4672 a.centre().distance(b.centre()) <= reach
4673 && (a.radius() - b.radius()).abs() <= reach
4674 && a.frame()
4675 .z()
4676 .vector()
4677 .cross(b.frame().z().vector())
4678 .magnitude()
4679 <= tol.angular()
4680}
4681
4682pub(crate) fn refind_edges(
4702 model: &Model,
4703 solid: &Shape,
4704 edge: &Shape,
4705 hosts: &[Shape],
4706 tol: Tolerances,
4707) -> OgeomResult<Vec<Shape>> {
4708 use ogeom_geom::Curve3d as _;
4709 use ogeom_topo::ShapeType;
4710 let (curve, range) = edge_curve(model, edge, tol)?;
4711 let within = |t: f64| -> bool {
4715 let slack = (range.1 - range.0).abs() * 1e-3;
4716 let t = if curve.is_periodic() {
4717 let (lo, hi) = curve.domain();
4718 let period = hi - lo;
4719 if period > 0.0 {
4720 range.0 + (t - range.0).rem_euclid(period)
4721 } else {
4722 t
4723 }
4724 } else {
4725 t
4726 };
4727 t >= range.0.min(range.1) - slack && t <= range.0.max(range.1) + slack
4728 };
4729 let reach = ogeom_algo::shape_bounds(model, edge, tol)?.expanded(tol.confusion() * 100.0);
4732 let mut matches: Vec<Shape> = Vec::new();
4733 for candidate in ogeom_topo::explore_unique(model, solid, ShapeType::Edge)? {
4734 if candidate.is_same(edge) {
4735 return Ok(vec![candidate]);
4736 }
4737 if !ogeom_algo::shape_bounds(model, &candidate, tol)?.intersects(&reach) {
4738 continue;
4739 }
4740 let Ok((c_curve, c_range)) = edge_curve(model, &candidate, tol) else {
4741 continue;
4742 };
4743 let mut all_on = true;
4744 for i in 0..3 {
4745 let t = c_range.0 + (c_range.1 - c_range.0) * f64::from(i) / 2.0;
4746 let p = c_curve.point_at(t, tol)?;
4747 let projected = ogeom_algo::project_on_curve(&curve, p, 64, tol)?;
4748 if projected.distance > tol.confusion() * 100.0
4749 || (i == 1 && !within(projected.parameter))
4750 {
4751 all_on = false;
4752 break;
4753 }
4754 }
4755 if !all_on || matches.iter().any(|m| m.is_same(&candidate)) {
4756 continue;
4757 }
4758 let mid = c_curve.point_at(f64::midpoint(c_range.0, c_range.1), tol)?;
4759 let mut normals: Vec<Vector> = Vec::new();
4760 for face in ogeom_topo::explore_unique(model, solid, ShapeType::Face)? {
4761 let touches = ogeom_topo::explore_unique(model, &face, ShapeType::Edge)?
4762 .iter()
4763 .any(|e| crate::support::same_occurrence(model, e, &candidate, tol));
4764 if let (true, Some(normal)) = (
4765 touches,
4766 crate::marched::face_normal_near(model, &face, mid, tol)?,
4767 ) {
4768 normals.push(normal);
4769 }
4770 }
4771 if matches!(normals.as_slice(), [first, second]
4775 if first.cross(*second).magnitude() <= 1e-2)
4776 {
4777 continue;
4778 }
4779 let mut host_normals: Vec<Vector> = Vec::with_capacity(hosts.len());
4780 for host in hosts {
4781 if let Some(normal) = crate::marched::face_normal_near(model, host, mid, tol)? {
4782 host_normals.push(normal);
4783 }
4784 }
4785 let along = |a: &[Vector], b: &[Vector]| {
4786 a.iter()
4787 .all(|n| b.iter().any(|m| n.cross(*m).magnitude() <= 1e-4))
4788 };
4789 if let ([_, _], [_, _]) = (normals.as_slice(), host_normals.as_slice())
4790 && !(along(&normals, &host_normals) && along(&host_normals, &normals))
4791 {
4792 continue;
4793 }
4794 matches.push(candidate);
4795 }
4796 if matches.is_empty() {
4797 ogeom_bail!(
4798 Construction,
4799 "an earlier blend in the chain consumed this edge; the chain's \
4800 members interfere"
4801 );
4802 }
4803 Ok(matches)
4804}
4805
4806#[allow(clippy::too_many_arguments, reason = "the seat's own, and the section")]
4815pub(crate) fn revolved_arc_fillet(
4816 model: &mut Model,
4817 solid: &Shape,
4818 edge: &Shape,
4819 rim: &CircleCurve,
4820 radius: f64,
4821 conic: Option<&crate::asym::RimConic>,
4822 mates: Option<(usize, &[Mate])>,
4823 tol: Tolerances,
4824) -> OgeomResult<Built> {
4825 use ogeom_geom::Curve3d as _;
4826 let seat = revolved_seat(model, solid, edge, rim, tol)?;
4827
4828 let ball = conic.map_or_else(|| seat.ball(radius), |c| c.contacts(&seat));
4831 let radius = conic.map_or(radius, crate::asym::RimConic::size);
4832 let tube_rho = ball.rho;
4833 if tube_rho <= tol.confusion() {
4834 ogeom_bail!(
4835 Construction,
4836 "a fillet of radius {radius} swallows the axis of a rim of \
4837 radius {}",
4838 seat.radius
4839 );
4840 }
4841 let tube_level = seat.centre + seat.up * ball.height;
4842 let wall_level = seat.centre + seat.up * ball.wall_height;
4843
4844 let seat_frame = seat.frame_at(seat.centre, tol)?;
4848 let (curve, crange) = edge_curve(model, edge, tol)?;
4849 let angle_of = |t: f64| -> OgeomResult<f64> {
4850 let local = seat_frame.to_local(curve.point_at(t, tol)?);
4851 Ok(local.y.atan2(local.x))
4852 };
4853 let tau_turn = core::f64::consts::TAU;
4854 let raw0 = angle_of(crange.0)?;
4855 let raw1 = angle_of(crange.1)?;
4856 let mid = angle_of(f64::midpoint(crange.0, crange.1))?;
4857 let (theta0, theta1) = {
4858 let span = (raw1 - raw0).rem_euclid(tau_turn);
4859 let inside = (mid - raw0).rem_euclid(tau_turn) <= span;
4860 if inside {
4861 (raw0, raw0 + span)
4862 } else {
4863 (raw1, raw1 + (tau_turn - span))
4864 }
4865 };
4866 if theta1 - theta0 <= tol.angular() {
4867 ogeom_bail!(Construction, "the rim arc has no length to blend along");
4868 }
4869 let dir_of = |theta: f64| -> Vector {
4870 seat_frame.x().vector() * theta.cos() + seat_frame.y().vector() * theta.sin()
4871 };
4872 let mut trims: Vec<(Point, Vector)> = Vec::new();
4879 let (theta0, theta1) = {
4880 let mut window = [theta0, theta1];
4881 let step = (radius / seat.radius) / 8.0;
4882 for (slot, sign) in [(0_usize, -1.0), (1, 1.0)] {
4883 let end = window[slot];
4884 let at = seat.centre + dir_of(end) * seat.radius;
4885 let ahead = seat.up.cross(dir_of(end)) * sign;
4886 let Some(face) = rim_end_stops_short(model, solid, edge, &seat, mates, at, ahead, tol)?
4887 else {
4888 continue;
4889 };
4890 let probe = crate::support::material_probe(model, solid, tol)?;
4891 let (on, back) = face;
4892 let mut past = None;
4893 let mut k = 1.0_f64;
4894 while k * step * seat.radius <= radius * 4.0 {
4900 let theta = end + sign * k * step;
4901 let (apex, wall, cap) = (
4904 seat.centre + dir_of(theta) * seat.radius,
4905 wall_level + dir_of(theta) * ball.wall_rho,
4906 seat.centre + dir_of(theta) * tube_rho,
4907 );
4908 let section = [
4909 apex + ((wall - apex) + (cap - apex)) * 0.01,
4910 wall + (apex - wall) * 0.01 + (cap - wall) * 0.01,
4911 cap + (apex - cap) * 0.01 + (wall - cap) * 0.01,
4912 ];
4913 let mut clear = true;
4914 let mut ledge = false;
4915 for p in section {
4916 if probe.holds(model, p, tol)? == ogeom_algo::Containment::In {
4917 clear = false;
4918 } else if (p - on).dot(back) > tol.confusion() {
4919 ledge = true;
4920 }
4921 }
4922 if ledge {
4923 break;
4924 }
4925 if clear {
4926 past = Some(k * step + 2.0 * step);
4927 break;
4928 }
4929 k += 1.0;
4930 }
4931 if let Some(past) = past {
4932 window[slot] = end + sign * past;
4933 trims.push(face);
4934 }
4935 }
4936 if window[1] - window[0] >= tau_turn - tol.angular() {
4937 ogeom_bail!(
4938 Construction,
4939 "a rim arc's band run on past both its ends would close the turn"
4940 );
4941 }
4942 (window[0], window[1])
4943 };
4944 let sweep = theta1 - theta0;
4945
4946 let dir_at = |theta: f64| -> Vector {
4949 seat_frame.x().vector() * theta.cos() + seat_frame.y().vector() * theta.sin()
4950 };
4951 let wedge_frame_at = |origin: Point| -> OgeomResult<Frame> {
4955 Frame::new(
4956 origin,
4957 Direction::new(seat.up, tol)?,
4958 Direction::new(dir_at(theta0), tol)?,
4959 tol,
4960 )
4961 };
4962 let apex_at = |theta: f64| seat.centre + dir_at(theta) * seat.radius;
4963 let wall_at = |theta: f64| wall_level + dir_at(theta) * ball.wall_rho;
4964 let cap_at = |theta: f64| seat.centre + dir_at(theta) * tube_rho;
4965
4966 let va0 = make_vertex(model, apex_at(theta0)).shape;
4969 let va1 = make_vertex(model, apex_at(theta1)).shape;
4970 let vw0 = make_vertex(model, wall_at(theta0)).shape;
4971 let vw1 = make_vertex(model, wall_at(theta1)).shape;
4972 let vc0 = make_vertex(model, cap_at(theta0)).shape;
4973 let vc1 = make_vertex(model, cap_at(theta1)).shape;
4974
4975 let ring_arc = |model: &mut Model,
4976 origin: Point,
4977 r: f64,
4978 from: &Shape,
4979 to: &Shape|
4980 -> OgeomResult<Shape> {
4981 let circle = Circle::new(wedge_frame_at(origin)?, r, tol)?;
4982 let curve = Curve::Circle(CircleCurve::new(circle));
4983 Ok(make_edge_between(model, curve, (0.0, sweep), from, to, tol)?.shape)
4984 };
4985 let quarter = |model: &mut Model, theta: f64, from: &Shape, to: &Shape| -> OgeomResult<Shape> {
4989 if let Some(c) = conic {
4990 return Ok(make_edge_between(
4991 model,
4992 c.meridian(&seat, theta, tol)?,
4993 (0.0, 1.0),
4994 from,
4995 to,
4996 tol,
4997 )?
4998 .shape);
4999 }
5000 let dir = dir_at(theta);
5001 let centre_m = tube_level + dir * tube_rho;
5002 let x_dir = Direction::new((dir - seat.up * seat.slope) * (seat.sigma * seat.tau), tol)?;
5003 let z_dir = Direction::new(dir.cross(seat.up) * seat.sigma, tol)?;
5004 let frame = Frame::new(centre_m, z_dir, x_dir, tol)?;
5005 let circle = Circle::new(frame, radius, tol)?;
5006 let curve = Curve::Circle(CircleCurve::new(circle));
5007 Ok(make_edge_between(model, curve, (0.0, ball.sweep), from, to, tol)?.shape)
5008 };
5009
5010 let arc_apex = ring_arc(model, seat.centre, seat.radius, &va0, &va1)?;
5011 let arc_wall = ring_arc(model, wall_level, ball.wall_rho, &vw0, &vw1)?;
5012 let arc_cap = ring_arc(model, seat.centre, tube_rho, &vc0, &vc1)?;
5013 let ruling0 = segment_between(model, (&va0, apex_at(theta0)), (&vw0, wall_at(theta0)), tol)?;
5014 let ruling1 = segment_between(model, (&vw1, wall_at(theta1)), (&va1, apex_at(theta1)), tol)?;
5015 let radial0 = segment_between(model, (&vc0, cap_at(theta0)), (&va0, apex_at(theta0)), tol)?;
5016 let radial1 = segment_between(model, (&va1, apex_at(theta1)), (&vc1, cap_at(theta1)), tol)?;
5017 let quarter0 = quarter(model, theta0, &vw0, &vc0)?;
5018 let quarter1 = quarter(model, theta1, &vw1, &vc1)?;
5019 let walked = |edges: Vec<Shape>, back: bool| -> Vec<Shape> {
5022 if back {
5023 edges.iter().rev().map(Shape::reversed).collect()
5024 } else {
5025 edges
5026 }
5027 };
5028
5029 let wall_patch = {
5035 let below = radius + 1.0;
5036 let surface = seat.wall_surface(
5037 (-below, below),
5038 (ball.wall_height.min(0.0), ball.wall_height.max(0.0)),
5039 Direction::new(dir_at(theta0), tol)?,
5040 tol,
5041 )?;
5042 let edges = walked(
5043 vec![
5044 arc_wall.clone(),
5045 ruling1.clone(),
5046 arc_apex.reversed(),
5047 ruling0.clone(),
5048 ],
5049 seat.tau < 0.0,
5050 );
5051 let face = face_from_edges(model, surface, &edges, tol)?;
5052 if seat.sigma * seat.tau < 0.0 {
5053 face.reversed()
5054 } else {
5055 face
5056 }
5057 };
5058
5059 let cap_sector = {
5064 let plane = Plane::through(seat.centre, Direction::new(seat.up * seat.tau, tol)?);
5065 let reach = (seat.radius + radius + 1.0) * 2.0;
5066 let surface: SurfaceGeometry =
5067 PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
5068 let rim_wider = seat.radius > tube_rho;
5069 let edges = walked(
5070 vec![
5071 arc_apex.clone(),
5072 radial1.clone(),
5073 arc_cap.reversed(),
5074 radial0.clone(),
5075 ],
5076 rim_wider != (seat.tau > 0.0),
5077 );
5078 face_from_edges(model, surface, &edges, tol)?
5079 };
5080
5081 let blend_patch = if let Some(c) = conic {
5087 c.band_sector(
5088 model,
5089 &seat,
5090 (theta0, sweep),
5091 [&arc_wall, &arc_cap, &quarter0, &quarter1],
5092 tol,
5093 )?
5094 } else {
5095 let surface: SurfaceGeometry = TorusSurface::new(Torus::new(
5096 wedge_frame_at(tube_level)?,
5097 tube_rho,
5098 radius,
5099 tol,
5100 )?)
5101 .into();
5102 let edges = walked(
5103 vec![
5104 arc_wall.clone(),
5105 quarter1.clone(),
5106 arc_cap.reversed(),
5107 quarter0.reversed(),
5108 ],
5109 seat.sigma < 0.0,
5110 );
5111 face_from_edges(model, surface, &edges, tol)?.reversed()
5112 };
5113
5114 let tangent_at = |theta: f64| seat.up.cross(dir_at(theta));
5121 let end_cap = |model: &mut Model,
5122 theta: f64,
5123 outward: Vector,
5124 edges: Vec<Shape>,
5125 [p, q, r]: [Point; 3]|
5126 -> OgeomResult<Shape> {
5127 let plane = Plane::through(apex_at(theta), Direction::new(outward, tol)?);
5128 let reach = (seat.radius + radius + 1.0) * 2.0;
5129 let surface: SurfaceGeometry =
5130 PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
5131 let edges = walked(edges, (q - p).cross(r - p).dot(outward) < 0.0);
5132 face_from_edges(model, surface, &edges, tol)
5133 };
5134 let cap_start = end_cap(
5135 model,
5136 theta0,
5137 -tangent_at(theta0),
5138 vec![ruling0.clone(), quarter0.clone(), radial0.clone()],
5139 [apex_at(theta0), wall_at(theta0), cap_at(theta0)],
5140 )?;
5141 let cap_end = end_cap(
5142 model,
5143 theta1,
5144 tangent_at(theta1),
5145 vec![ruling1.clone(), radial1.clone(), quarter1.reversed()],
5146 [wall_at(theta1), apex_at(theta1), cap_at(theta1)],
5147 )?;
5148
5149 let faces = [wall_patch, cap_sector, blend_patch, cap_start, cap_end];
5150 if !seat.additive() || trims.is_empty() {
5151 return apply_wedge(model, solid, Some(edge), &faces, seat.additive(), tol);
5152 }
5153 let mut wedge = crate::support::wedge_solid(model, &faces, false, tol)?;
5154 let span = (seat.radius + radius) * 4.0;
5155 for plane in trims {
5156 wedge = crate::support::trimmed_wedge(model, &wedge, plane, span, tol)?;
5157 }
5158 crate::support::apply_wedge_solid(model, solid, Some(edge), &wedge, true, tol)
5159}