Skip to main content

ogeom_fillet/
sharp.rs

1//! A setback corner with one edge left sharp: the bands of the vertex's
2//! other edges stopped at their setbacks, and the opening between them
3//! closed by a patch that runs into a point on the sharp edge.
4//!
5//! The opening crosses the sharp edge, and where it does, at `Q`, the
6//! patch is to be tangent to both of that edge's faces: two tangent planes
7//! at one point, which no patch with a tangent plane there has. The patch
8//! at `Q` is three-sided, its fourth side shrunk to `Q`, and its normal
9//! turns along that shrunk side from one face's to the other's, as a
10//! cone's turns round its apex: tangent to both faces up to `Q` and,
11//! locally, a cone at `Q` fanning between them.
12//!
13//! The sharp edge is read as a band of no width: its end on its section
14//! plane is the point `Q`, and the trims on its two faces run from `Q`
15//! into the bands' rails, each heading from `Q` for its band's rail half
16//! way back to the vertex, so the two trims at `Q` part and the cone there
17//! opens.
18//!
19//! The patch at `Q` runs out to the middle of each of those trims, where a
20//! curve between them bounds it; it is fitted by
21//! [`ogeom_algo::fit_network`], its halves of the trims held to them and
22//! held across the faces, so it is tangent to both faces exactly up to the
23//! held curves' own projection. The rest of the opening, six sides with
24//! that curve, is filled by one patch G1 to every side, the patch at `Q`
25//! among its supports.
26
27use ogeom_algo::{Across, Built, CurveDef, History, NetworkOptions, PatchDef, PatchSide};
28use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
29use ogeom_geom::{Continuity, Curve, Curve3d as _};
30use ogeom_math::{Point, Vector};
31use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
32
33use crate::fill_n::{FillBoundary, Region, make_filling_n_region, make_filling_n_within};
34use crate::setback::{
35    CornerEnd, Setbacks, bevel_end, corner_edges, filler_inside, point_of, round_end, trim_conic,
36};
37
38/// How many points along the curve between the two patches.
39const ALONG: u32 = 32;
40/// How many arcs, and points along each, seed the patch at `Q`.
41const SEED: u32 = 40;
42/// The lengths of each end tangent of an arc of the patch at `Q`, over the
43/// arc's chord, tried in turn until the rest of the opening meets the
44/// bands' target: fuller than a circle's (1.17 for a right angle), so the
45/// arc leaves each trim nearly straight and the patch's normal turns
46/// little where the rest meets it, and no fuller than keeps the patch's
47/// normal at `Q` turning one way. On the block's corner at ten even and
48/// uneven setbacks and radii, 1.6 met the target in every case, the worst
49/// at 0.75 of it, its normal at `Q` turning one way; 4/3 missed the target
50/// once and 2 folded the normal back twice, by up to 0.02 rad, and tangents
51/// as long as the chord missed by up to 1.7 times.
52const FULL: [f64; 3] = [1.6, 4.0 / 3.0, 2.0];
53/// How far out from `Q` the patch's normal is read as its limit there, and
54/// at how many stations along the shrunk side.
55const AT_Q: f64 = 1e-7;
56const TURNS: u32 = 64;
57
58/// Round the edges of `vertex` other than `sharp` with bands at `radius`,
59/// each stopped at its setback, and close the opening between them with a
60/// patch that runs into the point `sharp`'s setback names on it.
61///
62/// The vertex is one of three planar faces and three straight edges, a
63/// convex corner one ball touches every face of (a box's corner). The two
64/// bands are cut square to their edges at their setbacks; the sharp edge
65/// is cut at its own setback, at `Q`. On the face between the two bands
66/// the trim is the conic tangent to both rails, as
67/// [`round_vertex_setback`](crate::round_vertex_setback) trims; on each face
68/// of the sharp edge the trim is the conic from `Q` tangent to the band's
69/// rail, heading from `Q` for that rail half way back to the vertex.
70///
71/// The opening is closed by two patches. At `Q`, a three-sided one bounded
72/// by the halves of the two trims from `Q` and a curve between their
73/// middles, its fourth side shrunk to `Q`: fitted by
74/// [`ogeom_algo::fit_network`] to arcs swept from `Q` that leave each trim
75/// square across it in its face, its halves of the trims held to them and
76/// held across the two faces, so it is tangent to each face exactly up to
77/// the held curves' own projection, its normal turning at `Q` from one
78/// face's to the other's. Over the rest, one patch from
79/// [`make_filling_n`](crate::make_filling_n) meeting the two bands' end
80/// arcs, the trims and the patch at `Q` at G1, to the bands' fit target.
81/// Where it misses, or the patch at `Q` folds its normal back at `Q`, the
82/// arcs are drawn fuller or flatter and the two patches fitted again.
83///
84/// # Errors
85///
86/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) where
87/// `radius` or a setback is not a positive length, a setback is shorter
88/// than the ball's own along a rounded edge or reaches past an edge's far
89/// end, the sharp edge's setback lies inside the bands' own corner, the
90/// continuity is not G1, `sharp` does not leave the vertex, or the vertex
91/// is not a convex planar corner of three edges one ball touches every
92/// face of; [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) where,
93/// however its arcs are drawn, the rest misses the bands' fit target or
94/// the patch at `Q` folds its normal back.
95pub fn round_vertex_setback_sharp(
96    model: &mut Model,
97    solid: &Shape,
98    vertex: &Shape,
99    sharp: &Shape,
100    radius: f64,
101    setbacks: &Setbacks,
102    tol: Tolerances,
103) -> OgeomResult<Built> {
104    let mut own: Vec<Shape> = vec![vertex.clone(), sharp.clone()];
105    own.extend(setbacks.per_edge.iter().map(|(e, _)| e.clone()));
106    ogeom_algo::on_own_nodes(model, solid, &own, |model, solid, parts| {
107        let named: Vec<(Shape, f64)> = parts[2..]
108            .iter()
109            .zip(&setbacks.per_edge)
110            .map(|(e, (_, s))| (e.clone(), *s))
111            .collect();
112        let own = Setbacks {
113            per_edge: named,
114            default: setbacks.default,
115            continuity: setbacks.continuity,
116        };
117        sharp_own(model, solid, &parts[0], &parts[1], radius, &own, tol)
118    })
119}
120
121/// A curve of the opening, as the patches take it: its curve over a range,
122/// run the loop's way.
123struct Side {
124    curve: Curve,
125    range: (f64, f64),
126}
127
128impl Side {
129    fn at(&self, f: f64, tol: Tolerances) -> OgeomResult<Point> {
130        let (lo, hi) = self.range;
131        self.curve.point_at((hi - lo).mul_add(f, lo), tol)
132    }
133
134    /// The unit tangent a share `f` along, the loop's way.
135    fn tangent(&self, f: f64, tol: Tolerances) -> OgeomResult<Vector> {
136        let (lo, hi) = self.range;
137        let d = self.curve.d1_at((hi - lo).mul_add(f, lo), tol)? * (hi - lo).signum();
138        d.normalized(tol)
139    }
140
141    fn def(&self) -> CurveDef {
142        CurveDef::Curve {
143            curve: self.curve.clone(),
144            range: self.range,
145        }
146    }
147}
148
149/// [`round_vertex_setback_sharp`] on a shape every node below which is its
150/// own.
151#[allow(clippy::too_many_lines, reason = "one corner, assembled end to end")]
152fn sharp_own(
153    model: &mut Model,
154    solid: &Shape,
155    vertex: &Shape,
156    sharp: &Shape,
157    radius: f64,
158    setbacks: &Setbacks,
159    tol: Tolerances,
160) -> OgeomResult<Built> {
161    if !radius.is_finite() || radius <= tol.confusion() {
162        ogeom_bail!(Construction, "a blend radius must be a positive distance");
163    }
164    if setbacks.continuity != Continuity::G1 {
165        ogeom_bail!(
166            Construction,
167            "a setback corner with an edge left sharp meets its bands and faces \
168             at G1, not {:?}",
169            setbacks.continuity
170        );
171    }
172    let frame = crate::corner::corner_frame(model, solid, vertex, radius, tol)?;
173    let n = frame.d.len();
174    let (edges, reach) = corner_edges(model, solid, vertex, &frame, tol)?;
175    let Some(ks) = edges.iter().position(|e| e.is_same(sharp)) else {
176        ogeom_bail!(
177            Construction,
178            "the edge left sharp does not leave the vertex"
179        );
180    };
181    if n != 3 {
182        ogeom_bail!(
183            Construction,
184            "a setback corner with an edge left sharp closes a vertex of three \
185             edges; one of {n} is still owed; see docs/CAPABILITIES.md, \
186             fillet.setback-corners"
187        );
188    }
189    let setback: Vec<f64> = edges
190        .iter()
191        .map(|e| {
192            setbacks
193                .per_edge
194                .iter()
195                .find(|(named, _)| named.is_same(e))
196                .map_or(setbacks.default, |(_, s)| *s)
197        })
198        .collect();
199    for k in 0..n {
200        let s = setback[k];
201        if !s.is_finite() || s <= tol.confusion() {
202            ogeom_bail!(Construction, "a setback of {s} is not a positive distance");
203        }
204        let own = (frame.far - frame.corner).dot(frame.d[k].vector());
205        if k == ks && s <= own + tol.confusion() {
206            ogeom_bail!(
207                Construction,
208                "the edge left sharp is cut at {s}, inside the bands' own \
209                 corner, which reaches {own} along it; cut it farther from \
210                 the vertex"
211            );
212        }
213        if k != ks && s < own - tol.confusion() {
214            ogeom_bail!(
215                Construction,
216                "a setback of {s} stops the band inside the ball's own corner, \
217                 which reaches {own} along its edge"
218            );
219        }
220        if s >= reach[k] - tol.confusion() {
221            ogeom_bail!(
222                Construction,
223                "a setback of {s} reaches past its edge, {} long",
224                reach[k]
225            );
226        }
227    }
228    let rounded: Vec<Shape> = (0..n)
229        .filter(|&k| k != ks)
230        .map(|k| edges[k].clone())
231        .collect();
232    let banded = crate::fillet::fillet_edges(model, solid, &rounded, radius, tol)?;
233    let kinds = vec![CornerEnd::Round; n];
234    close_sharp_corner(
235        model, solid, banded, vertex, &frame, &setback, ks, &kinds, tol,
236    )
237}
238
239/// The bands of `banded` meeting at `vertex`, whose corner reads as
240/// `frame`, each stopped at its `setback`, edge `ks` left sharp and cut at
241/// its own, and the opening closed by the patch at `Q` and one patch over
242/// the rest, as [`round_vertex_setback_sharp`] closes it. Each other edge
243/// ends as `kinds` says: a round's end arc, or a bevel's straight end, which
244/// the rest meets in position only, as it meets the trims from that end:
245/// the bevel meets those faces in a crease. The patch at `Q` is held
246/// across both faces of the sharp edge whatever the bands are, so its
247/// normal turns there from one face's to the other's.
248#[allow(clippy::too_many_lines, reason = "one corner, assembled end to end")]
249#[allow(clippy::too_many_arguments, reason = "one corner and its reading")]
250pub(crate) fn close_sharp_corner(
251    model: &mut Model,
252    solid: &Shape,
253    banded: Built,
254    vertex: &Shape,
255    frame: &crate::corner::CornerFrame,
256    setback: &[f64],
257    ks: usize,
258    kinds: &[CornerEnd],
259    tol: Tolerances,
260) -> OgeomResult<Built> {
261    let n = frame.d.len();
262    // The cell the patches replace: in from every plane through the vertex
263    // and short of each edge's section, the sharp edge's included.
264    let mut walls: Vec<(Vector, Point)> = frame.m.iter().map(|mk| (*mk, frame.corner)).collect();
265    // The vertex's planes by their outward normals: the corner is convex,
266    // so what stands past one of them lies outside the solid.
267    let within: Vec<(Vector, Point)> = frame.m.iter().map(|mk| (-*mk, frame.corner)).collect();
268    let sections: Vec<(Vector, Point)> = (0..n)
269        .map(|k| {
270            let dk = frame.d[k].vector();
271            (-dk, frame.corner + dk * setback[k])
272        })
273        .collect();
274    walls.extend(sections.iter().copied());
275    let cell = crate::corner::convex_block(model, &walls, tol)?;
276    let cut = ogeom_bool::cut(model, &banded.shape, &cell, tol)?;
277    let planes: Vec<(Vector, Point)> = frame.m.iter().map(|mk| (*mk, frame.corner)).collect();
278    crate::setback::guard_cell(
279        model,
280        solid,
281        &banded.shape,
282        &cut.shape,
283        vertex,
284        &planes,
285        &walls,
286        tol,
287    )?;
288    let history = banded.history.then(&cut.history);
289    let near = tol.confusion() * 100.0;
290    let on_plane = |p: Point, (normal, at): (Vector, Point)| normal.dot(p - at).abs() <= near;
291    let mut kept: Vec<Shape> = Vec::new();
292    for face in explore_unique(model, &cut.shape, ShapeType::Face)? {
293        let data = model.node(&face).and_then(|n| n.data().as_face());
294        let planar = data
295            .and_then(|d| model.geometry().surface(d.surface))
296            .is_some_and(|s| matches!(s, ogeom_geom::SurfaceGeometry::Plane(_)));
297        let on_section = planar && {
298            let (origin, outward) = ogeom_algo::face_normal(model, &face, tol)?;
299            sections.iter().any(|section| {
300                outward.cross(section.0).magnitude() <= tol.angular() * 10.0
301                    && on_plane(origin, *section)
302            })
303        };
304        if !on_section {
305            kept.push(face);
306        }
307    }
308
309    // `Q`, where the cut leaves the sharp edge, and each band's end arc.
310    let q_point = sections[ks].1;
311    let Some(vq) = explore_unique(model, &cut.shape, ShapeType::Vertex)?
312        .into_iter()
313        .find(|v| point_of(model, v).is_ok_and(|p| p.distance(q_point) <= near))
314    else {
315        ogeom_bail!(
316            Construction,
317            "the cut does not leave the sharp edge at its setback"
318        );
319    };
320    let mut arcs: Vec<Option<(Shape, Shape)>> = Vec::with_capacity(n);
321    for (k, section) in sections.iter().enumerate() {
322        arcs.push(if k == ks {
323            None
324        } else if kinds[k] == CornerEnd::Bevel {
325            Some(bevel_end(model, &kept, frame, *section, &walls, tol)?)
326        } else {
327            Some(round_end(model, &kept, k, *section, &walls, tol)?)
328        });
329    }
330    let host = |t: usize| -> (Vector, Point) { (frame.m[frame.ring[t]], frame.corner) };
331    let end_on = |model: &Model, k: usize, plane: (Vector, Point)| -> OgeomResult<(Shape, Point)> {
332        let Some((arc, _)) = &arcs[k] else {
333            return Ok((vq.clone(), q_point));
334        };
335        let Some((a, b)) = ogeom_algo::edge_vertices(model, arc)? else {
336            ogeom_bail!(Construction, "a band's end arc has no vertices");
337        };
338        for v in [a, b] {
339            let p = point_of(model, &v)?;
340            if on_plane(p, plane) {
341                return Ok((v, p));
342            }
343        }
344        ogeom_bail!(Construction, "a band's end arc does not reach its face");
345    };
346
347    // The trim on the face after each edge, from its end there to the next
348    // edge's: tangent to a band's rail at its end, and from `Q` heading for
349    // the band's rail half way back to the vertex. A trim from `Q` is split
350    // at its middle, where the patch at `Q` ends.
351    let mut trims: Vec<Trim> = Vec::with_capacity(n);
352    let mut fillers: Vec<Shape> = Vec::with_capacity(n);
353    for k in 0..n {
354        let next = (k + 1) % n;
355        let plane = host(next);
356        let (v0, p0) = end_on(model, k, plane)?;
357        let (v1, p1) = end_on(model, next, plane)?;
358        let Some(q) = crate::corner::planes_meet(&plane, &sections[k], &sections[next]) else {
359            ogeom_bail!(
360                Construction,
361                "two section planes do not meet on the face between them"
362            );
363        };
364        let into = |from: (usize, Point), to: (usize, Point)| -> OgeomResult<Vector> {
365            if from.0 == ks {
366                let aim = to.1 - frame.d[to.0].vector() * (setback[to.0] / 2.0);
367                Ok((aim - from.1).normalized(tol)?)
368            } else {
369                Ok(-frame.d[from.0].vector())
370            }
371        };
372        let (conic, apex) = trim_conic(
373            (p0, into((k, p0), (next, p1))?),
374            (p1, into((next, p1), (k, p0))?),
375            tol,
376        )?;
377        let edges = trim_edges(model, &conic, (&v0, &v1), k == ks || next == ks, tol)?;
378        let filler = filler_inside(
379            model,
380            edges.clone(),
381            apex,
382            (&v0, p0),
383            (&v1, p1),
384            q,
385            plane,
386            tol,
387        )?;
388        trims.push(Trim {
389            curve: conic,
390            edges,
391            normal: plane.0,
392            filler: filler.clone(),
393        });
394        fillers.push(filler);
395    }
396    let (r1, r2) = ((ks + 1) % n, (ks + 2) % n);
397
398    let (Some(arc1), Some(arc2)) = (&arcs[r1], &arcs[r2]) else {
399        ogeom_bail!(Construction, "a rounded edge has no end arc");
400    };
401    // G1 to a round's end and to a trim between rounds; a bevel's end, and
402    // a trim from it, in position only.
403    let round = |k: usize| kinds[k] == CornerEnd::Round;
404    let (at_q, patch) = close_through_q(
405        model,
406        [&trims[ks], &trims[r1], &trims[r2]],
407        [arc1, arc2],
408        [
409            round(r1),
410            round(r1),
411            round(r1) && round(r2),
412            round(r2),
413            round(r2),
414        ],
415        &Held {
416            within: &within,
417            seam: &[],
418            region: None,
419        },
420        tol,
421    )?;
422    let mut faces = kept;
423    faces.extend(fillers);
424    sew_corner(model, faces, (at_q, patch), vertex, history, tol)
425}
426
427/// Close an opening that runs into `Q`, a point on a sharp edge: `out` the
428/// trim from `Q` to the first band's end, `middle` the trim from that band
429/// to the second, `back` the trim from the second band's end to `Q`, each
430/// built by [`trim_edges`], those from `Q` split at their middles; `arcs`
431/// the two bands' end arcs and their faces. The patch at `Q`, three-sided,
432/// its fourth side shrunk to `Q`, tangent to both faces of the sharp edge
433/// exactly up to the held trims' projection, and the patch over the rest,
434/// meeting the second half of `out`, the first arc, `middle`, the second
435/// arc and the first half of `back` at G1 where `rest` says and in
436/// position elsewhere, and the patch at `Q` at G1. Neither patch may stand
437/// outside what `held` holds them in.
438pub(crate) fn close_through_q(
439    model: &mut Model,
440    [out, middle, back]: [&Trim; 3],
441    [arc1, arc2]: [&(Shape, Shape); 2],
442    rest: [bool; 5],
443    held: &Held<'_>,
444    tol: Tolerances,
445) -> OgeomResult<(Shape, Shape)> {
446    let Held {
447        within,
448        seam,
449        region,
450    } = *held;
451    // The patch at `Q`: the first half of the trim from `Q`, a curve from
452    // its middle to the middle of the trim back to `Q`, and that trim's
453    // second half, its fourth side shrunk to `Q`.
454    let out_half = Side {
455        curve: out.curve.clone(),
456        range: (0.0, 0.5),
457    };
458    let back_half = Side {
459        curve: back.curve.clone(),
460        range: (0.5, 1.0),
461    };
462    let across = |normal: Vector, along: Vector, towards: Vector| -> OgeomResult<Vector> {
463        let d = normal.cross(along).normalized(tol)?;
464        Ok(if d.dot(towards) >= 0.0 { d } else { -d })
465    };
466    let fit = crate::marched::band_fit_target(tol);
467    // The patch at `Q` seeded on arcs of fullness `full`, and the rest of
468    // the opening filled against it.
469    let attempt = |model: &mut Model, full: f64| -> OgeomResult<(Shape, Shape)> {
470        // Its arc a share `d` of the way out from `Q` along both halves:
471        // the cubic from one to the other leaving each square across it in
472        // its face, so the patch seeded on them is tangent to both faces.
473        // The last is the curve between the patch and the rest.
474        let arc = |d: f64, w: f64| -> OgeomResult<Point> {
475            let (from, to) = (out_half.at(d, tol)?, back_half.at(1.0 - d, tol)?);
476            let reach0 = from.distance(to) * full;
477            let leave = across(out.normal, out_half.tangent(d, tol)?, to - from)?;
478            let leave = turned_within(leave, out.normal, from, reach0, seam, tol)?;
479            let arrive = -across(back.normal, back_half.tangent(1.0 - d, tol)?, from - to)?;
480            let arrive = -turned_within(-arrive, back.normal, to, reach0, seam, tol)?;
481            // Each arc lies within the hull of its Bezier points, the ends
482            // and a third of the way along each tangent: held inside the
483            // planes `seam` asks (the convex piece about the sharp edge,
484            // which the solid holds), the arc stays inside them.
485            let mut reach = reach0;
486            for (normal, on) in seam {
487                let lift = normal.dot(leave);
488                if lift > 0.0 {
489                    reach = reach.min(3.0 * (-normal.dot(from - *on)).max(0.0) / lift);
490                }
491                let lift = -normal.dot(arrive);
492                if lift > 0.0 {
493                    reach = reach.min(3.0 * (-normal.dot(to - *on)).max(0.0) / lift);
494                }
495            }
496            let (w2, w3) = (w * w, w * w * w);
497            let (h00, h10) = (2.0 * w3 - 3.0 * w2 + 1.0, w3 - 2.0 * w2 + w);
498            let (h01, h11) = (-2.0 * w3 + 3.0 * w2, w3 - w2);
499            Ok(Point::ORIGIN
500                + (from - Point::ORIGIN) * h00
501                + leave * (reach * h10)
502                + (to - Point::ORIGIN) * h01
503                + arrive * (reach * h11))
504        };
505        let between: Vec<Point> = (0..=ALONG)
506            .map(|i| arc(1.0, f64::from(i) / f64::from(ALONG)))
507            .collect::<OgeomResult<_>>()?;
508        let mut cloud: Vec<Point> = Vec::new();
509        for i in 1..=SEED {
510            let d = f64::from(i) / f64::from(SEED);
511            for j in 0..=SEED {
512                cloud.push(arc(d, f64::from(j) / f64::from(SEED))?);
513            }
514        }
515        let index = ogeom_mesh::PointIndex::build(&cloud)?;
516        let curves = vec![out_half.def(), CurveDef::Polyline(between), back_half.def()];
517        let side = |curve: usize| {
518            Some(PatchSide {
519                curve,
520                reversed: false,
521            })
522        };
523        let patches = vec![PatchDef {
524            sides: [side(0), side(1), side(2), None],
525        }];
526        // The halves are held to the trims and held across the faces, which
527        // makes the patch tangent to both up to `Q`; the points only shape
528        // it inside, so how near it comes to them is not asked.
529        let options = NetworkOptions {
530            tolerance: fit,
531            smoothing: fit * fit,
532            creases: Vec::new(),
533            spans: 2,
534            most: 16,
535            held: vec![0, 2],
536            across: vec![
537                (0, Across::Plane(out.normal)),
538                (2, Across::Plane(back.normal)),
539            ],
540        };
541        let network = ogeom_algo::fit_network(model, &patches, &curves, &index, &options, tol)?;
542        // Its normal at `Q`, the limit along the shrunk side from one face's
543        // to the other's, turns one way: the patch is a cone there that
544        // does not fold back on itself.
545        let surface = &network.surfaces[0];
546        let limit = |w: f64| -> OgeomResult<Vector> {
547            let (du, dw) = ogeom_geom::Surface::d1_at(surface, AT_Q, w, tol)?;
548            // `dw` shrinks with the distance from `Q`; scaled back, the
549            // cross product is the limit normal's own size.
550            du.cross(dw * (1.0 / AT_Q)).normalized(tol)
551        };
552        let first = limit(0.0)?;
553        let mut turned = 0.0_f64;
554        for i in 1..=TURNS {
555            let now = limit(f64::from(i) / f64::from(TURNS))?
556                .dot(first)
557                .clamp(-1.0, 1.0)
558                .acos();
559            if now < turned - fit {
560                ogeom_bail!(
561                    NotDone,
562                    "the patch at the sharp edge turns its normal back by {} there",
563                    turned - now
564                );
565            }
566            turned = turned.max(now);
567        }
568        let at_q = network.faces[0].clone();
569        // Neither patch may stand past a plane of the vertex: the corner is
570        // convex, so what stands past one lies outside the solid.
571        let past = past_planes(model, &at_q, within, tol)?;
572        if past > fit {
573            ogeom_bail!(
574                NotDone,
575                "the patch at the sharp edge stands {past} past a face of the vertex"
576            );
577        }
578        // The rest of the opening: the trim from `Q`'s second half, band
579        // `r1`'s arc, the trim between the bands, band `r2`'s arc, the trim
580        // back to `Q`'s first half, and the curve along the patch at `Q`,
581        // filled by one patch G1 to every side.
582        let side = |edge: &Shape, support: &Shape, tangent: bool| FillBoundary {
583            edge: edge.clone(),
584            support: Some(support.clone()),
585            continuity: if tangent {
586                Continuity::G1
587            } else {
588                Continuity::C0
589            },
590        };
591        let boundary = vec![
592            side(&out.edges[1], &out.filler, rest[0]),
593            side(&arc1.0, &arc1.1, rest[1]),
594            side(&middle.edges[0], &middle.filler, rest[2]),
595            side(&arc2.0, &arc2.1, rest[3]),
596            side(&back.edges[0], &back.filler, rest[4]),
597            side(&network.edges[1], &at_q, true),
598        ];
599        // Held inside the region the caller names where the vertex is not
600        // convex, and inside the vertex's planes where it is.
601        let filled = match region {
602            Some(region) => make_filling_n_region(model, &boundary, &[], fit, region, tol)?,
603            None => make_filling_n_within(model, &boundary, &[], fit, within, tol)?,
604        };
605        Ok((at_q, filled.built.shape))
606    };
607    let mut made = None;
608    for (round, full) in FULL.iter().enumerate() {
609        match attempt(model, *full) {
610            Ok(both) => {
611                made = Some(both);
612                break;
613            }
614            Err(ogeom_core::OgeomError::NotDone(_)) if round + 1 < FULL.len() => {}
615            Err(e) => return Err(e),
616        }
617    }
618    let Some(both) = made else {
619        ogeom_bail!(NotDone, "no fullness was tried");
620    };
621    Ok(both)
622}
623
624/// Sew `faces`, what the corner keeps and its fillers, with the patch at a
625/// sharp edge's point and the patch over the rest into one solid, each
626/// patch recorded as generated by `vertex`.
627pub(crate) fn sew_corner(
628    model: &mut Model,
629    mut faces: Vec<Shape>,
630    (at_q, patch): (Shape, Shape),
631    vertex: &Shape,
632    history: History,
633    tol: Tolerances,
634) -> OgeomResult<Built> {
635    let fit = crate::marched::band_fit_target(tol);
636    faces.push(at_q.clone());
637    faces.push(patch.clone());
638    let sewn = ogeom_algo::sew_within(model, &faces, fit, tol)?;
639    if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
640        ogeom_bail!(
641            Construction,
642            "the setback corner's faces did not close: {} shells",
643            sewn.shells.len()
644        );
645    }
646    let rounded = model.add_solid(std::slice::from_ref(&sewn.shells[0]))?;
647    // The two patches as the solid holds them: sewing copies a face it
648    // shares with another shape, the patch at `Q` its network's shell.
649    let surface_of = |model: &Model, face: &Shape| {
650        model
651            .node(face)
652            .and_then(|n| n.data().as_face())
653            .map(|d| d.surface)
654    };
655    let mut history: History = history;
656    for made in [&at_q, &patch] {
657        let wanted = surface_of(model, made);
658        let Some(face) = explore_unique(model, &rounded, ShapeType::Face)?
659            .into_iter()
660            .find(|f| wanted.is_some() && surface_of(model, f) == wanted)
661        else {
662            ogeom_bail!(
663                Construction,
664                "a patch of the corner is not in the sewn solid"
665            );
666        };
667        history.generate(vertex, face);
668    }
669    Ok(Built::new(rounded, history))
670}
671
672/// A trim's edges on `conic` from `v0` to `v1`: one, or two split at its
673/// middle where the patch at a sharp edge's point ends.
674pub(crate) fn trim_edges(
675    model: &mut Model,
676    conic: &Curve,
677    (v0, v1): (&Shape, &Shape),
678    split: bool,
679    tol: Tolerances,
680) -> OgeomResult<Vec<Shape>> {
681    let pieces: &[(f64, f64)] = if split {
682        &[(0.0, 0.5), (0.5, 1.0)]
683    } else {
684        &[(0.0, 1.0)]
685    };
686    let mut ends = vec![v0.clone()];
687    if split {
688        ends.push(ogeom_algo::make_vertex(model, conic.point_at(0.5, tol)?).shape);
689    }
690    ends.push(v1.clone());
691    let mut edges = Vec::with_capacity(pieces.len());
692    for (piece, range) in pieces.iter().enumerate() {
693        edges.push(
694            ogeom_algo::make_edge_between(
695                model,
696                conic.clone(),
697                *range,
698                &ends[piece],
699                &ends[piece + 1],
700                tol,
701            )?
702            .shape,
703        );
704    }
705    Ok(edges)
706}
707
708/// A face's trim: its conic, run from the end on the edge before it to the
709/// end on the edge after, the edges it is built as, the face's normal and
710/// its filler inside the cell.
711/// What [`close_through_q`] holds its patches in.
712#[derive(Clone, Copy)]
713pub(crate) struct Held<'a> {
714    /// Half-spaces, each an outward unit normal and a point on its plane,
715    /// neither patch may stand past: a convex vertex's planes, past which
716    /// lies nothing of the solid; none where the vertex is not convex.
717    pub within: &'a [(Vector, Point)],
718    /// Half-spaces the arcs of the patch at `Q` are kept inside: the convex
719    /// piece about the sharp edge, where the vertex is not convex.
720    pub seam: &'a [(Vector, Point)],
721    /// The region the patch over the rest is held inside, where the vertex
722    /// is not convex; the half-spaces `within` hold it otherwise.
723    pub region: Option<Region<'a>>,
724}
725
726pub(crate) struct Trim {
727    pub curve: Curve,
728    pub edges: Vec<Shape>,
729    pub normal: Vector,
730    pub filler: Shape,
731}
732
733/// How far `face` stands past the planes `within` (each an outward unit
734/// normal and a point), read at the vertices of its mesh drawn at a quarter
735/// of the bands' fit target.
736fn past_planes(
737    model: &Model,
738    face: &Shape,
739    within: &[(Vector, Point)],
740    tol: Tolerances,
741) -> OgeomResult<f64> {
742    // No plane to stand past (a vertex that is not convex): nothing to mesh.
743    if within.is_empty() {
744        return Ok(f64::NEG_INFINITY);
745    }
746    let deflection = ogeom_mesh::Deflection {
747        chord: crate::marched::band_fit_target(tol) * 0.25,
748        ..ogeom_mesh::Deflection::default()
749    };
750    let mesh = ogeom_mesh::triangulate_face(model, face, deflection, tol)?;
751    Ok(mesh
752        .positions
753        .iter()
754        .flat_map(|p| within.iter().map(move |(n, at)| n.dot(*p - *at)))
755        .fold(f64::NEG_INFINITY, f64::max))
756}
757
758/// `dir`, a unit direction in the plane with unit normal `face` leaving
759/// `from`, turned within that plane as little as keeps the point a third of
760/// `reach` along it inside every plane of `seam` (an outward unit normal
761/// and a point): its Bezier point stays in the convex piece they bound.
762/// Where a quarter turn does not reach, `dir` is returned and the reach is
763/// held instead.
764fn turned_within(
765    dir: Vector,
766    face: Vector,
767    from: Point,
768    reach: f64,
769    seam: &[(Vector, Point)],
770    tol: Tolerances,
771) -> OgeomResult<Vector> {
772    let mut d = dir;
773    if reach <= tol.confusion() {
774        return Ok(d);
775    }
776    for (normal, on) in seam {
777        let room = 3.0 * (-normal.dot(from - *on)).max(0.0) / reach;
778        let a = d.dot(*normal);
779        if a <= room {
780            continue;
781        }
782        let side = face.cross(d).normalized(tol)?;
783        let side = if side.dot(*normal) < 0.0 { side } else { -side };
784        let b = side.dot(*normal);
785        let r = a.hypot(b);
786        let turn = b.atan2(a) + (room / r).min(1.0).acos();
787        if !(0.0..=core::f64::consts::FRAC_PI_2).contains(&turn) {
788            return Ok(dir);
789        }
790        d = d * turn.cos() + side * turn.sin();
791    }
792    Ok(d)
793}