Skip to main content

geop_ops_surface/
boundary.rs

1//! [`BoundarySurface`]: a face standing on its own, spanned by edges — the
2//! ruled face between two of them, or a closed loop of them filled.
3//!
4//! **Two edges** that do not close a loop are joined by the ruled surface
5//! between them (see [`NurbSurface3D::ruled`]), the second turned, if need
6//! be, so that their ends line up, and a straight edge at either end.
7//!
8//! **A closed loop** — the edges picked in any order, each running either
9//! way, end to end — is filled:
10//!
11//! - **flat**, where every edge lies in one plane: one face on that plane,
12//!   trimmed by the loop, exactly as an extrude's cap is. Any number of
13//!   edges, a single circle included.
14//! - **four edges**: their Coons patch (see [`NurbSurface3D::coons`]), which
15//!   runs exactly along every one of them. Along edges picked as `tangent`
16//!   it is made tangent to the face each of those edges bounds, continuing
17//!   it smoothly (see [`NurbSurface3D::tangent_to`]): exactly to a flat
18//!   face, and to a curved one leaving it at a millionth of a radian
19//!   outwards, proven so all along, so that it never dips through it.
20//! - **three, or five and more**: a flat inner polygon and a quadrilateral
21//!   along every edge, between the edge, the spokes from its ends to the
22//!   inner polygon and the polygon's side (see [`NurbSurface3D::fill`]).
23//!   The fill runs exactly along the loop, every edge whole — it knits to
24//!   the faces around the hole — and is smooth inside: the quadrilaterals
25//!   meet each other across the spokes, and the polygon, tangentially.
26//!   Along edges picked as `tangent` it is tangent to their faces, as a
27//!   patch of four is.
28//!
29//! A Coons patch needs a corner wherever two of its sides meet: a loop of
30//! four that runs on smoothly through a vertex leaves a patch with no
31//! normal there, and is refused, naming the vertex. Two
32//! edges sharing an end are ruled into a face that comes to a point there,
33//! as a cone into its apex: that side of the patch collapses to the
34//! shared vertex.
35
36use geop_core_editor::{
37    edit_form::{EditForm, FormState},
38    editable::{Context, OperationGroup, OperationInfo, OperationTier},
39    part_operation,
40    part_operation::PartOperation,
41};
42use geop_core_geometry::{
43    nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D, flatten_point, loop_signed_area},
44    nurb_surface::{FlatPatch, Neighbour, NurbSurface3D},
45    shape::Plane,
46};
47use geop_core_math::{
48    geop_error::{GeopError, GeopResult},
49    scalars::Scalar,
50    vector::{Vector3, Vector4},
51};
52use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
53use geop_core_topology::{
54    Curve3, Sense,
55    build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
56};
57use geop_ops_model::sketch3d::CurveSource;
58use geop_ops_model::{
59    BodyNames, Chain, Edge, ModelOperation, NamedModel, Namer, build_model, read_model,
60};
61use serde::{Deserialize, Serialize};
62
63use crate::{NamedCurve, SheetSpec};
64
65/// Spans a face standing on its own between the curves `edges` — two of
66/// them ruled, a closed loop of them filled, see the module docs — for the
67/// operation `B`. A curve is an edge, of a face or of a wire — a 3-D
68/// sketch's line, arc or spline — or a planar sketch's curve. The curves
69/// are copied, the originals left as they are; with `tangent`, the fill
70/// continues the faces those of its edges bound smoothly.
71///
72/// The face is named `boundary(B)`. The copy of each curve or end `X`
73/// picked is `boundary(B,X)` — for a sketch's curve `X` is `K,c3`, and
74/// its ends `K,p1` (see [`NamedCurve`]); a straight edge a ruled face adds between
75/// vertices `V` and `W` is `boundary(B,V,W)`. A patch of quadrilaterals
76/// names the one along edge `E` `boundary(B,E,patch)`, the inner polygon
77/// `boundary(B,inner)`, its side alongside `E` `boundary(B,E,inner)`, its
78/// corner off vertex `V` `boundary(B,V,inner)` and the spoke between them
79/// `boundary(B,V,spoke)`.
80#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
81pub struct BoundarySurface;
82
83#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
84pub struct BoundarySurfaceArgs {
85    /// The curves to span: two, or a closed loop.
86    pub edges: Vec<CurveSource>,
87    /// Those of them the fill is to be tangent along, to the face each
88    /// bounds.
89    #[serde(default, skip_serializing_if = "Vec::is_empty")]
90    pub tangent: Vec<ElementReference<NamedModel, Edge>>,
91}
92
93part_operation!(BoundarySurface(BoundarySurfaceArgs));
94
95impl PartOperation for BoundarySurface {
96    const KIND: &'static str = "boundary_surface";
97    const INFO: OperationInfo = OperationInfo::new(
98        "Boundary surface",
99        "Span a face standing on its own between two edges, or fill a closed loop of edges — optionally tangent to the faces along them.",
100        OperationGroup::Surface,
101        OperationTier::Small,
102    )
103    .experimental()
104    .icon(crate::icons::BOUNDARY_SURFACE);
105    type Args = BoundarySurfaceArgs;
106    type Session = ();
107
108    /// Nothing picked yet.
109    fn new_args(_: &Context) -> BoundarySurfaceArgs {
110        BoundarySurfaceArgs::default()
111    }
112
113    fn run(ctx: &mut StepContext, args: &BoundarySurfaceArgs) -> GeopResult<()> {
114        let _step = ctx.step();
115        build_model::<Self>(ctx, args)
116    }
117
118    /// The edges, picked, and those to be tangent along.
119    fn form<'a>(
120        context: &'a Context<'a>,
121        args: &BoundarySurfaceArgs,
122        _: &FormState<()>,
123    ) -> EditForm<'a, BoundarySurfaceArgs> {
124        let before = &context.before;
125        let mut f = EditForm::<BoundarySurfaceArgs>::default();
126        f.pick(
127            "edges",
128            "edges",
129            before,
130            &args.edges,
131            true,
132            |edit, picked| edit.args.edges = picked,
133        );
134        f.pick(
135            "tangent",
136            "tangent along",
137            before,
138            &args.tangent,
139            true,
140            |edit, picked| edit.args.tangent = picked,
141        );
142        f.form.dialog.optional("tangent");
143        f
144    }
145}
146
147impl ModelOperation for BoundarySurface {
148    fn build(
149        model: &mut NamedModel,
150        reader: &Reader,
151        step: &str,
152        args: &BoundarySurfaceArgs,
153    ) -> GeopResult<()> {
154        let namer = Namer::new("boundary", step)?;
155        let edges = (args.edges.iter())
156            .map(|e| e.resolve(reader))
157            .collect::<GeopResult<Vec<_>>>()?;
158        let tangent: Vec<&str> = args.tangent.iter().map(|t| t.name()).collect();
159        let (spec, names) = boundary_surface(&*read_model(reader)?, &namer, &edges, &tangent)?;
160        model.build_body(spec, names)?;
161        Ok(())
162    }
163}
164
165/// A curve picked, as it runs along the boundary: its curve and its ends'
166/// points and names in that direction — `forward` if that is the curve's
167/// own.
168#[derive(Clone, Debug)]
169struct Side {
170    /// The name of the curve picked, as `tangent` names it.
171    entity: String,
172    name: String,
173    /// The edge's curve, as the edge runs.
174    curve: Curve3,
175    forward: bool,
176    start: Vector3,
177    end: Vector3,
178    start_name: String,
179    end_name: String,
180}
181
182impl Side {
183    fn of(chain: &Chain<NurbCurve3D>) -> GeopResult<Self> {
184        let curve = NamedCurve::of(chain)?;
185        Ok(Self {
186            entity: curve.name.clone(),
187            name: curve.name,
188            curve: curve.curve,
189            forward: true,
190            start: curve.start.1,
191            end: curve.end.1,
192            start_name: curve.start.0,
193            end_name: curve.end.0,
194        })
195    }
196
197    fn turned(&self) -> Self {
198        Self {
199            forward: !self.forward,
200            start: self.end,
201            end: self.start,
202            start_name: self.end_name.clone(),
203            end_name: self.start_name.clone(),
204            ..self.clone()
205        }
206    }
207
208    /// The curve, run the way the side runs.
209    fn oriented(&self) -> Curve3 {
210        self.sense().orient(&self.curve)
211    }
212
213    /// How a coedge running the way the side runs uses its edge's copy.
214    fn sense(&self) -> Sense {
215        Sense::from_forward(self.forward)
216    }
217}
218
219/// A plain number to compare by: a free choice between candidates.
220fn distance(a: &Vector3, b: &Vector3) -> f64 {
221    a.sub(b).norm().to_f64()
222}
223
224/// The sheet spanned between `edges`, tangent along `tangent` (see the
225/// module docs), each a curve picked, the edges of `tangent` by name, in
226/// `part`: what to build, named after `namer`, as [`BoundarySurface`]
227/// says.
228pub fn boundary_surface(
229    part: &NamedModel,
230    namer: &Namer,
231    edges: &[Chain<NurbCurve3D>],
232    tangent: &[&str],
233) -> GeopResult<SheetSpec> {
234    let sides = edges.iter().map(Side::of).collect::<GeopResult<Vec<_>>>()?;
235    if let Some(t) = (tangent.iter()).find(|t| !sides.iter().any(|s| s.entity == **t)) {
236        return Err(GeopError::new(format!(
237            "edge {t} is to be tangent along, but is not one of the curves spanned"
238        )));
239    }
240    let (spec, names) = match sides.as_slice() {
241        [] => return Err(GeopError::new("no edges to span a surface between")),
242        [a, b] if !closes(a, b) => {
243            if !tangent.is_empty() {
244                return Err(GeopError::new(
245                    "a ruled face between two edges cannot be made tangent: tangency is supported along a loop of four edges",
246                ));
247            }
248            ruled(namer, a, b)?
249        }
250        _ => {
251            let sides = chain(&sides)?;
252            let faces = tangent
253                .iter()
254                .map(|&t| Ok((t.to_string(), Tangency::along(part, t)?)))
255                .collect::<GeopResult<Vec<_>>>()?;
256            fill(namer, &sides, &faces)?
257        }
258    };
259    Ok((spec, names))
260}
261
262/// Whether the two edges close a loop between them: each end of one at an
263/// end of the other.
264fn closes(a: &Side, b: &Side) -> bool {
265    let meet = |p: &Vector3, q: &Vector3| p.could_be_equal(q);
266    (meet(&a.end, &b.start) && meet(&b.end, &a.start))
267        || (meet(&a.end, &b.end) && meet(&b.start, &a.start))
268}
269
270/// The ruled face between `a` and `b`, `b` turned if its ends line up
271/// better with `a`'s that way round. Where they share an end, the face
272/// comes to a point there: that side is the shared vertex.
273fn ruled(namer: &Namer, a: &Side, b: &Side) -> GeopResult<(BodySpec, BodyNames)> {
274    let along = distance(&a.start, &b.start) + distance(&a.end, &b.end);
275    let across = distance(&a.start, &b.end) + distance(&a.end, &b.start);
276    let b = if across < along {
277        b.turned()
278    } else {
279        b.clone()
280    };
281    let (apex_start, apex_end) = (
282        a.start.could_be_equal(&b.start),
283        a.end.could_be_equal(&b.end),
284    );
285    if apex_start && apex_end {
286        return Err(GeopError::new(format!(
287            "edges {} and {} share both ends: they close a loop, not two sides of a ruled face",
288            a.name, b.name
289        )));
290    }
291    // Vertices 0 -> 1 along `a`, 2 -> 3 along `b`: `b`'s end the same as
292    // `a`'s where they share it.
293    let mut vertices = vec![a.start, a.end];
294    let mut vertex_names = vec![namer.name(&[&a.start_name]), namer.name(&[&a.end_name])];
295    let mut vertex = |shared: bool, of_a: usize, point: Vector3, name: &str| {
296        if shared {
297            return of_a;
298        }
299        vertices.push(point);
300        vertex_names.push(namer.name(&[name]));
301        vertices.len() - 1
302    };
303    let b_start = vertex(apex_start, 0, b.start, &b.start_name);
304    let b_end = vertex(apex_end, 1, b.end, &b.end_name);
305    let copy = |side: &Side, start: usize, end: usize| EdgeSpec {
306        curve: side.curve.clone(),
307        start: if side.forward { start } else { end },
308        end: if side.forward { end } else { start },
309    };
310    let mut edges = vec![copy(a, 0, 1), copy(&b, b_start, b_end)];
311    let mut edge_names = vec![namer.name(&[&a.name]), namer.name(&[&b.name])];
312    // A side between the two edges: a line, or the shared vertex.
313    let mut side = |shared: bool, from: (usize, Vector3, &str), to: (usize, Vector3, &str)| {
314        if shared {
315            return Ok(CoedgeOn::Vertex(from.0));
316        }
317        edges.push(EdgeSpec {
318            curve: NurbCurve3D::line(from.1, to.1)?,
319            start: from.0,
320            end: to.0,
321        });
322        edge_names.push(namer.name(&[from.2, to.2]));
323        Ok::<_, GeopError>(CoedgeOn::Edge(edges.len() - 1, Sense::Forward))
324    };
325    let end_side = side(
326        apex_end,
327        (1, a.end, &a.end_name),
328        (b_end, b.end, &b.end_name),
329    )?;
330    let start_side = side(
331        apex_start,
332        (b_start, b.start, &b.start_name),
333        (0, a.start, &a.start_name),
334    )?;
335    let surface = NurbSurface3D::ruled(&a.oriented(), &b.oriented())?;
336    let face = FaceSpec::on_unit_square(
337        surface,
338        [
339            CoedgeOn::Edge(0, a.sense()),
340            end_side,
341            CoedgeOn::Edge(1, b.sense().opposite()),
342            start_side,
343        ],
344    )?;
345    Ok((
346        BodySpec {
347            vertices,
348            edges,
349            faces: vec![face],
350            shells: vec![vec![0]],
351            solid: false,
352        },
353        BodyNames {
354            vertices: vertex_names,
355            edges: edge_names,
356            faces: vec![namer.root()],
357            solid: None,
358        },
359    ))
360}
361
362/// `sides` put end to end into a closed loop, starting with the first as
363/// it runs: each turned to run on from where the one before ends. An error
364/// naming the edge where the loop breaks off or forks.
365fn chain(sides: &[Side]) -> GeopResult<Vec<Side>> {
366    let mut left: Vec<Side> = sides[1..].to_vec();
367    let mut out = vec![sides[0].clone()];
368    // Each round takes one side out of `left`, or refuses.
369    for _ in 1..sides.len() {
370        let end = out.last().expect("one side at least").end;
371        let mut next: Vec<(usize, bool)> = Vec::new();
372        for (i, s) in left.iter().enumerate() {
373            if s.start.could_be_equal(&end) {
374                next.push((i, false));
375            }
376            if s.end.could_be_equal(&end) {
377                next.push((i, true));
378            }
379        }
380        let last = &out.last().expect("one side at least").name;
381        match next.as_slice() {
382            [] => {
383                return Err(GeopError::new(format!(
384                    "the edges are not a closed loop: none of them goes on from where edge {last} ends"
385                )));
386            }
387            [(i, turn)] => {
388                let s = left.remove(*i);
389                out.push(if *turn { s.turned() } else { s });
390            }
391            _ => {
392                return Err(GeopError::new(format!(
393                    "the edges are not a simple loop: several of them go on from where edge {last} ends"
394                )));
395            }
396        }
397    }
398    let (first, last) = (&out[0], out.last().expect("one side at least"));
399    if !last.end.could_be_equal(&first.start) {
400        return Err(GeopError::new(format!(
401            "the edges are not a closed loop: edge {} ends where edge {} does not start",
402            last.name, first.name
403        )));
404    }
405    Ok(out)
406}
407
408/// What a fill is tangent to along an edge: the face the edge bounds — its
409/// surface, its plane if it is flat — and whether that face runs the edge
410/// the way the edge's curve does.
411#[derive(Clone, Debug)]
412struct Tangency {
413    surface: NurbSurface3D,
414    plane: Option<Plane>,
415    forward: bool,
416}
417
418impl Tangency {
419    /// The one face the edge named `name` bounds, standing on its own:
420    /// what a fill tangent along the edge continues.
421    fn along(part: &NamedModel, name: &str) -> GeopResult<Self> {
422        let model = part.topology();
423        let edge = part.edge_id(name)?;
424        let coedges = model.coedges_of_edge(edge);
425        let [coedge] = coedges.as_slice() else {
426            return Err(GeopError::new(format!(
427                "edge {name} bounds {} faces: tangency is to the one face an edge bounds, standing on its own",
428                coedges.len()
429            )));
430        };
431        let coedge = model.get_coedge(*coedge)?;
432        let surface = model.get_face(coedge.face)?.surface.clone();
433        Ok(Self {
434            plane: surface.as_plane()?,
435            surface,
436            forward: coedge.sense == Sense::Forward,
437        })
438    }
439
440    /// The face as what a patch along `side` is tangent to: it runs the
441    /// side as the loop does where it runs the edge as the side does.
442    fn neighbour(&self, side: &Side) -> GeopResult<Neighbour<'_>> {
443        Neighbour::new(&self.surface, side.forward == self.forward)
444    }
445}
446
447/// Whether the loop of `sides` has a corner where side `k` meets the next:
448/// their tangents there are not parallel.
449fn has_corner(a: &Curve3, b: &Curve3) -> GeopResult<bool> {
450    let end = a.tangent(a.domain().1)?;
451    let start = b.tangent(b.domain().0)?;
452    Ok(!end
453        .prod_cross(&start)
454        .norm_sq()
455        .could_be_equal(Scalar::ZERO))
456}
457
458/// The closed loop `sides` filled (see the module docs), tangent along the
459/// edges of `faces` to those faces.
460fn fill(
461    namer: &Namer,
462    sides: &[Side],
463    faces: &[(String, Tangency)],
464) -> GeopResult<(BodySpec, BodyNames)> {
465    let n = sides.len();
466    // The loop's corners, side `k` running from corner `k` to `k + 1`, and
467    // each side's copy.
468    let mut spec = BodySpec {
469        vertices: sides.iter().map(|s| s.start).collect(),
470        edges: Vec::new(),
471        faces: Vec::new(),
472        shells: Vec::new(),
473        solid: false,
474    };
475    let mut names = BodyNames {
476        vertices: sides.iter().map(|s| namer.name(&[&s.start_name])).collect(),
477        ..Default::default()
478    };
479
480    if let Some(plane) = flat(sides)? {
481        for (e, face) in faces {
482            let same = face.plane.as_ref().is_some_and(|p| {
483                p.normal
484                    .prod_cross(&plane.normal)
485                    .norm_sq()
486                    .could_be_equal(Scalar::ZERO)
487                    && p.signed_distance(&plane.point).could_be_equal(Scalar::ZERO)
488            });
489            if !same {
490                let side = sides.iter().find(|s| s.entity == *e).expect("a side");
491                return Err(GeopError::new(format!(
492                    "the loop is flat, in a plane other than that of the face along edge {}: the fill cannot be tangent to it",
493                    side.name
494                )));
495            }
496        }
497        for (k, s) in sides.iter().enumerate() {
498            let (a, b) = (k, (k + 1) % n);
499            spec.edges.push(EdgeSpec {
500                curve: s.curve.clone(),
501                start: if s.forward { a } else { b },
502                end: if s.forward { b } else { a },
503            });
504            names.edges.push(namer.name(&[&s.name]));
505        }
506        spec.faces.push(flat_face(sides, &plane)?);
507        spec.shells.push(vec![0]);
508        names.faces.push(namer.root());
509        return Ok((spec, names));
510    }
511
512    let curves: Vec<Curve3> = sides.iter().map(Side::oriented).collect();
513    if n < 3 {
514        return Err(GeopError::new(format!(
515            "a loop of {n} edges that is not flat is not supported: it needs three corners at least"
516        )));
517    }
518    // Per side, the face it is to be tangent to, if any.
519    let neighbours = sides
520        .iter()
521        .map(|side| {
522            faces
523                .iter()
524                .find(|(e, _)| side.entity == *e)
525                .map(|(_, face)| face.neighbour(side))
526                .transpose()
527        })
528        .collect::<GeopResult<Vec<_>>>()?;
529    if n != 4 {
530        return quads(namer, sides, &curves, &neighbours, spec, names);
531    }
532    for k in 0..n {
533        if !has_corner(&curves[(k + n - 1) % n], &curves[k])? {
534            return Err(GeopError::new(format!(
535                "the loop runs on smoothly through vertex {}, between edges {} and {}: a Coons patch filling it needs a corner there",
536                sides[k].start_name,
537                sides[(k + n - 1) % n].name,
538                sides[k].name
539            )));
540        }
541    }
542    for (k, s) in sides.iter().enumerate() {
543        spec.edges.push(EdgeSpec {
544            curve: s.curve.clone(),
545            start: if s.forward { k } else { (k + 1) % 4 },
546            end: if s.forward { (k + 1) % 4 } else { k },
547        });
548        names.edges.push(namer.name(&[&s.name]));
549    }
550    let mut surface = NurbSurface3D::coons([&curves[0], &curves[1], &curves[2], &curves[3]])?;
551    if !faces.is_empty() {
552        let neighbours: [Option<Neighbour<'_>>; 4] = std::array::from_fn(|k| neighbours[k].clone());
553        surface = surface.tangent_to(&neighbours)?;
554    }
555    spec.faces.push(FaceSpec::on_unit_square(
556        surface,
557        [0, 1, 2, 3].map(|k| CoedgeOn::Edge(k, sides[k].sense())),
558    )?);
559    spec.shells.push(vec![0]);
560    names.faces.push(namer.root());
561    Ok((spec, names))
562}
563
564/// The plane `sides` all lie in, if they do: through three of their
565/// control points far apart, every other control point on it — so every
566/// curve is in it. `None` for a loop that is not flat.
567fn flat(sides: &[Side]) -> GeopResult<Option<Plane>> {
568    let points: Vec<Vector3> = sides
569        .iter()
570        .flat_map(|s| &s.curve.control_points)
571        .map(Vector4::dehomogenized)
572        .collect::<GeopResult<_>>()?;
573    // Which three is a free choice: far apart, so the plane through them is
574    // well determined.
575    let p0 = points[0];
576    let far = |key: &dyn Fn(&Vector3) -> f64| {
577        *points
578            .iter()
579            .max_by(|a, b| key(a).total_cmp(&key(b)))
580            .expect("points")
581    };
582    let p1 = far(&|p| distance(p, &p0));
583    let p2 = far(&|p| p.sub(&p0).prod_cross(&p1.sub(&p0)).norm().to_f64());
584    let normal = p1.sub(&p0).prod_cross(&p2.sub(&p0));
585    if normal.norm_sq().could_be_equal(Scalar::ZERO) {
586        return Err(GeopError::new(
587            "the edges all lie along one line: they enclose nothing to fill",
588        ));
589    }
590    let plane = Plane::try_new(p0, normal)?;
591    Ok(points
592        .iter()
593        .all(|p| plane.signed_distance(p).could_be_equal(Scalar::ZERO))
594        .then_some(plane))
595}
596
597/// The flat face on `plane` within the loop `sides`: the box around the
598/// loop in the plane, as a bilinear patch, the loop's pcurves the curves'
599/// control points in its coordinates — an affine map, exact for any curve.
600/// Parametrized along the plane's `(x, y)` or `(y, x)`, whichever the loop
601/// winds counter-clockwise in.
602fn flat_face(sides: &[Side], plane: &Plane) -> GeopResult<FaceSpec> {
603    let origin = plane.point;
604    let first = &sides[0].curve.control_points;
605    let towards = Vector3::from_array([0, 1, 2].map(|k| first[first.len() - 1][k]))
606        .prod_scalar(Scalar::ONE.div(first[first.len() - 1][3])?);
607    // Any direction in the plane would do for `x`: towards the far end of
608    // the first curve, unless that is where it starts.
609    let mut e1 = towards.sub(&origin);
610    if e1.norm_sq().could_be_equal(Scalar::ZERO) {
611        e1 = plane
612            .normal
613            .orthonormal_complement()?
614            .into_iter()
615            .next()
616            .expect("a complement");
617    }
618    let e1 = e1
619        .sub(&plane.normal.prod_scalar(e1.prod_dot(&plane.normal)))
620        .normalize()?;
621    let e2 = plane.normal.prod_cross(&e1);
622    // The loop in the plane's coordinates: each curve's control points
623    // flattened, an affine map.
624    let local = sides
625        .iter()
626        .map(|side| {
627            let curve = side.oriented();
628            let control_points = curve
629                .control_points
630                .iter()
631                .map(|cp| flatten_point(cp, &origin, &e1, &e2))
632                .collect();
633            NurbCurve::try_new(curve.degree, control_points, curve.knot_vector.clone())
634        })
635        .collect::<GeopResult<Vec<NurbCurve2D>>>()?;
636    let area = loop_signed_area(&local)?;
637    let swapped = if area.definitely_greater(Scalar::ZERO) {
638        false
639    } else if area.definitely_less(Scalar::ZERO) {
640        true
641    } else {
642        return Err(GeopError::new(
643            "cannot tell which way the loop runs round: it encloses no area",
644        ));
645    };
646    let patch = FlatPatch::around(origin, e1, e2, &local, swapped)?;
647    let surface = patch.surface()?;
648    let pcurves = local
649        .iter()
650        .map(|curve| patch.pcurve(curve))
651        .collect::<GeopResult<Vec<_>>>()?;
652    let outer = sides
653        .iter()
654        .zip(pcurves)
655        .enumerate()
656        .map(|(k, (s, pcurve))| CoedgeSpec {
657            on: CoedgeOn::Edge(k, s.sense()),
658            pcurve,
659        })
660        .collect();
661    Ok(FaceSpec {
662        surface,
663        outer,
664        holes: Vec::new(),
665    })
666}
667
668/// The loop `sides` filled smoothly with a quadrilateral along each side
669/// around a flat inner polygon, tangent along the sides with `neighbours`
670/// to their faces (see [`NurbSurface3D::fill`]), added to `spec`, which
671/// has the loop's corners.
672fn quads(
673    namer: &Namer,
674    sides: &[Side],
675    curves: &[Curve3],
676    neighbours: &[Option<Neighbour<'_>>],
677    mut spec: BodySpec,
678    mut names: BodyNames,
679) -> GeopResult<(BodySpec, BodyNames)> {
680    let n = sides.len();
681    let fill = NurbSurface3D::fill(curves, neighbours)?;
682    let inner = &fill.inner;
683    // Vertices: the corners `0..n`, the inner corners `n..2n`.
684    for (s, p) in sides.iter().zip(inner) {
685        spec.vertices.push(*p);
686        names.vertices.push(namer.name(&[&s.start_name, "inner"]));
687    }
688    let inner_at = |k: usize| n + k % n;
689    // Edges: per side its copy, `3k`, the spoke from its start to the
690    // inner polygon, `3k + 1`, and the inner polygon's side alongside it,
691    // `3k + 2`.
692    let mut inner_sides = Vec::with_capacity(n);
693    for (k, s) in sides.iter().enumerate() {
694        let (a, b) = (k, (k + 1) % n);
695        spec.edges.push(EdgeSpec {
696            curve: s.curve.clone(),
697            start: if s.forward { a } else { b },
698            end: if s.forward { b } else { a },
699        });
700        names.edges.push(namer.name(&[&s.name]));
701        spec.edges.push(EdgeSpec {
702            curve: fill.spokes[k].clone(),
703            start: k,
704            end: inner_at(k),
705        });
706        names.edges.push(namer.name(&[&s.start_name, "spoke"]));
707        let curve = NurbCurve3D::line(inner[k], inner[(k + 1) % n])?;
708        spec.edges.push(EdgeSpec {
709            curve: curve.clone(),
710            start: inner_at(k),
711            end: inner_at(k + 1),
712        });
713        names.edges.push(namer.name(&[&s.name, "inner"]));
714        inner_sides.push(Side {
715            entity: s.entity.clone(),
716            name: namer.name(&[&s.name, "inner"]),
717            curve,
718            forward: true,
719            start: inner[k],
720            end: inner[(k + 1) % n],
721            start_name: String::new(),
722            end_name: String::new(),
723        });
724    }
725    let mut faces = Vec::with_capacity(n + 1);
726    for (k, surface) in fill.patches.iter().enumerate() {
727        let next = (k + 1) % n;
728        let surface = surface.clone();
729        faces.push(spec.faces.len());
730        spec.faces.push(FaceSpec::on_unit_square(
731            surface,
732            [
733                CoedgeOn::Edge(3 * k, sides[k].sense()),
734                CoedgeOn::Edge(3 * next + 1, Sense::Forward),
735                CoedgeOn::Edge(3 * k + 2, Sense::Reversed),
736                CoedgeOn::Edge(3 * k + 1, Sense::Reversed),
737            ],
738        )?);
739        names.faces.push(namer.name(&[&sides[k].name, "patch"]));
740    }
741    // The inner polygon, flat, its sides run as the loop runs.
742    let mut middle = flat_face(&inner_sides, &fill.plane)?;
743    for (k, c) in middle.outer.iter_mut().enumerate() {
744        c.on = CoedgeOn::Edge(3 * k + 2, Sense::Forward);
745    }
746    faces.push(spec.faces.len());
747    spec.faces.push(middle);
748    names.faces.push(namer.name(&["inner"]));
749    spec.shells.push(faces);
750    Ok((spec, names))
751}
752
753#[cfg(test)]
754pub(crate) mod tests;