Skip to main content

ogeom_fillet/
setback.rs

1//! Setback corners: the bands meeting at a vertex stopped a chosen
2//! distance along their edges, and one N-sided patch over the opening.
3//!
4//! The bands are rounded as [`fillet_edges`](crate::fillet_edges) rounds
5//! them, meeting in the ball's own corner. A cell is then cut from the
6//! corner: the polyhedron bounded by the planes through the vertex and,
7//! for each edge, the plane square to it at its setback. What is left of
8//! each band ends on its section plane in an exact arc. On each face the
9//! opening is closed by a conic from one band's rail end to the next one's,
10//! tangent to both rails, so the face's trim turns smoothly where the
11//! bands leave it. The opening is then a loop of arcs and conics, filled
12//! by one patch meeting each band and each face at the continuity asked.
13
14use 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/// How far each band meeting a vertex stops from it, and how the patch
23/// between them meets the bands and the faces.
24#[derive(Debug, Clone)]
25pub struct Setbacks {
26    /// Distance from the vertex along each edge at which that edge's band
27    /// stops; edges not listed take `default`.
28    pub per_edge: Vec<(Shape, f64)>,
29    /// The setback of every edge not listed.
30    pub default: f64,
31    /// How the patch meets each band and face: [`Continuity::G1`]
32    /// (tangent). [`Continuity::G2`] is refused while bands are circular,
33    /// G1 to their faces.
34    pub continuity: Continuity,
35}
36
37/// Round `vertex` of `solid` with the bands of its edges at `radius`, each
38/// stopped at its setback, and one patch over the opening between them.
39///
40/// Each band is cut square to its edge at its setback, measured from the
41/// vertex along the edge. On each face between two bands the trim runs
42/// from one band's rail end to the next one's on a conic tangent to both
43/// rails. The opening those arcs and conics bound is filled by one patch
44/// meeting each band end and each face at `setbacks.continuity`, fitted
45/// to the bands' own fit target. A setback equal to the ball's own (the
46/// distance along the edge to the foot of the ball's centre, the radius at
47/// a square corner) on every edge is [`fillet_edges`](crate::fillet_edges)'
48/// corner.
49///
50/// What `solid` shares with other shapes is copied before it is worked on
51/// ([`ogeom_algo::on_own_nodes`]), which leaves them as they are.
52///
53/// # Errors
54///
55/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) where
56/// `radius` or a setback is not a positive length, a setback is shorter
57/// than the ball's own along its edge or reaches past the edge's far end,
58/// the continuity is neither G1 nor G2, the vertex is not a convex planar
59/// corner one ball touches every face of (a curved face meeting it, or
60/// planes sharing no tangent ball, are still owed), or the opening does not
61/// close as a loop of the bands' arcs and the faces' conics;
62/// [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) where the
63/// patch misses its target.
64pub 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
89/// The point a vertex stands at.
90pub(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
100/// A setback corner read off the solid before its edges are rounded: the
101/// corner's frame, the solid's edge along each of its edge directions with
102/// that edge's length, and each edge's setback and the ball's own setback
103/// along it.
104struct 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    /// Whether every setback is the ball's own, the corner `fillet_edges`
114    /// closes.
115    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
123/// The setback corner at `vertex` of `solid`, its edges not yet rounded,
124/// checked as [`round_vertex_setback`] states.
125fn 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        // Where a band's end arc meets a face, the arc (radius r, tangent
138        // to the face) bends the patch at 1/r along itself, and the face
139        // asks it not to bend across the face's trim, which runs square to
140        // the arc there: no patch is G2 to both while the band itself is
141        // only G1 to the face.
142        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
206/// [`round_vertex_setback`] on a shape every node below which is its own.
207fn 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    // The bands, meeting in the ball's corner: all that a setback equal to
218    // the ball's own asks for.
219    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
235/// Round `edges` of `solid` at `radius`, as [`fillet_edges`](crate::fillet_edges)
236/// does, with each vertex of `corners` closed as a setback corner, as
237/// [`round_vertex_setback`] closes it.
238///
239/// Every corner is read off the solid before its edges are rounded, the
240/// edges are rounded once, and each corner is then cut back to its
241/// setbacks and its opening filled. Vertices not named close in the
242/// ball's own corner. A setback corner rounds every edge that meets its
243/// vertex, so each of them must be in `edges`. Along an edge between two
244/// named corners, the two setbacks must stop short of each other.
245///
246/// What `solid` shares with other shapes is copied before it is worked on
247/// ([`ogeom_algo::on_own_nodes`]), which leaves them as they are.
248///
249/// # Errors
250///
251/// As [`round_vertex_setback`] for each corner and
252/// [`fillet_edges`](crate::fillet_edges) for the edges;
253/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction)
254/// where a vertex is named twice, an edge meeting a named vertex is not in
255/// `edges`, or the setbacks at both ends of an edge meet.
256pub 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
293/// [`fillet_edges_cornered`] on a shape every node below which is its own.
294fn 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    // Two corners on one edge cut it back from both ends: the cells must
322    // stop short of each other along it, each band's middle left whole.
323    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
363/// The solid's edge along each of `frame`'s edge directions from `vertex`,
364/// and each one's length; refused by name where none does, or where the
365/// nearest is curved.
366pub(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/// How a band at a setback corner ends where the patch meets it.
423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
424pub(crate) enum CornerEnd {
425    /// A round: its end arc on its section plane, which the patch meets at
426    /// the corner's continuity.
427    Round,
428    /// A bevel: the straight end of its face on its section plane, which
429    /// the patch meets in position only, as it meets the trims from that
430    /// end on the faces either side: the bevel meets those faces in a
431    /// crease, and the patch, running along the bevel's end, leaves each
432    /// face at the bevel's angle there.
433    Bevel,
434}
435
436/// Refused by name unless the cell cut from `banded` at `vertex` holds
437/// nothing of `solid`, the solid the corner was asked on, but the corner
438/// itself. What is foreign is a face of `solid` that does not meet the
439/// vertex and lies on none of the corner's own planes through it
440/// (`planes`, each a normal and a point): the bands, the bevels and the
441/// ball's own corner are made by the rounding, on surfaces of their own.
442/// Refused where the cut takes a face on a foreign surface whole (every
443/// corner of it inside the cell, `walls`, each an inward normal and a
444/// point), where a face on a foreign surface that the cut leaves has an
445/// edge on a wall inside the cell (the cut passed through it), or where the
446/// cut leaves fewer shells. Any of these (a void, a hole, another feature)
447/// would be left out of what is sewn back, and the solid would close
448/// without it.
449#[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    // A face the cut takes whole.
509    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    // A face the cut passes through.
525    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/// The bands of `banded` meeting at `vertex`, whose corner reads as
547/// `frame`, stopped at `setback` along their edges and the opening between
548/// them filled by one patch at `continuity`.
549#[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/// As [`close_corner`], each band ending as `ends` says.
567#[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    // The cell the patch replaces: in from every plane through the vertex,
585    // and short of each band's section.
586    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    // Each band's end arc, on its section plane, and the band face it ends.
638    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    // On host plane `ring[k + 1]`, between band `k` and band `k + 1`: each
655    // band's arc end there, and where the two section planes meet it.
656    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, &sections[k], &sections[next]) else {
679            ogeom_bail!(
680                Construction,
681                "two section planes do not meet on the face between them"
682            );
683        };
684        // The rails run along the edges: each conic leaves its band's rail
685        // along that edge's direction, towards the vertex.
686        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        // A trim from a bevel's end meets its face in position only: the
699        // patch runs along the bevel's end there, which leaves the face at
700        // the bevel's angle, so it cannot take the face's tangent plane.
701        // Between two rounds both ends lie tangent to the face.
702        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    // The vertex is convex, so what of the patch stands past one of its
715    // planes lies outside the solid: the patch is held within them.
716    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    // Sewing may rebuild a face whose edges it joins: an earlier corner's
728    // patch, as well as this one, is followed to the face it became.
729    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/// The edge on `conic` from `v0` to `v1`, and the face's filler inside the
737/// corner's cell: the planar face on `plane` bounded by the trim and the
738/// two section lines from its ends to `q`, where they meet.
739#[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/// The face's filler inside the corner's cell: the planar face on `plane`
765/// bounded by `trim`, the edges in order from `v0` to `v1` along a trim
766/// whose middle control point is `apex`, and the two section lines from
767/// its ends to `q`, where they meet.
768#[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
810/// The conic from `p0` to `p1` on a face, tangent at each end to the line
811/// from it along its `t` (pointing into the conic), and its middle control
812/// point, where those lines meet; weighted by the cosine of half the turn
813/// between them, refused by name where the lines are parallel or do not run
814/// towards each other.
815pub(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        // p0 + a t0 = p1 + b t1, in the plane.
822        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
851/// Whether `p` lies inside the convex cell bounded by `walls`, each an
852/// inward normal and a point on it, to the slack the corner's planes are
853/// read to: this corner's, not another's on the same planes.
854pub(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
860/// Band `k`'s end arc on its section plane `section`, and the band face it
861/// ends: the curved edge of a curved face of `kept` lying on the plane
862/// inside the cell `walls` bound.
863pub(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
911/// A bevel's straight end on its section plane `section`, and the bevel's
912/// face: the planar face of `kept`, on none of the corner's host planes,
913/// with a straight edge lying on the section plane inside the cell `walls`
914/// bound.
915pub(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
968/// Close the corner at `vertex` of `banded`, where the bands of `request`
969/// (each edge with its radius) meet at different radii and no ball rolls
970/// round: each band stopped one of its own widths past the least setback
971/// where its section clears every other band, and the opening filled G1
972/// as [`round_vertex_setback`] fills it.
973///
974/// `banded` holds the bands as they were built up to one another, the
975/// later run on through the earlier; all of that lies inside the cell the
976/// patch replaces, so what is left does not depend on the order they were
977/// built in.
978pub(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    // Each corner edge's radius: the request edge leaving the vertex along
990    // it.
991    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
1038/// For each edge of the corner, the setback its band stops at: the least
1039/// distance along the edge at which the band's section clears every other
1040/// band of the corner, and past that the band's own width on its faces.
1041/// At the least distance a rail's end stands on its neighbour's rail line
1042/// and the face's trim would leave it square; one width further, the trim
1043/// leaves each rail along a leg as long as its band is wide.
1044///
1045/// Band `k` of radius `r_k` between planes at an interior angle `phi_k`
1046/// touches each plane `w_k = r_k (1 + cos phi_k) / sin phi_k` from its
1047/// edge, and everything it takes away lies within `w_k` of the edge's
1048/// line. Its section at distance `s` is the triangle of the edge's point
1049/// and the two touch points; the distance from another edge's line is
1050/// convex over it, so its corners decide. Each corner's distance to line
1051/// `j` is `|A s + B|`, increasing past `s* = -A·B / A·A`; the band clears
1052/// band `j` from the larger root of `|A s + B| = w_j`, or from `s*` where
1053/// it is never nearer.
1054fn 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
1077/// As [`least_setbacks`], each band taking `widths[k]` off the two faces of
1078/// its edge, in the order of the corner's ring: the first on the plane
1079/// `ring[k]`, the second on `ring[k + 1]`. Everything a band takes away
1080/// lies within the larger of its two widths of its edge's line.
1081pub(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        // Into each face, square to the edge.
1101        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    // And each band, up to its own section, short of every other band's
1135    // section plane: band `j` takes away the sweep of its section triangle
1136    // from the vertex to `s_j`, which reaches along `d_k` as far as
1137    // `s_j max(0, d_j·d_k) + o·d_k` at its farthest corner `o`, a touch
1138    // point on its rail. Where that reaches band `k`'s section, band `k`
1139    // stops one of its own widths past it. The bounds tie the setbacks
1140    // together, each a share below one of the others, so they settle by
1141    // going round until none moves.
1142    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            // An edge of no width (one left sharp) moves only where it is
1158            // passed, or it would be moved by nothing for ever.
1159            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
1175/// A vertex where a convex and a concave band of one request meet: three
1176/// straight edges, every one of them rounded, and the three planar faces
1177/// between them.
1178pub(crate) struct SignedCorner {
1179    /// The vertex itself, a node of the solid the request was given on.
1180    pub vertex: Shape,
1181    corner: Point,
1182    /// Each edge's unit direction away from the vertex, in ring order.
1183    d: Vec<Vector>,
1184    /// Each edge's radius.
1185    radius: Vec<f64>,
1186    /// Face `i`, between edge `i` and edge `i + 1`: its outward normal.
1187    normal: Vec<Vector>,
1188    /// Edge `i`'s legs: the unit directions square to it into face `i - 1`
1189    /// and face `i`, its two faces.
1190    legs: Vec<[Vector; 2]>,
1191    /// The edge the request leaves sharp, by its place in the ring: a band
1192    /// of no width, its radius nought.
1193    sharp: Option<usize>,
1194    /// Each edge's length, from the vertex to its far end.
1195    reach: Vec<f64>,
1196}
1197
1198/// A request edge with its radius and whether it is convex.
1199type Kinded = (Shape, f64, bool);
1200
1201/// The vertices of `solid` where the request's bands of both kinds meet,
1202/// each read as a [`SignedCorner`]; refused by name where such a vertex is
1203/// not one this closes.
1204///
1205/// An edge of such a vertex the request leaves sharp is read as a band of
1206/// no width: the opening between the bands' ends crosses it, and is closed
1207/// through a point on it where a patch turns its normal from one of its
1208/// faces to the other's ([`crate::sharp::close_through_q`]). Such an edge
1209/// is to be convex.
1210pub(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
1228/// The cusp corners of the request: vertices where its convex and concave
1229/// bands meet that [`crate::cusp::cusp_corner`] reads as one.
1230pub(crate) fn cusp_corners(
1231    model: &Model,
1232    solid: &Shape,
1233    request: &[(Shape, f64)],
1234    tol: Tolerances,
1235) -> OgeomResult<Vec<crate::cusp::CuspCorner>> {
1236    // Read in node order, so the corner and its sides are the same in any
1237    // order asked.
1238    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
1249/// The vertices two or more of the request's edges meet at, bands of both
1250/// kinds among them, each with its edges, radii and kinds.
1251fn signed_vertices(
1252    model: &Model,
1253    solid: &Shape,
1254    request: &[(Shape, f64)],
1255    tol: Tolerances,
1256) -> OgeomResult<Vec<(Shape, Vec<Kinded>)>> {
1257    // The vertices two or more of the request's edges meet at.
1258    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    // Each such edge's kind, where it reads.
1275    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
1306/// Whether the request's two edges at `vertex`, one convex and one
1307/// concave, run on through it into each other, tangent there and leaving
1308/// it opposite ways, where a face of the vertex is curved: a tangent chain
1309/// turning from convex to concave where a wall runs tangent into a curved
1310/// one (a plate's side into a boss). The two bands then lie either side of
1311/// the vertex and meet only across their end sections there, so there is
1312/// no corner to close: each is built as the request's own bands are, and
1313/// refused as they are where its face narrows to a cusp there and would
1314/// leave the band's fill over nothing.
1315fn 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    // Each edge's unit tangent at the vertex, pointing away from it.
1327    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
1357/// Whether `edge` of `solid` is convex: from its planar seat, else read on
1358/// its two faces at its middle as a marched seat reads it; `None` where it
1359/// does not part two faces.
1360fn 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
1403/// The refusal for a signed corner this does not close.
1404fn 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    // The faces at the vertex, coplanar neighbours (a fused solid's seam)
1422    // taken as one: each group's planar faces and its outward normal.
1423    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    // The edges at the vertex between two groups; a seam inside one is none.
1447    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    // Each face's two edges at the vertex.
1514    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    // The ring: edge 0, the face after it, the edge after that, and so on.
1526    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    // Into a face square to an edge: the side a point just off the edge's
1550    // middle lies inside the face on.
1551    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    // A sharp edge's patch is a cone at its point, which turns outward
1577    // across a convex edge: the face after it leans in from the face
1578    // before's outward side.
1579    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
1596/// Each band's width on its faces: `r tan(a / 2)`, `a` the turn between
1597/// its faces' normals, the same for a convex band and a concave one.
1598fn 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
1613/// For each edge, the setback its band stops at: as [`least_setbacks`]
1614/// reads it from each band's section triangle (the edge's point and its
1615/// touch on each face), which holds what the band takes away or adds,
1616/// whichever its kind.
1617fn 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    // The sharp edge is cut past where the bands reach along it by the
1650    // widest band, so the trims from its point have room to turn into the
1651    // bands' rails.
1652    if let Some(k) = c.sharp {
1653        setback[k] += width.iter().copied().fold(0.0, f64::max);
1654    }
1655    // Each edge is cut short of its far end, where its band's section
1656    // would fall on the face there and leave no end arc.
1657    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/// Close the signed corner `c` of `banded`: each band stopped at its
1669/// setback and the opening between them filled by one patch G1 to every
1670/// band end and face.
1671///
1672/// The cell cut away is the box square to the three edges, from each
1673/// band's section back past the vertex by twice the widest band, so it
1674/// holds every meeting of the bands; whatever order they were built in,
1675/// all of that goes. Its walls behind the vertex stand in the material,
1676/// where the solid before and after agree, so the faces the cut leaves on
1677/// every wall are dropped and nothing is put back there. On each face the
1678/// trim runs on a conic from one band's rail end to the next one's,
1679/// tangent to both; the face's part kept inside the box is the side of the
1680/// conic away from the vertex, out to the box's walls.
1681///
1682/// With an edge left sharp, that edge is cut at its setback, at `Q`, and
1683/// the trims beside it run from `Q` into the bands' rails, split at their
1684/// middles; on a face reflex at the vertex the trim is a cubic swinging
1685/// round the vertex inside the face. The opening is then closed as
1686/// [`crate::round_vertex_setback_sharp`] closes it: a patch at `Q`, its
1687/// fourth side shrunk there, and one over the rest.
1688#[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    // Inside the cell, to the slack: this corner's, not another's on the
1729    // same planes.
1730    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    // Every corner of the kept faces.
1757    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    // Each band's end arc on its section plane, and the band face it ends.
1767    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    // Where the cut leaves the sharp edge, at its setback: `Q`.
1805    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        // A rail's trim leaves it along its edge, towards the vertex; one
1852        // from the sharp edge's point heads for the band's rail half way
1853        // back to the vertex.
1854        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        // On a face reflex at the vertex, beside the sharp edge, the trim
1863        // from its point swings round the vertex inside the face: a cubic
1864        // leaving the point into the face half way between the sharp edge,
1865        // back towards the vertex, and square across it, and coming onto
1866        // the band's rail from a band's width past the sharp edge's line.
1867        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        // The face's plane inside the box: a convex polygon, ordered round
1925        // its middle.
1926        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        // Where each rail end sits on the polygon's boundary.
1953        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        // The two ways round the polygon from p1 back to p0.
1967        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        // The straight run back from p1 to p0, broken wherever a kept
2024        // face's corner stands on it (a seam of coplanar faces running
2025        // into the cell), so each piece meets one kept edge.
2026        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    // The patches are held inside what the solid and its concave rounds
2119    // hold near the vertex: out of each concave round's ball.
2120    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, &region, 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    // The opening runs into the sharp edge's point: a patch there turning
2135    // its normal from one of the edge's faces to the other's, and one over
2136    // the rest.
2137    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            // A signed corner is not convex (the bracket's front is reflex
2148            // at the vertex), so its planes do not bound the solid there.
2149            within: &[],
2150            // The sharp edge is convex: the solid holds the piece within its
2151            // two faces' planes there, and the arcs of the patch at `Q` are
2152            // kept inside it.
2153            seam: &[(c.normal[(ks + 2) % 3], c.corner), (c.normal[ks], c.corner)],
2154            region: Some(&region),
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
2162/// Refuses by name where a signed corner's patches stand outside what the
2163/// solid and its concave rounds hold near the vertex, by more than the
2164/// bands' fit target, proved over every point by [`certify_within`]. A convex
2165/// round only takes material off and a concave one adds no more than the
2166/// fill between its faces and its ball, so a patch past both stands where
2167/// no round of the request puts material.
2168fn 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
2188/// The worst any vertex of `patch`'s mesh, drawn at a quarter of the
2189/// band's fit target, stands outside [`signed_region`], and where: the sampled check the
2190/// certified one replaced, kept to cross-check it.
2191fn 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
2212/// How finely a span of a patch is split before a part of it no bound
2213/// certifies is taken as standing outside.
2214const CERTIFY_DEPTH: u32 = 12;
2215
2216/// Whether every point of `patch` lies within `fit` of [`signed_region`]:
2217/// `Ok` when proved, or a point of the patch that stands farther out and
2218/// by how much (a point the proof could not clear, where none is found).
2219///
2220/// The patch's surface is a B-spline whose weights are all positive, so
2221/// over each span it lies in the convex hull of that span's control
2222/// points, and over any rectangle of its parameters in the hull of the
2223/// control points of its segment there. Each piece of the region bounds
2224/// itself over a hull: a plane's height by its largest at the hull's
2225/// points, a concave round's ball by the hull's distance from its axis;
2226/// and the region's maxima and minima of pieces bound by the same of the
2227/// pieces' bounds. A rectangle whose bound is within `fit` is cleared; one
2228/// wholly outside the face's trim is passed over; the rest is split in
2229/// four, down to [`CERTIFY_DEPTH`] halvings of a span, a point inside the
2230/// trim read at each split.
2231fn 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        // Not a B-spline: read on the mesh as before.
2244        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    // The trim in the surface's parameters, and how far its polylines may
2254    // stand from the trim itself.
2255    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        // Weighted: each rectangle's segment, its hull that of its
2283        // projected control points, the weights all positive.
2284        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    // Polynomial: the patch's Bezier pieces, along u for each column of
2302    // control points and then along v for each row of those.
2303    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    // Each column of pieces along u on its own worker; the first piece
2311    // found standing outside, in the pieces' order, whatever the count.
2312    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
2345/// A Bezier patch's control net, `pv + 1` points along v for each of its
2346/// `pu + 1` rows along u.
2347struct Bezier {
2348    net: Vec<Point>,
2349    pu: usize,
2350    pv: usize,
2351}
2352
2353impl Bezier {
2354    /// The two halves split at the middle of u, then of v: four pieces, in
2355    /// the order of their rectangles' corners (low u low v, high u low v,
2356    /// low u high v, high u high v).
2357    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        // de Casteljau at a half along u, for each column.
2361        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/// [`certify_within`] over one Bezier piece of a polynomial patch, over
2395/// rectangle `(lo, hi)` of its parameters: cleared by its net's hull, or
2396/// split in four by de Casteljau.
2397#[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    // A point of the face here that stands past `fit` settles it.
2418    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/// [`certify_within`] over one rectangle of a weighted patch, split in four
2462/// while its segment's hull does not clear it.
2463#[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
2529/// A face's trim rings, bucketed: their segments on a grid of cells over
2530/// their bounds, for the nearest within a reach, and by bands of height,
2531/// for the crossings a point's ray to `+x` makes.
2532struct 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
2541/// The cells of a [`TrimIndex`] each way.
2542const 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    /// The cell holding `p`, clamped onto the grid.
2592    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    /// Whether a segment comes within `reach` of `p`: those of the cells
2608    /// the square of side `2 reach` about `p` meets, every segment
2609    /// within `reach` lying in one of them.
2610    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    /// Whether `p` lies inside the rings, by the parity of the crossings
2624    /// its ray to `+x` makes: only segments of its band of height can
2625    /// cross it.
2626    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
2645/// The distance from `p` to the segment from `a` to `b`.
2646fn 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
2657/// An upper bound of [`signed_outside`] over the convex hull of `points`:
2658/// each piece of the region bounded over the hull, a plane's height by its
2659/// largest at the points (it is affine), a concave round's ball by the
2660/// hull's least distance from its axis, and combined as the region
2661/// combines the pieces, a maximum's bound the maximum of bounds and a
2662/// minimum's the minimum.
2663fn 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        // The hull seen along the edge: its least distance from the ball's
2698        // axis, nought where the axis passes through it.
2699        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
2713/// The distance from `p` to the convex hull of `points` in the plane:
2714/// nought where `p` lies inside it.
2715fn 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    // Counter-clockwise: inside where `p` is left of every edge.
2747    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
2755/// How far `p` stands outside the solid near the corner's vertex together
2756/// with each concave round's fill there (negative inside): the vertex's
2757/// three face half-spaces, met across a convex edge and joined across a
2758/// concave one, and each concave edge's fill between its two faces, off its
2759/// ball and short of the chord between the ball's contacts, along the edge
2760/// from the vertex.
2761fn signed_outside(c: &SignedCorner, p: Point) -> f64 {
2762    signed_region(c, p).0
2763}
2764
2765/// [`signed_outside`] with the outward unit normal of the boundary piece
2766/// that decides it at `p`: a face's plane, a concave round's ball, the
2767/// chord between the ball's contacts, or the plane square to the edge
2768/// through the vertex.
2769fn 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    // Edge `k` lies between face `k - 1` and face `k`; it is convex where
2775    // its leg into face `k` runs inside face `k - 1`.
2776    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    /// The L-bracket's corner rounded at `radius` on `edges` (of the front
2853    /// edge, the re-entrant edge and the upright's front edge), its signed
2854    /// corner, and the fitted faces the rounding closed it with.
2855    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    /// The certified check proves what the fine mesh it replaced samples:
2900    /// on the bracket's corners, with and without the upright's edge left
2901    /// sharp, both clear every patch at the bands' fit target; asked to
2902    /// keep the patch a tenth of that target inside the region, which the
2903    /// patch's sides on the faces cannot, the mesh finds a point past it
2904    /// and the proof clears nothing the mesh does not.
2905    #[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}