1use ogeom_algo::{Built, History};
15use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
16use ogeom_geom::{Continuity, Curve, Curve3d as _};
17use ogeom_math::{Direction, Plane, Point, Vector};
18use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
19
20use crate::fill_n::{FillBoundary, make_filling_n_region, make_filling_n_within};
21
22#[derive(Debug, Clone)]
25pub struct Setbacks {
26 pub per_edge: Vec<(Shape, f64)>,
29 pub default: f64,
31 pub continuity: Continuity,
35}
36
37pub fn round_vertex_setback(
65 model: &mut Model,
66 solid: &Shape,
67 vertex: &Shape,
68 radius: f64,
69 setbacks: &Setbacks,
70 tol: Tolerances,
71) -> OgeomResult<Built> {
72 let mut own: Vec<Shape> = vec![vertex.clone()];
73 own.extend(setbacks.per_edge.iter().map(|(e, _)| e.clone()));
74 ogeom_algo::on_own_nodes(model, solid, &own, |model, solid, parts| {
75 let named: Vec<(Shape, f64)> = parts[1..]
76 .iter()
77 .zip(&setbacks.per_edge)
78 .map(|(e, (_, s))| (e.clone(), *s))
79 .collect();
80 let own = Setbacks {
81 per_edge: named,
82 default: setbacks.default,
83 continuity: setbacks.continuity,
84 };
85 setback_own(model, solid, &parts[0], radius, &own, tol)
86 })
87}
88
89pub(crate) fn point_of(model: &Model, vertex: &Shape) -> OgeomResult<Point> {
91 let Some(raw) = model
92 .node(vertex)
93 .and_then(|n| n.data().as_vertex().map(|d| d.point))
94 else {
95 ogeom_bail!(Construction, "the vertex holds no point");
96 };
97 Ok(vertex.transform(model.datums())?.apply(raw))
98}
99
100struct SetbackCorner {
105 frame: crate::corner::CornerFrame,
106 edges: Vec<Shape>,
107 reach: Vec<f64>,
108 setback: Vec<f64>,
109 own_setback: Vec<f64>,
110}
111
112impl SetbackCorner {
113 fn is_the_balls(&self, tol: Tolerances) -> bool {
116 self.setback
117 .iter()
118 .zip(&self.own_setback)
119 .all(|(s, own)| *s <= own + tol.confusion())
120 }
121}
122
123fn read_corner(
126 model: &Model,
127 solid: &Shape,
128 vertex: &Shape,
129 radius: f64,
130 setbacks: &Setbacks,
131 tol: Tolerances,
132) -> OgeomResult<SetbackCorner> {
133 if !radius.is_finite() || radius <= tol.confusion() {
134 ogeom_bail!(Construction, "a blend radius must be a positive distance");
135 }
136 if setbacks.continuity == Continuity::G2 {
137 ogeom_bail!(
143 Construction,
144 "a G2 setback corner needs bands curvature continuous with their \
145 faces; a circular band meets its face at G1, so its end arc \
146 bends the patch at 1/radius where the face asks none; see \
147 docs/CAPABILITIES.md, fillet.setback-corners"
148 );
149 }
150 if setbacks.continuity != Continuity::G1 {
151 ogeom_bail!(
152 Construction,
153 "a setback corner meets its bands and faces at G1, not {:?}",
154 setbacks.continuity
155 );
156 }
157 let frame = crate::corner::corner_frame(model, solid, vertex, radius, tol)?;
158 let n = frame.d.len();
159
160 let (edges, reach) = corner_edges(model, solid, vertex, &frame, tol)?;
161 let setback: Vec<f64> = edges
162 .iter()
163 .map(|e| {
164 setbacks
165 .per_edge
166 .iter()
167 .find(|(named, _)| named.is_same(e))
168 .map_or(setbacks.default, |(_, s)| *s)
169 })
170 .collect();
171 let own_setback: Vec<f64> = frame
172 .d
173 .iter()
174 .map(|dk| (frame.far - frame.corner).dot(dk.vector()))
175 .collect();
176 for k in 0..n {
177 let s = setback[k];
178 if !s.is_finite() || s <= tol.confusion() {
179 ogeom_bail!(Construction, "a setback of {s} is not a positive distance");
180 }
181 if s < own_setback[k] - tol.confusion() {
182 ogeom_bail!(
183 Construction,
184 "a setback of {s} stops the band inside the ball's own corner, \
185 which reaches {} along its edge",
186 own_setback[k]
187 );
188 }
189 if s >= reach[k] - tol.confusion() {
190 ogeom_bail!(
191 Construction,
192 "a setback of {s} reaches past its edge, {} long",
193 reach[k]
194 );
195 }
196 }
197 Ok(SetbackCorner {
198 frame,
199 edges,
200 reach,
201 setback,
202 own_setback,
203 })
204}
205
206fn setback_own(
208 model: &mut Model,
209 solid: &Shape,
210 vertex: &Shape,
211 radius: f64,
212 setbacks: &Setbacks,
213 tol: Tolerances,
214) -> OgeomResult<Built> {
215 let corner = read_corner(model, solid, vertex, radius, setbacks, tol)?;
216
217 let banded = crate::fillet::fillet_edges(model, solid, &corner.edges, radius, tol)?;
220 if corner.is_the_balls(tol) {
221 return Ok(banded);
222 }
223 close_corner(
224 model,
225 solid,
226 banded,
227 vertex,
228 &corner.frame,
229 &corner.setback,
230 setbacks.continuity,
231 tol,
232 )
233}
234
235pub fn fillet_edges_cornered(
257 model: &mut Model,
258 solid: &Shape,
259 edges: &[Shape],
260 radius: f64,
261 corners: &[(Shape, Setbacks)],
262 tol: Tolerances,
263) -> OgeomResult<Built> {
264 let mut own: Vec<Shape> = edges.to_vec();
265 own.extend(corners.iter().map(|(v, _)| v.clone()));
266 for (_, setbacks) in corners {
267 own.extend(setbacks.per_edge.iter().map(|(e, _)| e.clone()));
268 }
269 ogeom_algo::on_own_nodes(model, solid, &own, |model, solid, parts| {
270 let (edges, rest) = parts.split_at(edges.len());
271 let (vertices, mut named) = rest.split_at(corners.len());
272 let mut own_corners = Vec::with_capacity(corners.len());
273 for (vertex, (_, setbacks)) in vertices.iter().zip(corners) {
274 let (these, others) = named.split_at(setbacks.per_edge.len());
275 named = others;
276 own_corners.push((
277 vertex.clone(),
278 Setbacks {
279 per_edge: these
280 .iter()
281 .zip(&setbacks.per_edge)
282 .map(|(e, (_, s))| (e.clone(), *s))
283 .collect(),
284 default: setbacks.default,
285 continuity: setbacks.continuity,
286 },
287 ));
288 }
289 cornered_own(model, solid, edges, radius, &own_corners, tol)
290 })
291}
292
293fn cornered_own(
295 model: &mut Model,
296 solid: &Shape,
297 edges: &[Shape],
298 radius: f64,
299 corners: &[(Shape, Setbacks)],
300 tol: Tolerances,
301) -> OgeomResult<Built> {
302 let mut read: Vec<SetbackCorner> = Vec::with_capacity(corners.len());
303 for (i, (vertex, setbacks)) in corners.iter().enumerate() {
304 if corners[..i].iter().any(|(v, _)| v.is_same(vertex)) {
305 ogeom_bail!(Construction, "a vertex is named twice as a setback corner");
306 }
307 let corner = read_corner(model, solid, vertex, radius, setbacks, tol)?;
308 if corner
309 .edges
310 .iter()
311 .any(|e| !edges.iter().any(|asked| asked.is_same(e)))
312 {
313 ogeom_bail!(
314 Construction,
315 "a setback corner rounds every edge meeting its vertex, and an edge \
316 meeting a named corner is not among the edges to round"
317 );
318 }
319 read.push(corner);
320 }
321 for (i, a) in read.iter().enumerate() {
324 for b in &read[i + 1..] {
325 for (ka, ea) in a.edges.iter().enumerate() {
326 for (kb, eb) in b.edges.iter().enumerate() {
327 if ea.is_same(eb)
328 && a.setback[ka] + b.setback[kb] >= a.reach[ka] - tol.confusion()
329 {
330 ogeom_bail!(
331 Construction,
332 "the setbacks of {} and {} at both ends of an edge {} long \
333 meet; a band's middle must be left between two setback \
334 corners",
335 a.setback[ka],
336 b.setback[kb],
337 a.reach[ka]
338 );
339 }
340 }
341 }
342 }
343 }
344 let mut built = crate::fillet::fillet_edges(model, solid, edges, radius, tol)?;
345 for ((vertex, setbacks), corner) in corners.iter().zip(&read) {
346 if corner.is_the_balls(tol) {
347 continue;
348 }
349 built = close_corner(
350 model,
351 solid,
352 built,
353 vertex,
354 &corner.frame,
355 &corner.setback,
356 setbacks.continuity,
357 tol,
358 )?;
359 }
360 Ok(built)
361}
362
363pub(crate) fn corner_edges(
367 model: &Model,
368 solid: &Shape,
369 vertex: &Shape,
370 frame: &crate::corner::CornerFrame,
371 tol: Tolerances,
372) -> OgeomResult<(Vec<Shape>, Vec<f64>)> {
373 let n = frame.d.len();
374 let at_vertex: Vec<Shape> = explore_unique(model, solid, ShapeType::Edge)?
375 .into_iter()
376 .filter(|e| {
377 ogeom_algo::edge_vertices(model, e)
378 .ok()
379 .flatten()
380 .is_some_and(|(a, b)| a.is_same(vertex) || b.is_same(vertex))
381 })
382 .collect();
383 let mut edges: Vec<Shape> = Vec::with_capacity(n);
384 let mut reach: Vec<f64> = Vec::with_capacity(n);
385 for dk in &frame.d {
386 let mut best: Option<(f64, Shape, f64)> = None;
387 for e in &at_vertex {
388 let Some((a, b)) = ogeom_algo::edge_vertices(model, e)? else {
389 continue;
390 };
391 let far = if a.is_same(vertex) { b } else { a };
392 let along = point_of(model, &far)? - frame.corner;
393 let length = along.magnitude();
394 if length <= tol.confusion() {
395 continue;
396 }
397 let cos = along.dot(dk.vector()) / length;
398 if best.as_ref().is_none_or(|(c, _, _)| cos > *c) {
399 best = Some((cos, e.clone(), length));
400 }
401 }
402 let Some((cos, edge, length)) = best else {
403 ogeom_bail!(
404 Construction,
405 "no edge of the solid leaves the vertex along a corner edge"
406 );
407 };
408 if cos < 1.0 - tol.angular().sqrt() {
409 ogeom_bail!(
410 Construction,
411 "a setback corner rounds straight edges; an edge leaving the \
412 vertex is curved, which is still owed; see \
413 docs/CAPABILITIES.md, fillet.setback-corners"
414 );
415 }
416 edges.push(edge);
417 reach.push(length);
418 }
419 Ok((edges, reach))
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
424pub(crate) enum CornerEnd {
425 Round,
428 Bevel,
434}
435
436#[allow(
450 clippy::too_many_arguments,
451 reason = "one cell and what it is read against"
452)]
453pub(crate) fn guard_cell(
454 model: &Model,
455 solid: &Shape,
456 banded: &Shape,
457 cut: &Shape,
458 vertex: &Shape,
459 planes: &[(Vector, Point)],
460 walls: &[(Vector, Point)],
461 tol: Tolerances,
462) -> OgeomResult<()> {
463 let near = tol.confusion() * 100.0;
464 let surface_of = |face: &Shape| {
465 model
466 .node(face)
467 .and_then(|n| n.data().as_face())
468 .map(|d| d.surface)
469 };
470 let on_a_plane = |face: &Shape| -> OgeomResult<bool> {
471 let planar = model
472 .node(face)
473 .and_then(|n| n.data().as_face())
474 .and_then(|d| model.geometry().surface(d.surface))
475 .is_some_and(|s| matches!(s, ogeom_geom::SurfaceGeometry::Plane(_)));
476 if !planar {
477 return Ok(false);
478 }
479 let (origin, outward) = ogeom_algo::face_normal(model, face, tol)?;
480 Ok(planes.iter().any(|(normal, at)| {
481 outward.cross(*normal).magnitude() <= tol.angular() * 10.0
482 && normal.dot(origin - *at).abs() <= near
483 }))
484 };
485 let mut foreign = Vec::new();
486 for face in explore_unique(model, solid, ShapeType::Face)? {
487 let meets = explore_unique(model, &face, ShapeType::Vertex)?
488 .iter()
489 .any(|v| v.is_same(vertex));
490 if !meets
491 && !on_a_plane(&face)?
492 && let Some(surface) = surface_of(&face)
493 {
494 foreign.push(surface);
495 }
496 }
497 let is_foreign = |face: &Shape| surface_of(face).is_some_and(|s| foreign.contains(&s));
498 let inside = |p: Point| walls.iter().all(|(n, at)| n.dot(p - *at) >= -near);
499 let refused = || {
500 ogeom_core::ogeom_err!(
501 Construction,
502 "another feature of the solid lies inside the setback corner's cell (a face \
503 that is neither one of the corner's faces nor made by its bands, or a shell \
504 of its own), and cutting the cell would remove it; see \
505 docs/CAPABILITIES.md, fillet.setback-corners"
506 )
507 };
508 for face in explore_unique(model, banded, ShapeType::Face)? {
510 if !is_foreign(&face) {
511 continue;
512 }
513 let mut all_in = true;
514 for v in explore_unique(model, &face, ShapeType::Vertex)? {
515 if !inside(point_of(model, &v)?) {
516 all_in = false;
517 break;
518 }
519 }
520 if all_in {
521 return Err(refused());
522 }
523 }
524 for face in explore_unique(model, cut, ShapeType::Face)? {
526 if !is_foreign(&face) {
527 continue;
528 }
529 for e in explore_unique(model, &face, ShapeType::Edge)? {
530 let Some((curve, (lo, hi))) = ogeom_algo::placed_edge_curve(model, &e, tol)? else {
531 continue;
532 };
533 let mid = curve.point_at(f64::midpoint(lo, hi), tol)?;
534 if walls.iter().any(|(n, at)| n.dot(mid - *at).abs() <= near) && inside(mid) {
535 return Err(refused());
536 }
537 }
538 }
539 let shells = |shape: &Shape| explore_unique(model, shape, ShapeType::Shell).map(|s| s.len());
540 if shells(cut)? < shells(banded)? {
541 return Err(refused());
542 }
543 Ok(())
544}
545
546#[allow(clippy::too_many_arguments, reason = "one corner and its reading")]
550fn close_corner(
551 model: &mut Model,
552 solid: &Shape,
553 banded: Built,
554 vertex: &Shape,
555 frame: &crate::corner::CornerFrame,
556 setback: &[f64],
557 continuity: Continuity,
558 tol: Tolerances,
559) -> OgeomResult<Built> {
560 let ends = vec![CornerEnd::Round; frame.d.len()];
561 close_corner_ends(
562 model, solid, banded, vertex, frame, setback, &ends, continuity, tol,
563 )
564}
565
566#[allow(
568 clippy::too_many_arguments,
569 clippy::too_many_lines,
570 reason = "one corner, assembled end to end"
571)]
572pub(crate) fn close_corner_ends(
573 model: &mut Model,
574 solid: &Shape,
575 banded: Built,
576 vertex: &Shape,
577 frame: &crate::corner::CornerFrame,
578 setback: &[f64],
579 ends: &[CornerEnd],
580 continuity: Continuity,
581 tol: Tolerances,
582) -> OgeomResult<Built> {
583 let n = frame.d.len();
584 let mut walls: Vec<(Vector, Point)> = frame.m.iter().map(|mk| (*mk, frame.corner)).collect();
587 let sections: Vec<(Vector, Point)> = (0..n)
588 .map(|k| {
589 let dk = frame.d[k].vector();
590 (-dk, frame.corner + dk * setback[k])
591 })
592 .collect();
593 walls.extend(sections.iter().copied());
594 let cell = crate::corner::convex_block(model, &walls, tol)?;
595 let cut = ogeom_bool::cut(model, &banded.shape, &cell, tol)?;
596 let planes: Vec<(Vector, Point)> = frame.m.iter().map(|mk| (*mk, frame.corner)).collect();
597 guard_cell(
598 model,
599 solid,
600 &banded.shape,
601 &cut.shape,
602 vertex,
603 &planes,
604 &walls,
605 tol,
606 )?;
607 let history = banded.history.then(&cut.history);
608
609 let on_plane = |p: Point, (normal, at): (Vector, Point)| -> bool {
610 normal.dot(p - at).abs() <= tol.confusion() * 100.0
611 };
612 let section_of = |face: &Shape, model: &Model| -> OgeomResult<Option<usize>> {
613 let Some(data) = model.node(face).and_then(|n| n.data().as_face()) else {
614 return Ok(None);
615 };
616 if !matches!(
617 model.geometry().surface(data.surface),
618 Some(ogeom_geom::SurfaceGeometry::Plane(_))
619 ) {
620 return Ok(None);
621 }
622 let (origin, outward) = ogeom_algo::face_normal(model, face, tol)?;
623 let Some(k) = (0..n).find(|&k| {
624 outward.cross(sections[k].0).magnitude() <= tol.angular() * 10.0
625 && on_plane(origin, sections[k])
626 }) else {
627 return Ok(None);
628 };
629 for v in explore_unique(model, face, ShapeType::Vertex)? {
630 if !in_cell(&walls, point_of(model, &v)?, tol) {
631 return Ok(None);
632 }
633 }
634 Ok(Some(k))
635 };
636
637 let faces = explore_unique(model, &cut.shape, ShapeType::Face)?;
639 let mut kept: Vec<Shape> = Vec::with_capacity(faces.len());
640 for face in &faces {
641 if section_of(face, model)?.is_none() {
642 kept.push(face.clone());
643 }
644 }
645 let mut arcs: Vec<(Shape, Shape)> = Vec::with_capacity(n);
646 for (k, section) in sections.iter().enumerate() {
647 if ends[k] == CornerEnd::Bevel {
648 arcs.push(bevel_end(model, &kept, frame, *section, &walls, tol)?);
649 continue;
650 }
651 arcs.push(round_end(model, &kept, k, *section, &walls, tol)?);
652 }
653
654 let host = |t: usize| -> (Vector, Point) { (frame.m[frame.ring[t]], frame.corner) };
657 let end_on =
658 |model: &Model, arc: &Shape, plane: (Vector, Point)| -> OgeomResult<(Shape, Point)> {
659 let Some((a, b)) = ogeom_algo::edge_vertices(model, arc)? else {
660 ogeom_bail!(Construction, "a band's end arc has no vertices");
661 };
662 for v in [a, b] {
663 let p = point_of(model, &v)?;
664 if on_plane(p, plane) {
665 return Ok((v, p));
666 }
667 }
668 ogeom_bail!(Construction, "a band's end arc does not reach its face");
669 };
670 let fit = crate::marched::band_fit_target(tol);
671 let mut boundary: Vec<FillBoundary> = Vec::with_capacity(2 * n);
672 let mut fillers: Vec<Shape> = Vec::with_capacity(n);
673 for k in 0..n {
674 let next = (k + 1) % n;
675 let plane = host(next);
676 let (v0, p0) = end_on(model, &arcs[k].0, plane)?;
677 let (v1, p1) = end_on(model, &arcs[next].0, plane)?;
678 let Some(q) = crate::corner::planes_meet(&plane, §ions[k], §ions[next]) else {
679 ogeom_bail!(
680 Construction,
681 "two section planes do not meet on the face between them"
682 );
683 };
684 let (t0, t1) = (-frame.d[k].vector(), -frame.d[next].vector());
687 let (conic, apex) = trim_conic((p0, t0), (p1, t1), tol)?;
688 let (trim, filler) =
689 trim_and_filler(model, conic, apex, (&v0, p0), (&v1, p1), q, plane, tol)?;
690 boundary.push(FillBoundary {
691 edge: arcs[k].0.clone(),
692 support: Some(arcs[k].1.clone()),
693 continuity: match ends[k] {
694 CornerEnd::Round => continuity,
695 CornerEnd::Bevel => Continuity::C0,
696 },
697 });
698 boundary.push(FillBoundary {
703 edge: trim,
704 support: Some(filler.clone()),
705 continuity: if ends[k] == CornerEnd::Round && ends[next] == CornerEnd::Round {
706 continuity
707 } else {
708 Continuity::C0
709 },
710 });
711 fillers.push(filler);
712 }
713
714 let within: Vec<(Vector, Point)> = frame.m.iter().map(|m| (-*m, frame.corner)).collect();
717 let filled = make_filling_n_within(model, &boundary, &[], fit, &within, tol)?;
718 let patch = filled.built.shape.clone();
719 let mut faces = kept;
720 faces.extend(fillers);
721 faces.push(patch.clone());
722 let sewn = ogeom_algo::sew(model, &faces, tol)?;
723 if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
724 ogeom_bail!(Construction, "the setback corner's faces did not close");
725 }
726 let rounded = model.add_solid(std::slice::from_ref(&sewn.shells[0]))?;
727 let mut history: History = history.then(&sewn.history);
730 for image in sewn.history.trace(&patch) {
731 history.generate(vertex, image.clone());
732 }
733 Ok(Built::new(rounded, history))
734}
735
736#[allow(clippy::too_many_arguments, reason = "one face's trim and its filler")]
740pub(crate) fn trim_and_filler(
741 model: &mut Model,
742 conic: Curve,
743 apex: Point,
744 (v0, p0): (&Shape, Point),
745 (v1, p1): (&Shape, Point),
746 q: Point,
747 plane: (Vector, Point),
748 tol: Tolerances,
749) -> OgeomResult<(Shape, Shape)> {
750 let trim = ogeom_algo::make_edge_between(model, conic, (0.0, 1.0), v0, v1, tol)?.shape;
751 let filler = filler_inside(
752 model,
753 vec![trim.clone()],
754 apex,
755 (v0, p0),
756 (v1, p1),
757 q,
758 plane,
759 tol,
760 )?;
761 Ok((trim, filler))
762}
763
764#[allow(clippy::too_many_arguments, reason = "one face's filler")]
769pub(crate) fn filler_inside(
770 model: &mut Model,
771 trim: Vec<Shape>,
772 apex: Point,
773 (v0, p0): (&Shape, Point),
774 (v1, p1): (&Shape, Point),
775 q: Point,
776 plane: (Vector, Point),
777 tol: Tolerances,
778) -> OgeomResult<Shape> {
779 let vq = ogeom_algo::make_vertex(model, q).shape;
780 let mut ring = trim;
781 ring.push(crate::support::segment_between(
782 model,
783 (v1, p1),
784 (&vq, q),
785 tol,
786 )?);
787 ring.push(crate::support::segment_between(
788 model,
789 (&vq, q),
790 (v0, p0),
791 tol,
792 )?);
793 let outward = -plane.0;
794 let reach = (p0.distance(q) + p1.distance(q) + apex.distance(q)) * 2.0 + 1.0;
795 let surface: ogeom_geom::SurfaceGeometry = ogeom_geom::PlaneSurface::over(
796 Plane::through(q, Direction::new(outward, tol)?),
797 (-reach, reach),
798 (-reach, reach),
799 )?
800 .into();
801 let turn = (p0 - q).cross(p1 - q);
802 let ring: Vec<Shape> = if turn.dot(outward) > 0.0 {
803 ring
804 } else {
805 ring.iter().rev().map(Shape::reversed).collect()
806 };
807 Ok(ogeom_algo::make_face_with_pcurves(model, surface, &[ring], tol)?.shape)
808}
809
810pub(crate) fn trim_conic(
816 (p0, t0): (Point, Vector),
817 (p1, t1): (Point, Vector),
818 tol: Tolerances,
819) -> OgeomResult<(Curve, Point)> {
820 let apex = {
821 let w = p1 - p0;
823 let c = t0.cross(t1);
824 let cc = c.dot(c);
825 if cc <= tol.angular() {
826 ogeom_bail!(Construction, "two rails on a face are parallel");
827 }
828 let a = w.cross(t1).dot(c) / cc;
829 p0 + t0 * a
830 };
831 if (apex - p0).dot(t0) <= 0.0 || (apex - p1).dot(t1) <= 0.0 {
832 ogeom_bail!(
833 Construction,
834 "the rails on a face do not run towards each other past their setbacks"
835 );
836 }
837 let (u0, u1) = ((apex - p0).normalized(tol)?, (p1 - apex).normalized(tol)?);
838 let weight = ((1.0 + u0.dot(u1)) / 2.0).max(0.0).sqrt();
839 let knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], 2)?;
840 let control = vec![
841 ogeom_math::Weighted::new(p0, 1.0, tol)?,
842 ogeom_math::Weighted::new(apex, weight, tol)?,
843 ogeom_math::Weighted::new(p1, 1.0, tol)?,
844 ];
845 Ok((
846 Curve::BSpline(ogeom_geom::BSplineCurve::rational(knots, control)?),
847 apex,
848 ))
849}
850
851pub(crate) fn in_cell(walls: &[(Vector, Point)], p: Point, tol: Tolerances) -> bool {
855 walls
856 .iter()
857 .all(|(normal, at)| normal.dot(p - *at) >= -tol.confusion() * 100.0)
858}
859
860pub(crate) fn round_end(
864 model: &Model,
865 kept: &[Shape],
866 k: usize,
867 section: (Vector, Point),
868 walls: &[(Vector, Point)],
869 tol: Tolerances,
870) -> OgeomResult<(Shape, Shape)> {
871 let mut found: Option<(Shape, Shape)> = None;
872 for face in kept {
873 let is_plane = model
874 .node(face)
875 .and_then(|n| n.data().as_face())
876 .and_then(|d| model.geometry().surface(d.surface))
877 .is_some_and(|s| matches!(s, ogeom_geom::SurfaceGeometry::Plane(_)));
878 if is_plane {
879 continue;
880 }
881 for e in explore_unique(model, face, ShapeType::Edge)? {
882 let Some((c, (lo, hi))) = ogeom_algo::placed_edge_curve(model, &e, tol)? else {
883 continue;
884 };
885 if matches!(c, Curve::Line(_)) {
886 continue;
887 }
888 let on = [lo, f64::midpoint(lo, hi), hi]
889 .iter()
890 .map(|t| c.point_at(*t, tol))
891 .collect::<OgeomResult<Vec<Point>>>()?
892 .into_iter()
893 .all(|p| {
894 section.0.dot(p - section.1).abs() <= tol.confusion() * 100.0
895 && in_cell(walls, p, tol)
896 });
897 if on {
898 found = Some((e, face.clone()));
899 }
900 }
901 }
902 let Some(found) = found else {
903 ogeom_bail!(
904 Construction,
905 "band {k} does not end in an arc on its section plane"
906 );
907 };
908 Ok(found)
909}
910
911pub(crate) fn bevel_end(
916 model: &Model,
917 kept: &[Shape],
918 frame: &crate::corner::CornerFrame,
919 section: (Vector, Point),
920 walls: &[(Vector, Point)],
921 tol: Tolerances,
922) -> OgeomResult<(Shape, Shape)> {
923 let on_plane = |p: Point| {
924 section.0.dot(p - section.1).abs() <= tol.confusion() * 100.0 && in_cell(walls, p, tol)
925 };
926 for face in kept {
927 let Some(data) = model.node(face).and_then(|n| n.data().as_face()) else {
928 continue;
929 };
930 if !matches!(
931 model.geometry().surface(data.surface),
932 Some(ogeom_geom::SurfaceGeometry::Plane(_))
933 ) {
934 continue;
935 }
936 let (_, outward) = ogeom_algo::face_normal(model, face, tol)?;
937 let host = frame
938 .m
939 .iter()
940 .any(|mk| mk.cross(outward).magnitude() <= tol.angular() * 10.0);
941 if host || outward.cross(section.0).magnitude() <= tol.angular() * 10.0 {
942 continue;
943 }
944 for e in explore_unique(model, face, ShapeType::Edge)? {
945 let Some((c, (lo, hi))) = ogeom_algo::placed_edge_curve(model, &e, tol)? else {
946 continue;
947 };
948 if !matches!(c, Curve::Line(_)) {
949 continue;
950 }
951 let on = [lo, hi]
952 .iter()
953 .map(|t| c.point_at(*t, tol))
954 .collect::<OgeomResult<Vec<Point>>>()?
955 .into_iter()
956 .all(on_plane);
957 if on {
958 return Ok((e, face.clone()));
959 }
960 }
961 }
962 ogeom_bail!(
963 Construction,
964 "a bevel does not end in a straight edge on its section plane"
965 );
966}
967
968pub(crate) fn close_mixed_corner(
979 model: &mut Model,
980 solid: &Shape,
981 banded: Built,
982 vertex: &Shape,
983 request: &[(Shape, f64)],
984 tol: Tolerances,
985) -> OgeomResult<Built> {
986 let largest = request.iter().map(|(_, r)| *r).fold(0.0, f64::max);
987 let frame = crate::corner::corner_frame(model, &banded.shape, vertex, largest, tol)?;
988 let n = frame.d.len();
989 let mut radius: Vec<f64> = Vec::with_capacity(n);
992 for dk in &frame.d {
993 let mut best: Option<(f64, f64)> = None;
994 for (edge, r) in request {
995 let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? else {
996 continue;
997 };
998 let far = if a.is_same(vertex) {
999 b
1000 } else if b.is_same(vertex) {
1001 a
1002 } else {
1003 continue;
1004 };
1005 let along = point_of(model, &far)? - frame.corner;
1006 let length = along.magnitude();
1007 if length <= tol.confusion() {
1008 continue;
1009 }
1010 let cos = along.dot(dk.vector()) / length;
1011 if best.is_none_or(|(c, _)| cos > c) {
1012 best = Some((cos, *r));
1013 }
1014 }
1015 match best {
1016 Some((cos, r)) if cos >= 1.0 - tol.angular().sqrt() => radius.push(r),
1017 _ => ogeom_bail!(
1018 Construction,
1019 "a corner of different radii where an edge of the vertex is not \
1020 rounded, or is curved, is still owed; see docs/CAPABILITIES.md, \
1021 fillet.setback-corners"
1022 ),
1023 }
1024 }
1025 let setback = least_setbacks(&frame, &radius, tol)?;
1026 close_corner(
1027 model,
1028 solid,
1029 banded,
1030 vertex,
1031 &frame,
1032 &setback,
1033 Continuity::G1,
1034 tol,
1035 )
1036}
1037
1038fn least_setbacks(
1055 frame: &crate::corner::CornerFrame,
1056 radius: &[f64],
1057 tol: Tolerances,
1058) -> OgeomResult<Vec<f64>> {
1059 let n = frame.d.len();
1060 let planes = |k: usize| (frame.m[frame.ring[k]], frame.m[frame.ring[(k + 1) % n]]);
1061 let reach: Vec<f64> = (0..n)
1062 .map(|k| {
1063 let (ma, mb) = planes(k);
1064 let cos = -ma.dot(mb);
1065 let sin = (1.0 - cos * cos).max(0.0).sqrt();
1066 if sin <= tol.angular() {
1067 f64::INFINITY
1068 } else {
1069 radius[k] * (1.0 + cos) / sin
1070 }
1071 })
1072 .collect();
1073 let widths: Vec<(f64, f64)> = reach.iter().map(|&w| (w, w)).collect();
1074 least_setbacks_widths(frame, &widths, tol)
1075}
1076
1077pub(crate) fn least_setbacks_widths(
1082 frame: &crate::corner::CornerFrame,
1083 widths: &[(f64, f64)],
1084 tol: Tolerances,
1085) -> OgeomResult<Vec<f64>> {
1086 let n = frame.d.len();
1087 let planes = |k: usize| (frame.m[frame.ring[k]], frame.m[frame.ring[(k + 1) % n]]);
1088 let reach: Vec<f64> = widths.iter().map(|&(a, b)| a.max(b)).collect();
1089 let mut setback = vec![0.0_f64; n];
1090 let mut corners: Vec<[Vector; 3]> = Vec::with_capacity(n);
1091 for k in 0..n {
1092 if !reach[k].is_finite() {
1093 ogeom_bail!(
1094 Construction,
1095 "two faces at a corner meet flat; there is no edge to round"
1096 );
1097 }
1098 let dk = frame.d[k].vector();
1099 let (ma, mb) = planes(k);
1100 let leg = |own: Vector, other: Vector| -> OgeomResult<Vector> {
1102 let u = own.cross(dk).normalized(tol)?;
1103 Ok(if u.dot(other) < 0.0 { -u } else { u })
1104 };
1105 let offsets = [
1106 Vector::ZERO,
1107 leg(ma, mb)? * widths[k].0,
1108 leg(mb, ma)? * widths[k].1,
1109 ];
1110 let mut least = reach[k];
1111 for j in (0..n).filter(|&j| j != k) {
1112 let dj = frame.d[j].vector();
1113 let a = dk - dj * dk.dot(dj);
1114 let aa = a.dot(a);
1115 if aa <= tol.angular() {
1116 ogeom_bail!(Construction, "two edges at a corner run together");
1117 }
1118 for o in offsets {
1119 let b = o - dj * o.dot(dj);
1120 let ab = a.dot(b);
1121 let turn = -ab / aa;
1122 let disc = ab * ab - aa * (b.dot(b) - reach[j] * reach[j]);
1123 let from = if disc >= 0.0 {
1124 ((-ab + disc.sqrt()) / aa).max(turn)
1125 } else {
1126 turn
1127 };
1128 least = least.max(from);
1129 }
1130 }
1131 setback[k] = least + reach[k];
1132 corners.push(offsets);
1133 }
1134 let ties = |setback: &[f64], k: usize| -> f64 {
1143 let dk = frame.d[k].vector();
1144 let mut far = f64::NEG_INFINITY;
1145 for (j, offsets) in corners.iter().enumerate().filter(|&(j, _)| j != k) {
1146 let along = frame.d[j].vector().dot(dk).max(0.0);
1147 for o in offsets {
1148 far = far.max(setback[j].mul_add(along, o.dot(dk)));
1149 }
1150 }
1151 far
1152 };
1153 for _ in 0..=64 * n {
1154 let mut moved = false;
1155 for k in 0..n {
1156 let far = ties(&setback, k);
1157 if far > setback[k] || (far >= setback[k] && reach[k] > 0.0) {
1160 setback[k] = far + reach[k];
1161 moved = true;
1162 }
1163 }
1164 if !moved {
1165 return Ok(setback);
1166 }
1167 }
1168 ogeom_bail!(
1169 Construction,
1170 "the bands at this corner reach past each other's sections however far \
1171 they are set back"
1172 )
1173}
1174
1175pub(crate) struct SignedCorner {
1179 pub vertex: Shape,
1181 corner: Point,
1182 d: Vec<Vector>,
1184 radius: Vec<f64>,
1186 normal: Vec<Vector>,
1188 legs: Vec<[Vector; 2]>,
1191 sharp: Option<usize>,
1194 reach: Vec<f64>,
1196}
1197
1198type Kinded = (Shape, f64, bool);
1200
1201pub(crate) fn signed_corners(
1211 model: &Model,
1212 solid: &Shape,
1213 request: &[(Shape, f64)],
1214 tol: Tolerances,
1215) -> OgeomResult<Vec<SignedCorner>> {
1216 let mut corners = Vec::new();
1217 for (vertex, at) in signed_vertices(model, solid, request, tol)? {
1218 if runs_through(model, solid, &vertex, &at, tol)?
1219 || crate::cusp::cusp_corner(model, solid, &vertex, &at, tol)?.is_some()
1220 {
1221 continue;
1222 }
1223 corners.push(signed_corner(model, solid, &vertex, &at, tol)?);
1224 }
1225 Ok(corners)
1226}
1227
1228pub(crate) fn cusp_corners(
1231 model: &Model,
1232 solid: &Shape,
1233 request: &[(Shape, f64)],
1234 tol: Tolerances,
1235) -> OgeomResult<Vec<crate::cusp::CuspCorner>> {
1236 let mut sorted = request.to_vec();
1239 sorted.sort_by_key(|(edge, _)| edge.node());
1240 let mut corners = Vec::new();
1241 for (vertex, at) in signed_vertices(model, solid, &sorted, tol)? {
1242 if let Some(c) = crate::cusp::cusp_corner(model, solid, &vertex, &at, tol)? {
1243 corners.push(c);
1244 }
1245 }
1246 Ok(corners)
1247}
1248
1249fn signed_vertices(
1252 model: &Model,
1253 solid: &Shape,
1254 request: &[(Shape, f64)],
1255 tol: Tolerances,
1256) -> OgeomResult<Vec<(Shape, Vec<Kinded>)>> {
1257 let mut by_vertex: Vec<(Shape, Vec<(Shape, f64)>)> = Vec::new();
1259 for (edge, radius) in request {
1260 let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? else {
1261 continue;
1262 };
1263 for v in [a, b] {
1264 if let Some(slot) = by_vertex.iter_mut().find(|(held, _)| held.is_same(&v)) {
1265 if !slot.1.iter().any(|(e, _)| e.is_same(edge)) {
1266 slot.1.push((edge.clone(), *radius));
1267 }
1268 } else {
1269 by_vertex.push((v, vec![(edge.clone(), *radius)]));
1270 }
1271 }
1272 }
1273 by_vertex.retain(|(_, at)| at.len() >= 2);
1274 let mut kinds: Vec<(Shape, Option<bool>)> = Vec::new();
1276 let mut read = |model: &Model, edge: &Shape, radius: f64| -> OgeomResult<Option<bool>> {
1277 if let Some((_, kind)) = kinds.iter().find(|(e, _)| e.is_same(edge)) {
1278 return Ok(*kind);
1279 }
1280 let kind = match edge_is_convex(model, solid, edge, radius, tol) {
1281 Ok(kind) => kind,
1282 Err(ogeom_core::OgeomError::Construction(_) | ogeom_core::OgeomError::NotDone(_)) => {
1283 None
1284 }
1285 Err(e) => return Err(e),
1286 };
1287 kinds.push((edge.clone(), kind));
1288 Ok(kind)
1289 };
1290 let mut signed: Vec<(Shape, Vec<Kinded>)> = Vec::new();
1291 for (vertex, at) in by_vertex {
1292 let mut known = Vec::with_capacity(at.len());
1293 for (edge, radius) in at {
1294 if let Some(convex) = read(model, &edge, radius)? {
1295 known.push((edge, radius, convex));
1296 }
1297 }
1298 signed.push((vertex, known));
1299 }
1300 Ok(signed
1301 .into_iter()
1302 .filter(|(_, at)| at.iter().any(|e| e.2) && at.iter().any(|e| !e.2))
1303 .collect())
1304}
1305
1306fn runs_through(
1316 model: &Model,
1317 solid: &Shape,
1318 vertex: &Shape,
1319 at: &[Kinded],
1320 tol: Tolerances,
1321) -> OgeomResult<bool> {
1322 let [(first, _, _), (second, _, _)] = at else {
1323 return Ok(false);
1324 };
1325 let corner = point_of(model, vertex)?;
1326 let leaving = |edge: &Shape| -> OgeomResult<Option<Vector>> {
1328 let Some((curve, (lo, hi))) = ogeom_algo::placed_edge_curve(model, edge, tol)? else {
1329 return Ok(None);
1330 };
1331 let (at_lo, at_hi) = (curve.point_at(lo, tol)?, curve.point_at(hi, tol)?);
1332 let (t, sense) = if at_lo.distance(corner) <= at_hi.distance(corner) {
1333 (lo, 1.0)
1334 } else {
1335 (hi, -1.0)
1336 };
1337 Ok(Some(curve.d1_at(t, tol)?.normalized(tol)? * sense))
1338 };
1339 let (Some(a), Some(b)) = (leaving(first)?, leaving(second)?) else {
1340 return Ok(false);
1341 };
1342 if a.dot(b) > -1.0 + tol.angular() * 10.0 {
1343 return Ok(false);
1344 }
1345 for f in explore_unique(model, solid, ShapeType::Face)? {
1346 if explore_unique(model, &f, ShapeType::Vertex)?
1347 .iter()
1348 .any(|v| v.is_same(vertex))
1349 && crate::rolled::flat(model, &f, tol)?.is_none()
1350 {
1351 return Ok(true);
1352 }
1353 }
1354 Ok(false)
1355}
1356
1357fn edge_is_convex(
1361 model: &Model,
1362 solid: &Shape,
1363 edge: &Shape,
1364 radius: f64,
1365 tol: Tolerances,
1366) -> OgeomResult<Option<bool>> {
1367 if let Some(seat) = crate::consumed::seat_of(model, solid, edge, tol)? {
1368 return Ok(Some(seat.convex));
1369 }
1370 let mut faces: Vec<Shape> = Vec::with_capacity(2);
1371 for f in explore_unique(model, solid, ShapeType::Face)? {
1372 if explore_unique(model, &f, ShapeType::Edge)?
1373 .iter()
1374 .any(|e| e.is_same(edge))
1375 {
1376 faces.push(f);
1377 }
1378 }
1379 let [first, second] = faces.as_slice() else {
1380 return Ok(None);
1381 };
1382 let (Some(a), Some(b)) = (
1383 crate::rolled::host(model, first, tol)?,
1384 crate::rolled::host(model, second, tol)?,
1385 ) else {
1386 return Ok(None);
1387 };
1388 let Some((curve, range)) = ogeom_algo::placed_edge_curve(model, edge, tol)? else {
1389 return Ok(None);
1390 };
1391 crate::marched::crease_convexity(
1392 model,
1393 first,
1394 [(&a.0, a.1), (&b.0, b.1)],
1395 &curve,
1396 range,
1397 radius,
1398 tol,
1399 )
1400 .map(Some)
1401}
1402
1403fn signed_refusal(why: &str) -> ogeom_core::OgeomError {
1405 ogeom_core::ogeom_err!(
1406 Construction,
1407 "a convex and a concave round meeting at one vertex close as one corner \
1408 only where {why}; the two rounds alone would take the corner in the \
1409 order asked; see docs/CAPABILITIES.md, fillet.setback-corners"
1410 )
1411}
1412
1413fn signed_corner(
1414 model: &Model,
1415 solid: &Shape,
1416 vertex: &Shape,
1417 request: &[Kinded],
1418 tol: Tolerances,
1419) -> OgeomResult<SignedCorner> {
1420 let corner = point_of(model, vertex)?;
1421 let mut groups: Vec<(Vec<Shape>, crate::rolled::Flat)> = Vec::new();
1424 for f in explore_unique(model, solid, ShapeType::Face)? {
1425 if !explore_unique(model, &f, ShapeType::Vertex)?
1426 .iter()
1427 .any(|v| v.is_same(vertex))
1428 {
1429 continue;
1430 }
1431 let Some(flat) = crate::rolled::flat(model, &f, tol)? else {
1432 return Err(signed_refusal("every face of the vertex is planar"));
1433 };
1434 let same = groups.iter_mut().find(|(_, g)| {
1435 g.normal.dot(flat.normal) >= 1.0 - tol.angular()
1436 && g.normal.dot(flat.origin - g.origin).abs() <= tol.confusion() * 100.0
1437 });
1438 match same {
1439 Some((members, g)) => {
1440 members.push(f.clone());
1441 g.sides.extend(flat.sides.iter().copied());
1442 }
1443 None => groups.push((vec![f.clone()], flat)),
1444 }
1445 }
1446 let mut at_vertex: Vec<Shape> = Vec::new();
1448 let mut edge_groups: Vec<[usize; 2]> = Vec::new();
1449 for e in explore_unique(model, solid, ShapeType::Edge)? {
1450 if !ogeom_algo::edge_vertices(model, &e)?
1451 .is_some_and(|(a, b)| a.is_same(vertex) || b.is_same(vertex))
1452 {
1453 continue;
1454 }
1455 let mut own: Vec<usize> = Vec::new();
1456 for (gi, (members, _)) in groups.iter().enumerate() {
1457 for f in members {
1458 if explore_unique(model, f, ShapeType::Edge)?
1459 .iter()
1460 .any(|x| x.is_same(&e))
1461 && !own.contains(&gi)
1462 {
1463 own.push(gi);
1464 }
1465 }
1466 }
1467 match own.as_slice() {
1468 [_] => {}
1469 [a, b] => {
1470 at_vertex.push(e);
1471 edge_groups.push([*a, *b]);
1472 }
1473 _ => {
1474 return Err(signed_refusal(
1475 "each edge of the vertex parts two of its faces",
1476 ));
1477 }
1478 }
1479 }
1480 if at_vertex.len() != 3 || groups.len() != 3 {
1481 return Err(signed_refusal("three edges meet at the vertex"));
1482 }
1483 let mut radius_of = Vec::with_capacity(3);
1484 let mut dir = Vec::with_capacity(3);
1485 let mut reach = Vec::with_capacity(3);
1486 let mut sharp_edge: Option<usize> = None;
1487 for (k, e) in at_vertex.iter().enumerate() {
1488 let r = match request.iter().find(|(asked, _, _)| asked.is_same(e)) {
1489 Some((_, r, _)) => *r,
1490 None if sharp_edge.is_none() => {
1491 sharp_edge = Some(k);
1492 0.0
1493 }
1494 None => {
1495 return Err(signed_refusal(
1496 "the request rounds all but at most one edge of the vertex",
1497 ));
1498 }
1499 };
1500 let Some([p, q]) = crate::consumed::segment(model, e, tol)? else {
1501 return Err(signed_refusal("every edge of the vertex is straight"));
1502 };
1503 let far = if p.distance(corner) < q.distance(corner) {
1504 q
1505 } else {
1506 p
1507 };
1508 let along = far - corner;
1509 reach.push(along.magnitude());
1510 dir.push(along.normalized(tol)?);
1511 radius_of.push(r);
1512 }
1513 let flats: Vec<crate::rolled::Flat> = groups.iter().map(|(_, g)| g.clone()).collect();
1515 let mut pairs: Vec<[usize; 2]> = Vec::with_capacity(3);
1516 for gi in 0..3 {
1517 let own: Vec<usize> = (0..3).filter(|&k| edge_groups[k].contains(&gi)).collect();
1518 let [a, b] = own.as_slice() else {
1519 return Err(signed_refusal(
1520 "each face of the vertex holds two of its edges",
1521 ));
1522 };
1523 pairs.push([*a, *b]);
1524 }
1525 let mut ring_edges = vec![0_usize];
1527 let mut ring_faces: Vec<usize> = Vec::with_capacity(3);
1528 let mut used = [false; 3];
1529 while ring_faces.len() < 3 {
1530 let last = *ring_edges.last().unwrap_or(&0);
1531 let Some(fi) = (0..3).find(|&i| !used[i] && pairs[i].contains(&last)) else {
1532 return Err(signed_refusal("the faces of the vertex close round it"));
1533 };
1534 used[fi] = true;
1535 ring_faces.push(fi);
1536 let next = if pairs[fi][0] == last {
1537 pairs[fi][1]
1538 } else {
1539 pairs[fi][0]
1540 };
1541 if ring_faces.len() < 3 {
1542 ring_edges.push(next);
1543 }
1544 }
1545 let d: Vec<Vector> = ring_edges.iter().map(|&k| dir[k]).collect();
1546 let radius: Vec<f64> = ring_edges.iter().map(|&k| radius_of[k]).collect();
1547 let length: Vec<f64> = ring_edges.iter().map(|&k| reach[k]).collect();
1548 let normal: Vec<Vector> = ring_faces.iter().map(|&f| flats[f].normal).collect();
1549 let into = |fi: usize, k: usize| -> OgeomResult<Vector> {
1552 let flat = &flats[fi];
1553 let u = flat.normal.cross(dir[k]).normalized(tol)?;
1554 let base = corner + dir[k] * (reach[k] * 0.5);
1555 let step = reach[k] * 1e-3;
1556 let frame = crate::rolled::basis(flat.normal);
1557 match (
1558 crate::rolled::inside(&flat.sides, base + u * step, frame),
1559 crate::rolled::inside(&flat.sides, base - u * step, frame),
1560 ) {
1561 (true, false) => Ok(u),
1562 (false, true) => Ok(-u),
1563 _ => Err(signed_refusal(
1564 "each face lies on one side of each of its edges",
1565 )),
1566 }
1567 };
1568 let mut legs = Vec::with_capacity(3);
1569 for i in 0..3 {
1570 let k = ring_edges[i];
1571 let before = ring_faces[(i + 2) % 3];
1572 let after = ring_faces[i];
1573 legs.push([into(before, k)?, into(after, k)?]);
1574 }
1575 let sharp = sharp_edge.and_then(|k| ring_edges.iter().position(|&e| e == k));
1576 if let Some(i) = sharp
1580 && legs[i][1].dot(normal[(i + 2) % 3]) >= 0.0
1581 {
1582 return Err(signed_refusal("an edge the request leaves sharp is convex"));
1583 }
1584 Ok(SignedCorner {
1585 vertex: vertex.clone(),
1586 corner,
1587 d,
1588 radius,
1589 normal,
1590 legs,
1591 sharp,
1592 reach: length,
1593 })
1594}
1595
1596fn signed_widths(c: &SignedCorner, tol: Tolerances) -> OgeomResult<Vec<f64>> {
1599 (0..3)
1600 .map(|i| {
1601 let (a, b) = (c.normal[(i + 2) % 3], c.normal[i]);
1602 let turn = a.dot(b).clamp(-1.0, 1.0).acos();
1603 if turn <= tol.angular() || turn >= core::f64::consts::PI - tol.angular() {
1604 return Err(signed_refusal(
1605 "the faces of each edge turn by less than a half turn",
1606 ));
1607 }
1608 Ok(c.radius[i] * (turn / 2.0).tan())
1609 })
1610 .collect()
1611}
1612
1613fn signed_setbacks(c: &SignedCorner, width: &[f64], tol: Tolerances) -> OgeomResult<Vec<f64>> {
1618 let mut setback = vec![0.0_f64; 3];
1619 for k in 0..3 {
1620 let dk = c.d[k];
1621 let offsets = [
1622 Vector::ZERO,
1623 c.legs[k][0] * width[k],
1624 c.legs[k][1] * width[k],
1625 ];
1626 let mut least = width[k];
1627 for j in (0..3).filter(|&j| j != k && c.sharp != Some(j)) {
1628 let dj = c.d[j];
1629 let a = dk - dj * dk.dot(dj);
1630 let aa = a.dot(a);
1631 if aa <= tol.angular() {
1632 return Err(signed_refusal("no two edges of the vertex run together"));
1633 }
1634 for o in offsets {
1635 let b = o - dj * o.dot(dj);
1636 let ab = a.dot(b);
1637 let turn = -ab / aa;
1638 let disc = ab * ab - aa * (b.dot(b) - width[j] * width[j]);
1639 let from = if disc >= 0.0 {
1640 ((-ab + disc.sqrt()) / aa).max(turn)
1641 } else {
1642 turn
1643 };
1644 least = least.max(from);
1645 }
1646 }
1647 setback[k] = least + width[k];
1648 }
1649 if let Some(k) = c.sharp {
1653 setback[k] += width.iter().copied().fold(0.0, f64::max);
1654 }
1655 for (s, length) in setback.iter().zip(&c.reach) {
1658 if *s >= length - tol.confusion() * 100.0 {
1659 return Err(signed_refusal(
1660 "each edge runs on past its band's setback, its own width beyond \
1661 the least at which its section clears the others",
1662 ));
1663 }
1664 }
1665 Ok(setback)
1666}
1667
1668#[allow(clippy::too_many_lines, reason = "one corner, assembled end to end")]
1689pub(crate) fn close_signed_corner(
1690 model: &mut Model,
1691 solid: &Shape,
1692 banded: Built,
1693 c: &SignedCorner,
1694 tol: Tolerances,
1695) -> OgeomResult<Built> {
1696 let width = signed_widths(c, tol)?;
1697 let setback = signed_setbacks(c, &width, tol)?;
1698 let back = 2.0 * width.iter().copied().fold(0.0, f64::max);
1699 let sections: Vec<(Vector, Point)> = (0..3)
1700 .map(|k| (-c.d[k], c.corner + c.d[k] * setback[k]))
1701 .collect();
1702 let mut walls = sections.clone();
1703 walls.extend((0..3).map(|k| (c.d[k], c.corner - c.d[k] * back)));
1704 let cell = crate::corner::convex_block(model, &walls, tol)?;
1705 let cut = ogeom_bool::cut(model, &banded.shape, &cell, tol)?;
1706 let planes: Vec<(Vector, Point)> = c.normal.iter().map(|n| (*n, c.corner)).collect();
1707 guard_cell(
1708 model,
1709 solid,
1710 &banded.shape,
1711 &cut.shape,
1712 &c.vertex,
1713 &planes,
1714 &walls,
1715 tol,
1716 )?;
1717 let history = banded.history.then(&cut.history);
1718 let slack = tol.confusion() * 100.0;
1719 let on_plane =
1720 |p: Point, (normal, at): (Vector, Point)| -> bool { normal.dot(p - at).abs() <= slack };
1721 let is_plane = |face: &Shape, model: &Model| {
1722 model
1723 .node(face)
1724 .and_then(|n| n.data().as_face())
1725 .and_then(|d| model.geometry().surface(d.surface))
1726 .is_some_and(|s| matches!(s, ogeom_geom::SurfaceGeometry::Plane(_)))
1727 };
1728 let in_cell = |p: Point| walls.iter().all(|(n, at)| n.dot(p - *at) >= -slack);
1731 let on_wall = |face: &Shape, model: &Model| -> OgeomResult<bool> {
1732 if !is_plane(face, model) {
1733 return Ok(false);
1734 }
1735 let (origin, outward) = ogeom_algo::face_normal(model, face, tol)?;
1736 if !walls
1737 .iter()
1738 .any(|w| outward.cross(w.0).magnitude() <= tol.angular() * 10.0 && on_plane(origin, *w))
1739 {
1740 return Ok(false);
1741 }
1742 for v in explore_unique(model, face, ShapeType::Vertex)? {
1743 if !in_cell(point_of(model, &v)?) {
1744 return Ok(false);
1745 }
1746 }
1747 Ok(true)
1748 };
1749 let faces = explore_unique(model, &cut.shape, ShapeType::Face)?;
1750 let mut kept: Vec<Shape> = Vec::with_capacity(faces.len());
1751 for face in &faces {
1752 if !on_wall(face, model)? {
1753 kept.push(face.clone());
1754 }
1755 }
1756 let mut held: Vec<Point> = Vec::new();
1758 for face in &kept {
1759 for v in explore_unique(model, face, ShapeType::Vertex)? {
1760 let p = point_of(model, &v)?;
1761 if !held.iter().any(|q| q.distance(p) <= slack) {
1762 held.push(p);
1763 }
1764 }
1765 }
1766 let mut arcs: Vec<Option<(Shape, Shape)>> = Vec::with_capacity(3);
1768 for (k, section) in sections.iter().enumerate() {
1769 if c.sharp == Some(k) {
1770 arcs.push(None);
1771 continue;
1772 }
1773 let mut found: Option<(Shape, Shape)> = None;
1774 for face in &kept {
1775 if is_plane(face, model) {
1776 continue;
1777 }
1778 for e in explore_unique(model, face, ShapeType::Edge)? {
1779 let Some((curve, (lo, hi))) = ogeom_algo::placed_edge_curve(model, &e, tol)? else {
1780 continue;
1781 };
1782 if matches!(curve, Curve::Line(_)) {
1783 continue;
1784 }
1785 let on = [lo, f64::midpoint(lo, hi), hi]
1786 .iter()
1787 .map(|t| curve.point_at(*t, tol))
1788 .collect::<OgeomResult<Vec<Point>>>()?
1789 .into_iter()
1790 .all(|p| on_plane(p, *section) && in_cell(p));
1791 if on {
1792 found = Some((e, face.clone()));
1793 }
1794 }
1795 }
1796 let Some(found) = found else {
1797 ogeom_bail!(
1798 Construction,
1799 "band {k} of a signed corner does not end in an arc on its section plane"
1800 );
1801 };
1802 arcs.push(Some(found));
1803 }
1804 let at_q: Option<(Shape, Point)> = match c.sharp {
1806 None => None,
1807 Some(k) => {
1808 let q = sections[k].1;
1809 let Some(v) = explore_unique(model, &cut.shape, ShapeType::Vertex)?
1810 .into_iter()
1811 .find(|v| point_of(model, v).is_ok_and(|p| p.distance(q) <= slack))
1812 else {
1813 ogeom_bail!(
1814 Construction,
1815 "the cut does not leave the sharp edge of a signed corner at its setback"
1816 );
1817 };
1818 Some((v, q))
1819 }
1820 };
1821 let end_on = |model: &Model, k: usize, plane: (Vector, Point)| -> OgeomResult<(Shape, Point)> {
1822 let Some((arc, _)) = &arcs[k] else {
1823 return at_q
1824 .clone()
1825 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a band has no end arc"));
1826 };
1827 let Some((a, b)) = ogeom_algo::edge_vertices(model, arc)? else {
1828 ogeom_bail!(Construction, "a band's end arc has no vertices");
1829 };
1830 for v in [a, b] {
1831 let p = point_of(model, &v)?;
1832 if on_plane(p, plane) {
1833 return Ok((v, p));
1834 }
1835 }
1836 ogeom_bail!(Construction, "a band's end arc does not reach its face");
1837 };
1838 let fit = crate::marched::band_fit_target(tol);
1839 let mut boundary: Vec<FillBoundary> = Vec::with_capacity(6);
1840 let mut fillers: Vec<Shape> = Vec::with_capacity(3);
1841 let mut trims: Vec<crate::sharp::Trim> = Vec::with_capacity(3);
1842 #[allow(
1843 clippy::needless_range_loop,
1844 reason = "face k lies between edge k and edge k + 1"
1845 )]
1846 for k in 0..3 {
1847 let next = (k + 1) % 3;
1848 let plane = (c.normal[k], c.corner);
1849 let (v0, p0) = end_on(model, k, plane)?;
1850 let (v1, p1) = end_on(model, next, plane)?;
1851 let into = |from: (usize, Point), to: (usize, Point)| -> OgeomResult<Vector> {
1855 if c.sharp == Some(from.0) {
1856 let aim = to.1 - c.d[to.0] * (setback[to.0] / 2.0);
1857 Ok((aim - from.1).normalized(tol)?)
1858 } else {
1859 Ok(-c.d[from.0])
1860 }
1861 };
1862 let reflex = c.sharp.and_then(|ks| {
1868 let (band, leg) = if k == ks {
1869 (next, c.legs[ks][1])
1870 } else if next == ks {
1871 (k, c.legs[ks][0])
1872 } else {
1873 return None;
1874 };
1875 (c.d[band].dot(leg) < 0.0).then_some((ks, band, leg))
1876 });
1877 let conic = if let Some((ks, band, leg)) = reflex {
1878 let (q, rail) = if k == ks { (p0, p1) } else { (p1, p0) };
1879 let w = width[band];
1880 let back = -c.d[band];
1881 let near_q = q + (leg - c.d[ks]) * (w / 2.0);
1882 let past = rail + back * ((rail - q).dot(-back) + w);
1883 let poles = if k == ks {
1884 vec![q, near_q, past, rail]
1885 } else {
1886 vec![rail, past, near_q, q]
1887 };
1888 let knots =
1889 ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3)?;
1890 ogeom_geom::BSplineCurve::new(knots, poles, tol)?
1891 } else {
1892 let (t0, t1) = (into((k, p0), (next, p1))?, into((next, p1), (k, p0))?);
1893 let apex = {
1894 let w = p1 - p0;
1895 let cr = t0.cross(t1);
1896 let cc = cr.dot(cr);
1897 if cc <= tol.angular() {
1898 ogeom_bail!(Construction, "two rails on a face are parallel");
1899 }
1900 p0 + t0 * (w.cross(t1).dot(cr) / cc)
1901 };
1902 if (apex - p0).dot(t0) <= 0.0 || (apex - p1).dot(t1) <= 0.0 {
1903 ogeom_bail!(
1904 Construction,
1905 "the rails on a face do not run towards each other past their setbacks"
1906 );
1907 }
1908 let (u0, u1) = ((apex - p0).normalized(tol)?, (p1 - apex).normalized(tol)?);
1909 let weight = ((1.0 + u0.dot(u1)) / 2.0).max(0.0).sqrt();
1910 let knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], 2)?;
1911 let control = vec![
1912 ogeom_math::Weighted::new(p0, 1.0, tol)?,
1913 ogeom_math::Weighted::new(apex, weight, tol)?,
1914 ogeom_math::Weighted::new(p1, 1.0, tol)?,
1915 ];
1916 ogeom_geom::BSplineCurve::rational(knots, control)?
1917 };
1918 let samples: Vec<Point> = (0..=16)
1919 .map(|i| conic.point_at(f64::from(i) / 16.0, tol))
1920 .collect::<OgeomResult<_>>()?;
1921 let conic = Curve::BSpline(conic);
1922 let at_sharp = c.sharp == Some(k) || c.sharp == Some(next);
1923 let trim = crate::sharp::trim_edges(model, &conic, (&v0, &v1), at_sharp, tol)?;
1924 let outward = c.normal[k];
1927 let mut corners: Vec<Point> = Vec::new();
1928 for i in 0..walls.len() {
1929 for j in i + 1..walls.len() {
1930 let Some(p) = crate::corner::planes_meet(&plane, &walls[i], &walls[j]) else {
1931 continue;
1932 };
1933 if walls.iter().any(|(n, at)| n.dot(p - *at) < -slack)
1934 || corners.iter().any(|q| q.distance(p) <= slack)
1935 {
1936 continue;
1937 }
1938 corners.push(p);
1939 }
1940 }
1941 if corners.len() < 3 {
1942 ogeom_bail!(Construction, "a face of a signed corner misses its cell");
1943 }
1944 let (e1, e2) = crate::rolled::basis(outward);
1945 let middle = corners
1946 .iter()
1947 .fold(Vector::ZERO, |acc, p| acc + (*p - Point::ORIGIN))
1948 * (1.0 / 3.0_f64.max(f64::from(u32::try_from(corners.len()).unwrap_or(3))));
1949 let middle = Point::ORIGIN + middle;
1950 let angle = |p: &Point| (*p - middle).dot(e2).atan2((*p - middle).dot(e1));
1951 corners.sort_by(|a, b| angle(a).total_cmp(&angle(b)));
1952 let place = |p: Point| -> Option<(usize, f64)> {
1954 let m = corners.len();
1955 (0..m).find_map(|i| {
1956 let (a, b) = (corners[i], corners[(i + 1) % m]);
1957 let ab = b - a;
1958 let t = (p - a).dot(ab) / ab.dot(ab);
1959 ((-1e-9..=1.0 + 1e-9).contains(&t) && (a + ab * t).distance(p) <= slack)
1960 .then_some((i, t))
1961 })
1962 };
1963 let (Some(at0), Some(at1)) = (place(p0), place(p1)) else {
1964 ogeom_bail!(Construction, "a band's rail end misses its section wall");
1965 };
1966 let walk = |forward: bool| -> Vec<Point> {
1968 let m = corners.len();
1969 let mut path = Vec::new();
1970 if forward {
1971 let mut i = at1.0;
1972 loop {
1973 i = (i + 1) % m;
1974 path.push(corners[i]);
1975 if i == at0.0 {
1976 break;
1977 }
1978 }
1979 } else {
1980 let mut i = at1.0;
1981 loop {
1982 path.push(corners[i]);
1983 if i == (at0.0 + 1) % m {
1984 break;
1985 }
1986 i = (i + m - 1) % m;
1987 }
1988 }
1989 path
1990 };
1991 let flat2 = |p: &Point| ((*p - c.corner).dot(e1), (*p - c.corner).dot(e2));
1992 let holds_vertex = |path: &[Point]| -> bool {
1993 let mut ring: Vec<(f64, f64)> = samples.iter().map(flat2).collect();
1994 ring.extend(path.iter().map(flat2));
1995 let mut odd = false;
1996 for i in 0..ring.len() {
1997 let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
1998 if (a.1 > 0.0) != (b.1 > 0.0) && 0.0 < a.0 + (b.0 - a.0) * (0.0 - a.1) / (b.1 - a.1)
1999 {
2000 odd = !odd;
2001 }
2002 }
2003 odd
2004 };
2005 let same_side = at0.0 == at1.0;
2006 let path = if same_side {
2007 Vec::new()
2008 } else {
2009 let (a, b) = (walk(true), walk(false));
2010 match (holds_vertex(&a), holds_vertex(&b)) {
2011 (false, true) => a,
2012 (true, false) => b,
2013 _ => ogeom_bail!(
2014 Construction,
2015 "a face's trim does not part the vertex from the cell's walls"
2016 ),
2017 }
2018 };
2019 let path: Vec<Point> = path
2020 .into_iter()
2021 .filter(|p| p.distance(p0) > slack && p.distance(p1) > slack)
2022 .collect();
2023 let mut stops: Vec<Point> = Vec::new();
2027 let mut from = p1;
2028 for p in path.iter().copied().chain(core::iter::once(p0)) {
2029 let run = p - from;
2030 let length = run.magnitude();
2031 let mut between: Vec<(f64, Point)> = held
2032 .iter()
2033 .filter_map(|q| {
2034 let t = (*q - from).dot(run) / (length * length);
2035 (t > 0.0
2036 && t < 1.0
2037 && (from + run * t).distance(*q) <= slack
2038 && q.distance(from) > slack
2039 && q.distance(p) > slack)
2040 .then_some((t, *q))
2041 })
2042 .collect();
2043 between.sort_by(|a, b| a.0.total_cmp(&b.0));
2044 stops.extend(between.into_iter().map(|(_, q)| q));
2045 stops.push(p);
2046 from = p;
2047 }
2048 stops.pop();
2049 let mut ring = trim.clone();
2050 let mut prev = (v1.clone(), p1);
2051 for p in &stops {
2052 let v = ogeom_algo::make_vertex(model, *p).shape;
2053 ring.push(crate::support::segment_between(
2054 model,
2055 (&prev.0, prev.1),
2056 (&v, *p),
2057 tol,
2058 )?);
2059 prev = (v, *p);
2060 }
2061 ring.push(crate::support::segment_between(
2062 model,
2063 (&prev.0, prev.1),
2064 (&v0, p0),
2065 tol,
2066 )?);
2067 let mut loop2: Vec<(f64, f64)> = samples.iter().map(flat2).collect();
2068 loop2.extend(path.iter().map(flat2));
2069 let area: f64 = (0..loop2.len())
2070 .map(|i| {
2071 let (a, b) = (loop2[i], loop2[(i + 1) % loop2.len()]);
2072 a.0 * b.1 - b.0 * a.1
2073 })
2074 .sum();
2075 let ring: Vec<Shape> = if area > 0.0 {
2076 ring
2077 } else {
2078 ring.iter().rev().map(Shape::reversed).collect()
2079 };
2080 let reach = corners
2081 .iter()
2082 .map(|p| p.distance(c.corner))
2083 .fold(0.0, f64::max)
2084 * 2.0
2085 + 1.0;
2086 let surface: ogeom_geom::SurfaceGeometry = ogeom_geom::PlaneSurface::over(
2087 Plane::through(c.corner, Direction::new(outward, tol)?),
2088 (-reach, reach),
2089 (-reach, reach),
2090 )?
2091 .into();
2092 let filler = ogeom_algo::make_face_with_pcurves(model, surface, &[ring], tol)?.shape;
2093 if let Some((arc, face)) = &arcs[k] {
2094 boundary.push(FillBoundary {
2095 edge: arc.clone(),
2096 support: Some(face.clone()),
2097 continuity: Continuity::G1,
2098 });
2099 }
2100 if let [whole] = trim.as_slice() {
2101 boundary.push(FillBoundary {
2102 edge: whole.clone(),
2103 support: Some(filler.clone()),
2104 continuity: Continuity::G1,
2105 });
2106 }
2107 trims.push(crate::sharp::Trim {
2108 curve: conic,
2109 edges: trim,
2110 normal: outward,
2111 filler: filler.clone(),
2112 });
2113 fillers.push(filler);
2114 }
2115 let mut faces = kept;
2116 faces.extend(fillers);
2117 let mut history: History = history;
2118 let region = |p: Point| signed_region(c, p);
2121 let Some(ks) = c.sharp else {
2122 let filled = make_filling_n_region(model, &boundary, &[], fit, ®ion, tol)?;
2123 let patch = filled.built.shape.clone();
2124 signed_patch_within(model, c, &[&patch], tol)?;
2125 faces.push(patch.clone());
2126 let sewn = ogeom_algo::sew(model, &faces, tol)?;
2127 if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
2128 ogeom_bail!(Construction, "the signed corner's faces did not close");
2129 }
2130 let rounded = model.add_solid(std::slice::from_ref(&sewn.shells[0]))?;
2131 history.generate(&c.vertex, patch);
2132 return Ok(Built::new(rounded, history));
2133 };
2134 let (r1, r2) = ((ks + 1) % 3, (ks + 2) % 3);
2138 let (Some(arc1), Some(arc2)) = (&arcs[r1], &arcs[r2]) else {
2139 ogeom_bail!(Construction, "a rounded edge has no end arc");
2140 };
2141 let patches = crate::sharp::close_through_q(
2142 model,
2143 [&trims[ks], &trims[r1], &trims[r2]],
2144 [arc1, arc2],
2145 [true; 5],
2146 &crate::sharp::Held {
2147 within: &[],
2150 seam: &[(c.normal[(ks + 2) % 3], c.corner), (c.normal[ks], c.corner)],
2154 region: Some(®ion),
2155 },
2156 tol,
2157 )?;
2158 signed_patch_within(model, c, &[&patches.0, &patches.1], tol)?;
2159 crate::sharp::sew_corner(model, faces, patches, &c.vertex, history, tol)
2160}
2161
2162fn signed_patch_within(
2169 model: &Model,
2170 c: &SignedCorner,
2171 patches: &[&Shape],
2172 tol: Tolerances,
2173) -> OgeomResult<()> {
2174 let fit = crate::marched::band_fit_target(tol);
2175 for patch in patches {
2176 let certified = certify_within(model, c, patch, fit, tol)?;
2177 if let Err((out, at)) = certified {
2178 ogeom_bail!(
2179 NotDone,
2180 "the signed corner's patch stands {out} outside what the solid and its \
2181 concave rounds hold, at {at:?}"
2182 );
2183 }
2184 }
2185 Ok(())
2186}
2187
2188fn mesh_outside(
2192 model: &Model,
2193 c: &SignedCorner,
2194 patch: &Shape,
2195 tol: Tolerances,
2196) -> OgeomResult<(f64, Point)> {
2197 let deflection = ogeom_mesh::Deflection {
2198 chord: crate::marched::band_fit_target(tol) * 0.25,
2199 ..ogeom_mesh::Deflection::default()
2200 };
2201 let mesh = ogeom_mesh::triangulate_face(model, patch, deflection, tol)?;
2202 let mut worst = (f64::NEG_INFINITY, Point::ORIGIN);
2203 for p in &mesh.positions {
2204 let out = signed_outside(c, *p);
2205 if out > worst.0 {
2206 worst = (out, *p);
2207 }
2208 }
2209 Ok(worst)
2210}
2211
2212const CERTIFY_DEPTH: u32 = 12;
2215
2216fn certify_within(
2232 model: &Model,
2233 c: &SignedCorner,
2234 patch: &Shape,
2235 fit: f64,
2236 tol: Tolerances,
2237) -> OgeomResult<Result<(), (f64, Point)>> {
2238 let surface = model
2239 .node(patch)
2240 .and_then(|n| n.data().as_face())
2241 .and_then(|d| model.geometry().surface(d.surface));
2242 let Some(ogeom_geom::SurfaceGeometry::BSpline(surface)) = surface else {
2243 let (out, at) = mesh_outside(model, c, patch, tol)?;
2245 return Ok(if out > fit { Err((out, at)) } else { Ok(()) });
2246 };
2247 let grid = surface.grid();
2248 if grid.points().iter().any(|w| w.weight <= 0.0) {
2249 let (out, at) = mesh_outside(model, c, patch, tol)?;
2250 return Ok(if out > fit { Err((out, at)) } else { Ok(()) });
2251 }
2252 let place = patch.transform(model.datums())?;
2253 let chord = crate::marched::band_fit_target(tol) * 0.25;
2256 let rings = ogeom_mesh::face_boundary(
2257 model,
2258 patch,
2259 ogeom_mesh::Deflection {
2260 chord,
2261 ..ogeom_mesh::Deflection::default()
2262 },
2263 tol,
2264 )?;
2265 let slack = chord * 16.0;
2266 let index = TrimIndex::of(&rings);
2267 let near_trim = |lo: ogeom_math::Point2, hi: ogeom_math::Point2| -> bool {
2268 let centre = ogeom_math::Point2::new(f64::midpoint(lo.x, hi.x), f64::midpoint(lo.y, hi.y));
2269 let half = (hi.x - lo.x).hypot(hi.y - lo.y) * 0.5;
2270 index.near(centre, half + slack) || index.inside(centre)
2271 };
2272 let (u_knots, v_knots) = (surface.u_knots(), surface.v_knots());
2273 let (pu, pv) = (u_knots.degree(), v_knots.degree());
2274 let (un, vn) = (grid.u_count(), grid.v_count());
2275 let points: Vec<Point> = grid
2276 .points()
2277 .iter()
2278 .map(|w| place.apply(w.point()))
2279 .collect();
2280 let rational = grid.points().iter().any(|w| w.weight != 1.0);
2281 if rational {
2282 let us: Vec<f64> = u_knots.distinct().iter().map(|k| k.0).collect();
2285 let vs: Vec<f64> = v_knots.distinct().iter().map(|k| k.0).collect();
2286 for w in us.windows(2) {
2287 for z in vs.windows(2) {
2288 let rect = (
2289 ogeom_math::Point2::new(w[0], z[0]),
2290 ogeom_math::Point2::new(w[1], z[1]),
2291 );
2292 if let Err(found) =
2293 certify_segment(surface, c, &place, rect, fit, &near_trim, &rings, 0, tol)?
2294 {
2295 return Ok(Err(found));
2296 }
2297 }
2298 }
2299 return Ok(Ok(()));
2300 }
2301 let mut columns: Vec<Vec<ogeom_math::bspline::BezierSegment<Point>>> = Vec::with_capacity(vn);
2304 for j in 0..vn {
2305 let column: Vec<Point> = (0..un).map(|i| points[i * vn + j]).collect();
2306 columns.push(ogeom_math::bspline::to_bezier_segments(
2307 u_knots, &column, tol,
2308 )?);
2309 }
2310 let spans: Vec<usize> = (0..columns[0].len()).collect();
2313 let found = ogeom_core::parallel::map_ordered(&spans, |_, &s_u| {
2314 let (ua, ub) = columns[0][s_u].0;
2315 let mut rows: Vec<Vec<ogeom_math::bspline::BezierSegment<Point>>> =
2316 Vec::with_capacity(pu + 1);
2317 for k in 0..=pu {
2318 let row: Vec<Point> = columns.iter().map(|col| col[s_u].1[k]).collect();
2319 rows.push(ogeom_math::bspline::to_bezier_segments(v_knots, &row, tol)?);
2320 }
2321 for s_v in 0..rows[0].len() {
2322 let (va, vb) = rows[0][s_v].0;
2323 let net: Vec<Point> = rows.iter().flat_map(|r| r[s_v].1.iter().copied()).collect();
2324 let rect = (
2325 ogeom_math::Point2::new(ua, va),
2326 ogeom_math::Point2::new(ub, vb),
2327 );
2328 let piece = Bezier { net, pu, pv };
2329 if let Err(found) = certify_bezier(
2330 surface, c, &place, &piece, rect, fit, &near_trim, &rings, 0, tol,
2331 )? {
2332 return Ok(Err(found));
2333 }
2334 }
2335 Ok(Ok(()))
2336 });
2337 for result in found {
2338 if let Err(found) = result? {
2339 return Ok(Err(found));
2340 }
2341 }
2342 Ok(Ok(()))
2343}
2344
2345struct Bezier {
2348 net: Vec<Point>,
2349 pu: usize,
2350 pv: usize,
2351}
2352
2353impl Bezier {
2354 fn quarters(&self) -> [Self; 4] {
2358 let (pu, pv) = (self.pu, self.pv);
2359 let at = |net: &[Point], i: usize, j: usize| net[i * (pv + 1) + j];
2360 let mut low = vec![Point::ORIGIN; self.net.len()];
2362 let mut high = vec![Point::ORIGIN; self.net.len()];
2363 for j in 0..=pv {
2364 let mut line: Vec<Point> = (0..=pu).map(|i| at(&self.net, i, j)).collect();
2365 for r in 0..=pu {
2366 low[r * (pv + 1) + j] = line[0];
2367 high[(pu - r) * (pv + 1) + j] = line[pu - r];
2368 for k in 0..pu - r {
2369 line[k] = line[k].midpoint(line[k + 1]);
2370 }
2371 }
2372 }
2373 let split_v = |net: &[Point]| -> (Vec<Point>, Vec<Point>) {
2374 let mut a = vec![Point::ORIGIN; net.len()];
2375 let mut b = vec![Point::ORIGIN; net.len()];
2376 for i in 0..=pu {
2377 let mut line: Vec<Point> = (0..=pv).map(|j| net[i * (pv + 1) + j]).collect();
2378 for r in 0..=pv {
2379 a[i * (pv + 1) + r] = line[0];
2380 b[i * (pv + 1) + pv - r] = line[pv - r];
2381 for k in 0..pv - r {
2382 line[k] = line[k].midpoint(line[k + 1]);
2383 }
2384 }
2385 }
2386 (a, b)
2387 };
2388 let (ll, lh) = split_v(&low);
2389 let (hl, hh) = split_v(&high);
2390 [ll, hl, lh, hh].map(|net| Self { net, pu, pv })
2391 }
2392}
2393
2394#[allow(clippy::too_many_arguments, reason = "one piece of a recursion")]
2398fn certify_bezier(
2399 surface: &ogeom_geom::BSplineSurface,
2400 c: &SignedCorner,
2401 place: &ogeom_math::Transform,
2402 piece: &Bezier,
2403 (lo, hi): (ogeom_math::Point2, ogeom_math::Point2),
2404 fit: f64,
2405 near_trim: &dyn Fn(ogeom_math::Point2, ogeom_math::Point2) -> bool,
2406 rings: &[Vec<ogeom_math::Point2>],
2407 depth: u32,
2408 tol: Tolerances,
2409) -> OgeomResult<Result<(), (f64, Point)>> {
2410 if !near_trim(lo, hi) {
2411 return Ok(Ok(()));
2412 }
2413 let bound = signed_bound(c, &piece.net);
2414 if bound <= fit {
2415 return Ok(Ok(()));
2416 }
2417 let mid = ogeom_math::Point2::new(f64::midpoint(lo.x, hi.x), f64::midpoint(lo.y, hi.y));
2419 let p = place.apply(ogeom_geom::Surface::point_at(surface, mid.x, mid.y, tol)?);
2420 if ogeom_mesh::inside_boundary(rings, mid) {
2421 let out = signed_outside(c, p);
2422 if out > fit {
2423 return Ok(Err((out, p)));
2424 }
2425 }
2426 if depth >= CERTIFY_DEPTH {
2427 return Ok(Err((bound, p)));
2428 }
2429 let quarters = piece.quarters();
2430 let rects = [
2431 (lo, mid),
2432 (
2433 ogeom_math::Point2::new(mid.x, lo.y),
2434 ogeom_math::Point2::new(hi.x, mid.y),
2435 ),
2436 (
2437 ogeom_math::Point2::new(lo.x, mid.y),
2438 ogeom_math::Point2::new(mid.x, hi.y),
2439 ),
2440 (mid, hi),
2441 ];
2442 for (quarter, rect) in quarters.iter().zip(rects) {
2443 if let Err(found) = certify_bezier(
2444 surface,
2445 c,
2446 place,
2447 quarter,
2448 rect,
2449 fit,
2450 near_trim,
2451 rings,
2452 depth + 1,
2453 tol,
2454 )? {
2455 return Ok(Err(found));
2456 }
2457 }
2458 Ok(Ok(()))
2459}
2460
2461#[allow(clippy::too_many_arguments, reason = "one rectangle of a recursion")]
2464fn certify_segment(
2465 surface: &ogeom_geom::BSplineSurface,
2466 c: &SignedCorner,
2467 place: &ogeom_math::Transform,
2468 (lo, hi): (ogeom_math::Point2, ogeom_math::Point2),
2469 fit: f64,
2470 near_trim: &dyn Fn(ogeom_math::Point2, ogeom_math::Point2) -> bool,
2471 rings: &[Vec<ogeom_math::Point2>],
2472 depth: u32,
2473 tol: Tolerances,
2474) -> OgeomResult<Result<(), (f64, Point)>> {
2475 if !near_trim(lo, hi) {
2476 return Ok(Ok(()));
2477 }
2478 let piece = surface.segment((lo.x, hi.x), (lo.y, hi.y), tol)?;
2479 let hull: Vec<Point> = piece
2480 .grid()
2481 .points()
2482 .iter()
2483 .map(|w| place.apply(w.point()))
2484 .collect();
2485 let bound = signed_bound(c, &hull);
2486 if bound <= fit {
2487 return Ok(Ok(()));
2488 }
2489 let mid = ogeom_math::Point2::new(f64::midpoint(lo.x, hi.x), f64::midpoint(lo.y, hi.y));
2490 let p = place.apply(ogeom_geom::Surface::point_at(surface, mid.x, mid.y, tol)?);
2491 if ogeom_mesh::inside_boundary(rings, mid) {
2492 let out = signed_outside(c, p);
2493 if out > fit {
2494 return Ok(Err((out, p)));
2495 }
2496 }
2497 if depth >= CERTIFY_DEPTH {
2498 return Ok(Err((bound, p)));
2499 }
2500 for rect in [
2501 (lo, mid),
2502 (
2503 ogeom_math::Point2::new(mid.x, lo.y),
2504 ogeom_math::Point2::new(hi.x, mid.y),
2505 ),
2506 (
2507 ogeom_math::Point2::new(lo.x, mid.y),
2508 ogeom_math::Point2::new(mid.x, hi.y),
2509 ),
2510 (mid, hi),
2511 ] {
2512 if let Err(found) = certify_segment(
2513 surface,
2514 c,
2515 place,
2516 rect,
2517 fit,
2518 near_trim,
2519 rings,
2520 depth + 1,
2521 tol,
2522 )? {
2523 return Ok(Err(found));
2524 }
2525 }
2526 Ok(Ok(()))
2527}
2528
2529struct TrimIndex {
2533 segments: Vec<(ogeom_math::Point2, ogeom_math::Point2)>,
2534 lo: ogeom_math::Point2,
2535 hi: ogeom_math::Point2,
2536 step: (f64, f64),
2537 cells: Vec<Vec<usize>>,
2538 bands: Vec<Vec<usize>>,
2539}
2540
2541const TRIM_CELLS: usize = 64;
2543
2544impl TrimIndex {
2545 fn of(rings: &[Vec<ogeom_math::Point2>]) -> Self {
2546 let mut segments = Vec::new();
2547 for ring in rings {
2548 for k in 0..ring.len() {
2549 segments.push((ring[k], ring[(k + 1) % ring.len()]));
2550 }
2551 }
2552 let (mut lo, mut hi) = (
2553 ogeom_math::Point2::new(f64::INFINITY, f64::INFINITY),
2554 ogeom_math::Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
2555 );
2556 for (a, b) in &segments {
2557 for p in [a, b] {
2558 lo = ogeom_math::Point2::new(lo.x.min(p.x), lo.y.min(p.y));
2559 hi = ogeom_math::Point2::new(hi.x.max(p.x), hi.y.max(p.y));
2560 }
2561 }
2562 #[expect(clippy::cast_precision_loss, reason = "a cell count of sixty-four")]
2563 let count = TRIM_CELLS as f64;
2564 let step = (
2565 ((hi.x - lo.x) / count).max(f64::MIN_POSITIVE),
2566 ((hi.y - lo.y) / count).max(f64::MIN_POSITIVE),
2567 );
2568 let mut index = Self {
2569 segments,
2570 lo,
2571 hi,
2572 step,
2573 cells: vec![Vec::new(); TRIM_CELLS * TRIM_CELLS],
2574 bands: vec![Vec::new(); TRIM_CELLS],
2575 };
2576 for (k, (a, b)) in index.segments.iter().enumerate() {
2577 let (i0, j0) = index.cell(ogeom_math::Point2::new(a.x.min(b.x), a.y.min(b.y)));
2578 let (i1, j1) = index.cell(ogeom_math::Point2::new(a.x.max(b.x), a.y.max(b.y)));
2579 for i in i0..=i1 {
2580 for j in j0..=j1 {
2581 index.cells[i * TRIM_CELLS + j].push(k);
2582 }
2583 }
2584 for band in &mut index.bands[j0..=j1] {
2585 band.push(k);
2586 }
2587 }
2588 index
2589 }
2590
2591 fn cell(&self, p: ogeom_math::Point2) -> (usize, usize) {
2593 #[expect(
2594 clippy::cast_possible_truncation,
2595 clippy::cast_sign_loss,
2596 reason = "a cell index, clamped into the grid"
2597 )]
2598 let at = |t: f64, lo: f64, step: f64| -> usize {
2599 (((t - lo) / step).floor().max(0.0) as usize).min(TRIM_CELLS - 1)
2600 };
2601 (
2602 at(p.x, self.lo.x, self.step.0),
2603 at(p.y, self.lo.y, self.step.1),
2604 )
2605 }
2606
2607 fn near(&self, p: ogeom_math::Point2, reach: f64) -> bool {
2611 let (i0, j0) = self.cell(ogeom_math::Point2::new(p.x - reach, p.y - reach));
2612 let (i1, j1) = self.cell(ogeom_math::Point2::new(p.x + reach, p.y + reach));
2613 (i0..=i1).any(|i| {
2614 (j0..=j1).any(|j| {
2615 self.cells[i * TRIM_CELLS + j].iter().any(|&k| {
2616 let (a, b) = self.segments[k];
2617 segment_distance(p, a, b) <= reach
2618 })
2619 })
2620 })
2621 }
2622
2623 fn inside(&self, p: ogeom_math::Point2) -> bool {
2627 if p.y < self.lo.y || p.y > self.hi.y {
2628 return false;
2629 }
2630 let (_, j) = self.cell(p);
2631 let mut crossings = 0usize;
2632 for &k in &self.bands[j] {
2633 let (a, b) = self.segments[k];
2634 if (a.y > p.y) != (b.y > p.y) {
2635 let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
2636 if x > p.x {
2637 crossings += 1;
2638 }
2639 }
2640 }
2641 crossings % 2 == 1
2642 }
2643}
2644
2645fn segment_distance(p: ogeom_math::Point2, a: ogeom_math::Point2, b: ogeom_math::Point2) -> f64 {
2647 let (ux, uy) = (b.x - a.x, b.y - a.y);
2648 let length = ux.mul_add(ux, uy * uy);
2649 let t = if length > 0.0 {
2650 (((p.x - a.x) * ux + (p.y - a.y) * uy) / length).clamp(0.0, 1.0)
2651 } else {
2652 0.0
2653 };
2654 (p.x - (a.x + ux * t)).hypot(p.y - (a.y + uy * t))
2655}
2656
2657fn signed_bound(c: &SignedCorner, points: &[Point]) -> f64 {
2664 let xs: Vec<Vector> = points.iter().map(|p| *p - c.corner).collect();
2665 let top = |n: Vector| {
2666 xs.iter()
2667 .map(|x| n.dot(*x))
2668 .fold(f64::NEG_INFINITY, f64::max)
2669 };
2670 let h = |f: usize| top(c.normal[f]);
2671 let concave: Vec<usize> = (0..3)
2672 .filter(|&k| c.legs[k][1].dot(c.normal[(k + 2) % 3]) >= 0.0)
2673 .collect();
2674 let solid = match concave.as_slice() {
2675 [k] => h((k + 1) % 3).max(h((k + 2) % 3).min(h(*k))),
2676 [k1, k2] => {
2677 let faces = |k: usize| [(k + 2) % 3, k];
2678 let shared = (0..3)
2679 .find(|f| faces(*k1).contains(f) && faces(*k2).contains(f))
2680 .unwrap_or(0);
2681 let others: Vec<usize> = (0..3).filter(|&f| f != shared).collect();
2682 h(shared).min(h(others[0]).max(h(others[1])))
2683 }
2684 _ => h(0).max(h(1)).max(h(2)),
2685 };
2686 let mut out = solid;
2687 for &k in &concave {
2688 let r = c.radius[k];
2689 if r <= 0.0 {
2690 continue;
2691 }
2692 let (na, nb, d) = (c.normal[(k + 2) % 3], c.normal[k], c.d[k]);
2693 let centre = (na + nb) * (r / (1.0 + na.dot(nb)));
2694 let contact = centre - na * r;
2695 let towards = na + nb;
2696 let towards = towards / towards.magnitude();
2697 let (e1, e2) = crate::rolled::basis(d);
2700 let flat: Vec<(f64, f64)> = xs.iter().map(|x| (e1.dot(*x), e2.dot(*x))).collect();
2701 let axis = (e1.dot(centre), e2.dot(centre));
2702 let ball = r - hull_distance(&flat, axis);
2703 let fill = top(-na)
2704 .max(top(-nb))
2705 .max(ball)
2706 .max(top(towards) - towards.dot(contact))
2707 .max(top(-d));
2708 out = out.min(fill);
2709 }
2710 out
2711}
2712
2713fn hull_distance(points: &[(f64, f64)], p: (f64, f64)) -> f64 {
2716 let mut sorted: Vec<(f64, f64)> = points.to_vec();
2717 sorted.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1)));
2718 sorted.dedup();
2719 if sorted.len() == 1 {
2720 return (sorted[0].0 - p.0).hypot(sorted[0].1 - p.1);
2721 }
2722 let cross = |o: (f64, f64), a: (f64, f64), b: (f64, f64)| {
2723 (a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0)
2724 };
2725 let mut hull: Vec<(f64, f64)> = Vec::with_capacity(2 * sorted.len());
2726 for &q in sorted.iter().chain(sorted.iter().rev().skip(1)) {
2727 while hull.len() >= 2 && cross(hull[hull.len() - 2], hull[hull.len() - 1], q) <= 0.0 {
2728 hull.pop();
2729 }
2730 hull.push(q);
2731 }
2732 hull.pop();
2733 let n = hull.len();
2734 let edge = |a: (f64, f64), b: (f64, f64)| {
2735 segment_distance(
2736 ogeom_math::Point2::new(p.0, p.1),
2737 ogeom_math::Point2::new(a.0, a.1),
2738 ogeom_math::Point2::new(b.0, b.1),
2739 )
2740 };
2741 if n < 3 {
2742 return (0..n)
2743 .map(|k| edge(hull[k], hull[(k + 1) % n]))
2744 .fold(f64::INFINITY, f64::min);
2745 }
2746 if (0..n).all(|k| cross(hull[k], hull[(k + 1) % n], p) >= 0.0) {
2748 return 0.0;
2749 }
2750 (0..n)
2751 .map(|k| edge(hull[k], hull[(k + 1) % n]))
2752 .fold(f64::INFINITY, f64::min)
2753}
2754
2755fn signed_outside(c: &SignedCorner, p: Point) -> f64 {
2762 signed_region(c, p).0
2763}
2764
2765fn signed_region(c: &SignedCorner, p: Point) -> (f64, Vector) {
2770 let x = p - c.corner;
2771 let h = |f: usize| (c.normal[f].dot(x), c.normal[f]);
2772 let most = |a: (f64, Vector), b: (f64, Vector)| if b.0 > a.0 { b } else { a };
2773 let least = |a: (f64, Vector), b: (f64, Vector)| if b.0 < a.0 { b } else { a };
2774 let concave: Vec<usize> = (0..3)
2777 .filter(|&k| c.legs[k][1].dot(c.normal[(k + 2) % 3]) >= 0.0)
2778 .collect();
2779 let solid = match concave.as_slice() {
2780 [k] => most(h((k + 1) % 3), least(h((k + 2) % 3), h(*k))),
2781 [k1, k2] => {
2782 let faces = |k: usize| [(k + 2) % 3, k];
2783 let shared = (0..3)
2784 .find(|f| faces(*k1).contains(f) && faces(*k2).contains(f))
2785 .unwrap_or(0);
2786 let others: Vec<usize> = (0..3).filter(|&f| f != shared).collect();
2787 least(h(shared), most(h(others[0]), h(others[1])))
2788 }
2789 _ => most(most(h(0), h(1)), h(2)),
2790 };
2791 let mut out = solid;
2792 for &k in &concave {
2793 let r = c.radius[k];
2794 if r <= 0.0 {
2795 continue;
2796 }
2797 let (na, nb, d) = (c.normal[(k + 2) % 3], c.normal[k], c.d[k]);
2798 let centre = (na + nb) * (r / (1.0 + na.dot(nb)));
2799 let across = x - d * d.dot(x);
2800 let contact = centre - na * r;
2801 let towards = na + nb;
2802 let towards = towards / towards.magnitude();
2803 let off = across - centre;
2804 let reach = off.magnitude();
2805 let ball = if reach > 0.0 {
2806 (r - reach, off * (-1.0 / reach))
2807 } else {
2808 (r, -towards)
2809 };
2810 let fill = most(
2811 most(
2812 most((-na.dot(x), -na), (-nb.dot(x), -nb)),
2813 most(ball, (towards.dot(across - contact), towards)),
2814 ),
2815 (-d.dot(x), -d),
2816 );
2817 out = least(out, fill);
2818 }
2819 out
2820}
2821
2822#[cfg(test)]
2823mod tests {
2824 #![allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
2825
2826 use super::{SignedCorner, certify_within, mesh_outside, signed_corners};
2827 use ogeom_core::Tolerances;
2828 use ogeom_geom::{Curve3d as _, SurfaceGeometry};
2829 use ogeom_math::{Direction, Frame, Point};
2830 use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
2831
2832 const T: Tolerances = Tolerances::millimetres();
2833
2834 fn edge_near(m: &Model, s: &Shape, q: Point) -> Shape {
2835 let d = |e: &Shape| {
2836 let (c, (a, b)) = ogeom_algo::placed_edge_curve(m, e, T).unwrap().unwrap();
2837 (0..=64)
2838 .map(|i| {
2839 c.point_at(a + (b - a) * f64::from(i) / 64.0, T)
2840 .unwrap()
2841 .distance(q)
2842 })
2843 .fold(f64::INFINITY, f64::min)
2844 };
2845 explore_unique(m, s, ShapeType::Edge)
2846 .unwrap()
2847 .into_iter()
2848 .min_by(|a, b| d(a).total_cmp(&d(b)))
2849 .unwrap()
2850 }
2851
2852 fn corner(radius: f64, three: bool) -> (Model, SignedCorner, Vec<Shape>) {
2856 let mut m = Model::new();
2857 let frame = Frame::new(Point::ORIGIN, Direction::Z, Direction::X, T).unwrap();
2858 let a = ogeom_algo::make_box(&mut m, frame, (20.0, 20.0, 5.0), T)
2859 .unwrap()
2860 .shape;
2861 let b = ogeom_algo::make_box(&mut m, frame, (5.0, 20.0, 20.0), T)
2862 .unwrap()
2863 .shape;
2864 let l = ogeom_bool::fuse(&mut m, &a, &b, T).unwrap().shape;
2865 let mut edges = vec![
2866 edge_near(&m, &l, Point::new(12.5, 0.0, 5.0)),
2867 edge_near(&m, &l, Point::new(5.0, 10.0, 5.0)),
2868 ];
2869 if three {
2870 edges.push(edge_near(&m, &l, Point::new(5.0, 0.0, 12.5)));
2871 }
2872 let request: Vec<(Shape, f64)> = edges.iter().map(|e| (e.clone(), radius)).collect();
2873 let c = signed_corners(&m, &l, &request, T)
2874 .unwrap()
2875 .into_iter()
2876 .next()
2877 .expect("the bracket's signed corner");
2878 let rounded = crate::fillet_edges(&mut m, &l, &edges, radius, T)
2879 .unwrap()
2880 .shape;
2881 let patches: Vec<Shape> = explore_unique(&m, &rounded, ShapeType::Face)
2882 .unwrap()
2883 .into_iter()
2884 .filter(|f| {
2885 let data = m.node(f).unwrap().data().as_face().unwrap();
2886 matches!(
2887 m.geometry().surface(data.surface),
2888 Some(SurfaceGeometry::BSpline(_))
2889 ) && ogeom_algo::shape_bounds(&m, f, T)
2890 .unwrap()
2891 .distance_to(Point::new(5.0, 0.0, 5.0))
2892 < 2.0 * radius
2893 })
2894 .collect();
2895 assert!(!patches.is_empty(), "the corner's patches");
2896 (m, c, patches)
2897 }
2898
2899 #[test]
2906 fn the_certified_check_agrees_with_the_fine_mesh() {
2907 let fit = crate::marched::band_fit_target(T);
2908 for (radius, three) in [(2.0, false), (2.0, true)] {
2909 let (m, c, patches) = corner(radius, three);
2910 for patch in &patches {
2911 let sampled = mesh_outside(&m, &c, patch, T).unwrap();
2912 let proved = certify_within(&m, &c, patch, fit, T).unwrap();
2913 assert!(sampled.0 <= fit, "the mesh finds {} outside", sampled.0);
2914 assert!(proved.is_ok(), "the proof refuses {proved:?}");
2915 let strict = -0.1 * fit;
2916 let proved = certify_within(&m, &c, patch, strict, T).unwrap();
2917 assert!(sampled.0 > strict, "the mesh finds every point well inside");
2918 assert!(proved.is_err(), "the proof clears what the mesh does not");
2919 }
2920 }
2921 }
2922}