Skip to main content

geop_lib_shell/
shell.rs

1//! [`shell`]: a solid hollowed out to walls of one thickness — or of one
2//! per face — open where faces are taken away.
3//!
4//! The hollow is built directly, not cut out by a boolean: the walls'
5//! inner side is the solid's own boundary moved inward, face for face, so
6//! it has exactly the solid's topology, and everything about how the inner
7//! and the outer side fit together is known without intersecting
8//! anything.
9//!
10//! **The inner side.** Every face kept is offset inward by the thickness
11//! (see [`NurbSurface3D::offset`]: any surface, fitted and enclosed where it
12//! is no plane or revolution), every face taken away stays where it is:
13//! its inner copy is what the opening leaves of it. The inner side is the
14//! solid rebuilt on those surfaces ([`crate::rebuilt`]): a vertex moves to
15//! where the offsets of the faces around it meet, an edge to where the
16//! offsets of its two faces meet, whatever curve that is. A vertex on the
17//! axis of a revolved face, at its pole, moves to the pole of the face's
18//! inner copy: for a cone's apex, the offset cone's apex.
19//!
20//! **Faces that collapse.** A face whose offset collapses — every point
21//! of it past its centre of curvature, a blend narrower than the walls are
22//! thick, or the corner patch where such blends meet — has no inner copy.
23//! The solid is contracted before it is rebuilt ([`Contracted`]): a
24//! collapsed blend's edges along the faces it rounds, where it is tangent
25//! to them, share one copy, where those faces' offsets meet — a sharp inner
26//! corner; its edges across it vanish, and their ends share one vertex.
27//!
28//! **Faces that turn inside out.** A chamfer narrower than the walls has
29//! an offset, but the offsets of the faces it cuts meet before it: placed
30//! where the offsets meet, its edges across it turn around. Such an edge
31//! vanishes, and a face left with two edges by it vanishes too, its two
32//! remaining edges sharing one copy, as a collapsed blend's do. The
33//! vertices are placed again without it, until no edge turns around. An
34//! edge that turns around between two faces that stay vanishes the same
35//! way, its ends one vertex where four faces meet.
36//!
37//! **The opening.** Where a face `F` is taken away, its inner copy `F'`
38//! lies on `F`'s own surface, inside it, and what is left of `F` is `F`
39//! without `F'`: a rim, the walls' cut edge. Its boundary is `F`'s
40//! boundary and `F'`'s, run the other way. Two faces taken away side by
41//! side share an edge, and their inner copies share the middle of it — the
42//! part between the walls; what is left of that edge is a short piece at
43//! each end, where a wall reaches it, and the rims run along those. A rim
44//! is put together from these pieces by following them end to end, and may
45//! come out as several faces — a face taken away together with the faces
46//! on two opposite sides of it leaves two strips.
47//!
48//! A face taken away that every face kept around it runs smoothly into —
49//! the top of a box whose top edges are rounded — leaves no rim: the walls
50//! end where it was, each of its edges a free edge of the face kept there,
51//! with an end face along it as a sheet's border has ([`smoothly_open`]).
52//!
53//! **An opening at an inward corner.** A face taken away that meets a
54//! face kept at an inward corner — a pocket's floor, a step, the face a
55//! boss stands on — has an inner copy that reaches past it: the wall kept
56//! there is thickened away from it and stands on its surface beyond its
57//! edge. What is left on that surface is then the face less its copy (the
58//! rim, facing as the face) and the copy less the face (the underside of
59//! the wall, facing the other way): the hollow's section by the surface.
60//! That is what the solid less its inner side, closed into a solid of its
61//! own, comes to, and a boolean finds it, the face and its copy lying on
62//! one surface as faces of a union often do. The walls' names there are
63//! the boolean's.
64//!
65//! **The result.** The faces kept, their inner copies turned around, and
66//! the rims, as one solid. Without an opening, the inner side is a void of
67//! its own: a second shell.
68//!
69//! **A solid with voids.** Each of its shells is offset into the material
70//! on its own: the wall round a void stands free in the hollow, a solid of
71//! its own (`shell(Scalar,void,k)` for the `k`-th shell), its void the
72//! void's faces, its outside their inner copies.
73//!
74//! **A sheet** — faces standing on their own — is shelled the same way
75//! ([`thicken`]): its faces are the outer side, their inner copies the
76//! inner one, and along every free edge, used by one face only, a wall
77//! joins the edge to its inner copy — the ruled surface between them, with
78//! a straight edge from each of its vertices to that vertex's inner copy.
79//! The inner side alone is the sheet offset ([`offset_faces`]).
80//!
81//! What it does not do (yet): take away a face tangent to faces kept along
82//! some of its edges and not others, open the walls grown outward where a
83//! face taken away meets one kept at an inward corner, or open the wall
84//! round a void. Walls so thick
85//! that an inner edge turns around or shrinks to a point, or that an edge
86//! of the result crosses one of its faces, are refused; two faces crossing
87//! along a curve no edge reaches are not noticed.
88
89use std::collections::HashMap;
90
91use geop_core_geometry::{
92    intersection::curve_surface_overlaps,
93    nurb_curve::{NurbCurve2D, NurbCurve3D, loop_signed_area},
94    nurb_surface::NurbSurface3D,
95    search::{MAX_NODES, PROJECT_ITERATIONS, min_subdivision_size},
96};
97use geop_core_math::{
98    geop_error::{GeopError, GeopResult, WithContext},
99    scalars::Scalar,
100    solvers::walk::{Walked, walk},
101    vector::{Vector2, Vector3},
102    with_context,
103};
104use geop_core_topology::{
105    Body, Curve2, Curve3, FaceId, Model, Sense, ShellId, SolidId,
106    build::{BodySpec, BuiltBody, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
107    contains::face::{PointClassification, pcurves_contain},
108    validation::{ValidationParameters, body_crossing},
109};
110use geop_lib_booleans::{RemeshParams, boolean};
111use geop_ops_model::{BodyNames, BooleanOp, NamedModel, Namer};
112
113use crate::rebuild::{Placed, along, room, smooth_along};
114
115/// Seed of the ray casting that finds which outer loop a hole lies in:
116/// any gives the same answer (see `pcurves_contain`), a constant keeps it
117/// reproducible.
118const SEED: u64 = 0x5E11_0000_0000_0001;
119
120/// Hollows the solid `solid` of `part` out to walls `thickness` thick —
121/// measured inward from its faces, or outward for a negative thickness —
122/// those of the faces in `thicker` as thick as it says instead — open where
123/// its faces `open` were — see the module docs — and returns the hollowed
124/// solid, which replaces it and is named `namer`'s root.
125///
126/// What stays of the solid keeps its names. What is new is named after
127/// what it was made from, with `Scalar` the operation:
128///
129/// - `shell(Scalar,X)`: the inner copy of the face, edge or vertex `X` kept; for
130///   a face `X` taken away, the rim it leaves — `shell(Scalar,X,0)`,
131///   `shell(Scalar,X,1)`, ... if that is several faces;
132/// - `shell(Scalar,E,start)` / `shell(Scalar,E,end)`: what is left of an edge `E`
133///   between two faces taken away, at its start and its end.
134pub fn shell(
135    part: &mut NamedModel,
136    namer: &Namer,
137    solid: SolidId,
138    open: &[FaceId],
139    thickness: Scalar,
140    thicker: &[(FaceId, Scalar)],
141) -> GeopResult<SolidId> {
142    let ctx = with_context!(
143        "shell({}, {solid}, open={open:?}, {thickness:?}, thicker={thicker:?})",
144        namer.root()
145    );
146    // Outward, the walls grow from the faces out: the solid turned inside
147    // out and shelled inward, then turned back.
148    let outward = thickness.definitely_less(Scalar::ZERO);
149    let sign = |t: Scalar| if outward { t.neg() } else { t };
150    for &t in std::iter::once(&thickness).chain(thicker.iter().map(|(_, t)| t)) {
151        if !sign(t).definitely_greater(Scalar::ZERO) {
152            return Err(GeopError::new(format!(
153                "a shell's walls need a thickness other than zero, all inward or all outward, not {t:?}"
154            )))
155            .with_context(ctx);
156        }
157    }
158    let model = part.topology();
159    let faces = model.solid_faces(solid).with_context(ctx)?;
160    if let Some(f) = (open.iter())
161        .chain(thicker.iter().map(|(f, _)| f))
162        .find(|f| !faces.contains(f))
163    {
164        return Err(GeopError::new(format!(
165            "face {f} is not a face of the solid being shelled"
166        )))
167        .with_context(ctx);
168    }
169    // Each shell of the solid, its outside first and then each void's,
170    // offset into the material: around a void, the wall stands free in
171    // the hollow, a solid of its own.
172    let mut shells = Vec::new();
173    for &s in &model.get_solid(solid).with_context(ctx)?.shells {
174        shells.push(model.get_shell(s).with_context(ctx)?.faces.clone());
175    }
176    if let Some(&f) = open.iter().find(|f| !shells[0].contains(f)) {
177        return Err(GeopError::new(format!(
178            "face {} bounds a void of the solid: opening the wall round a void is not supported",
179            part.name(f)?
180        )))
181        .with_context(ctx);
182    }
183    // Per face, how thick its wall is: a face of one thickness and its
184    // neighbour of another still meet where their offsets do.
185    let thick = |f: &FaceId| {
186        sign(
187            (thicker.iter())
188                .find(|(g, _)| g == f)
189                .map_or(thickness, |&(_, t)| t),
190        )
191    };
192    let mut bodies: Vec<(BodySpec, BodyNames)> = Vec::new();
193    let mut inner_of_opening = None;
194    for (k, faces) in shells.iter().enumerate() {
195        let (mut spec, sources) = model.body_spec(faces, true).with_context(ctx)?;
196        if outward {
197            for face in &mut spec.faces {
198                *face = face.reversed();
199            }
200        }
201        let names = part.source_names(&sources).with_context(ctx)?;
202        let removed: Vec<bool> = sources.faces.iter().map(|f| open.contains(f)).collect();
203        let thickness: Vec<Scalar> = sources.faces.iter().map(thick).collect();
204        let corner = inward_corner(&spec, &removed).with_context(ctx)?;
205        if let Some((f, g, e)) = corner.filter(|_| outward) {
206            return Err(GeopError::new(format!(
207                "face {} is taken away where it meets face {} at an inward corner, along edge {}, and the walls grow outward: not supported",
208                names.faces[f], names.faces[g], names.edges[e]
209            )))
210            .with_context(ctx);
211        }
212        let hollow = Hollow::new(&spec, &names, removed, &thickness).with_context(ctx)?;
213        if corner.is_some() {
214            inner_of_opening = Some(hollow.inner_side(namer).with_context(ctx)?);
215            continue;
216        }
217        let (mut result, mut result_names) = hollow.result(namer).with_context(ctx)?;
218        // Turned inside out, outward, the faces face the void they bound
219        // now, and their offsets, turned around as inner copies are, face
220        // out: the offsets are the outside, listed first. So too round a
221        // void: its faces bound the wall's void, its offsets the wall.
222        if outward != (k > 0) {
223            result.shells.reverse();
224        }
225        if k > 0 {
226            result_names.solid = Some(namer.name(&["void", &k.to_string()]));
227        }
228        bodies.push((result, result_names));
229    }
230    refuse_crossing(&bodies).with_context(ctx)?;
231    let mut walls = bodies.into_iter();
232    let shelled = match inner_of_opening {
233        Some(inner) => subtracted(part, namer, solid, inner).with_context(ctx)?,
234        None => {
235            let (result, names) = walls.next().expect("the outside's walls");
236            part.replace_with_solid(&[solid.into()], result, names)
237                .with_context(ctx)?
238        }
239    };
240    for (result, names) in walls {
241        part.build_body(result, names).with_context(ctx)?;
242    }
243    Ok(shelled)
244}
245
246/// Thickens the sheet `sheet` of `part` into a solid of walls `thickness`
247/// thick — against its faces' normals, or along them if `along_normal` —
248/// and returns it: the sheet shelled (see the module docs), which replaces
249/// it and is named `namer`'s root.
250///
251/// The sheet's faces, edges and vertices keep their names, their inner
252/// copies are named as [`shell`] names them, and along a free edge `E` of
253/// the sheet the wall is `N(E,side)`, with `N` the operation, the straight
254/// edge from a vertex `V` of it to its inner copy `N(V,side)`.
255pub fn thicken(
256    part: &mut NamedModel,
257    namer: &Namer,
258    sheet: ShellId,
259    thickness: Scalar,
260    along_normal: bool,
261) -> GeopResult<SolidId> {
262    let ctx = with_context!(
263        "thicken({}, sheet {sheet}, {thickness:?}, along_normal={along_normal})",
264        namer.root()
265    );
266    if !thickness.definitely_greater(Scalar::ZERO) {
267        return Err(GeopError::new(format!(
268            "a thickness greater than zero is needed, not {thickness:?}"
269        )))
270        .with_context(ctx);
271    }
272    let model = part.topology();
273    let faces = model.body_faces(Body::Sheet(sheet)).with_context(ctx)?;
274    let (mut spec, sources) = model.body_spec(&faces, false).with_context(ctx)?;
275    if along_normal {
276        // Turned around, the walls go inward from the other side.
277        for face in &mut spec.faces {
278            *face = face.reversed();
279        }
280    }
281    let names = part.source_names(&sources).with_context(ctx)?;
282    let removed = vec![false; spec.faces.len()];
283    let thickness = vec![thickness; spec.faces.len()];
284    let hollow = Hollow::new(&spec, &names, removed, &thickness).with_context(ctx)?;
285    let (result, result_names) = hollow.result(namer).with_context(ctx)?;
286    let bodies = [(result, result_names)];
287    refuse_crossing(&bodies).with_context(ctx)?;
288    let [(result, result_names)] = bodies;
289    part.replace_with_solid(&[Body::Sheet(sheet)], result, result_names)
290        .with_context(ctx)
291}
292
293/// Copies the faces `faces` of `part` — of a solid or of a sheet — into a
294/// sheet `distance` along their normals (against them for a negative
295/// distance), and returns it: the inner side of the faces shelled (see the
296/// module docs), sharing nothing with them, which stay as they are.
297///
298/// The copy of each face, edge and vertex `X` is named `N(X)`, with `N`
299/// the operation.
300pub fn offset_faces(
301    part: &mut NamedModel,
302    namer: &Namer,
303    faces: &[FaceId],
304    distance: Scalar,
305) -> GeopResult<BuiltBody> {
306    let ctx = with_context!("offset_faces({}, {faces:?}, {distance:?})", namer.root());
307    if distance.could_be_equal(Scalar::ZERO) {
308        return Err(GeopError::new(format!(
309            "an offset needs a distance other than zero, not {distance:?}"
310        )))
311        .with_context(ctx);
312    }
313    let (spec, sources) = part.topology().body_spec(faces, false).with_context(ctx)?;
314    let names = part.source_names(&sources).with_context(ctx)?;
315    let removed = vec![false; spec.faces.len()];
316    let thickness = vec![distance.neg(); spec.faces.len()];
317    let hollow = Hollow::new(&spec, &names, removed, &thickness).with_context(ctx)?;
318    let bodies = [hollow.inner_side(namer).with_context(ctx)?];
319    refuse_crossing(&bodies).with_context(ctx)?;
320    let [(result, result_names)] = bodies;
321    part.build_body(result, result_names).with_context(ctx)
322}
323
324/// Refuses the bodies `bodies`, each with its names, if they would run
325/// into themselves or each other: an edge of one that crosses a face of
326/// one away from every vertex. The walls are built face by face from the
327/// faces' offsets, which say nothing about where faces apart from each
328/// other end up: a hole closer to a side than the walls are thick moves
329/// its wall through the side's, a void's wall can reach the outside's.
330/// Asked of the bodies built on their own, so the part is left as it was.
331fn refuse_crossing(bodies: &[(BodySpec, BodyNames)]) -> GeopResult<()> {
332    let mut model = Model::new();
333    let mut built = Vec::new();
334    for (spec, _) in bodies {
335        built.push(model.build_body(spec.clone())?);
336    }
337    let params = ValidationParameters::default();
338    let all = built
339        .iter()
340        .map(|b| b.body())
341        .collect::<GeopResult<Vec<_>>>()?;
342    let Some((edge, face, point)) = body_crossing(&params, &model, &all)? else {
343        return Ok(());
344    };
345    let name = |find: &dyn Fn(&BuiltBody) -> Option<usize>, of: fn(&BodyNames) -> &Vec<String>| {
346        (built.iter().zip(bodies))
347            .find_map(|(b, (_, names))| find(b).map(|i| of(names)[i].clone()))
348            .expect("built from these")
349    };
350    Err(GeopError::new(format!(
351        "the walls run into each other: edge {} crosses face {} at {point:?}: the walls are too thick for this part, which is not supported",
352        name(&|b| b.edges.iter().position(|&e| e == edge), |n| &n.edges),
353        name(&|b| b.faces.iter().position(|&f| f == face), |n| &n.faces),
354    )))
355}
356
357/// The solid `solid` of `part` less `inner`, the inner side of its
358/// outside (see [`Hollow::inner_side`]), closed into a solid: the walls,
359/// open where faces are taken away, as a boolean finds them (see the
360/// module docs). The inner side's faces keep their names, the rest as the
361/// boolean names what is left of the solid's.
362fn subtracted(
363    part: &mut NamedModel,
364    namer: &Namer,
365    solid: SolidId,
366    (mut inner, mut names): (BodySpec, BodyNames),
367) -> GeopResult<SolidId> {
368    inner.solid = true;
369    names.solid = Some(namer.name(&["inner"]));
370    let inner = part
371        .build_body(inner, names)
372        .map_err(|e| e.with_context("closing the inner side into a solid"))?
373        .solid
374        .ok_or_else(|| GeopError::new("the inner side closes into no solid"))?;
375    boolean(
376        part,
377        namer,
378        solid,
379        inner,
380        BooleanOp::Difference,
381        RemeshParams::default(),
382    )
383    .map_err(|e| e.with_context("the solid less its inner side"))?
384    .ok_or_else(|| GeopError::new("the walls leave nothing of the solid"))
385}
386
387/// A face taken away, `f`, that meets a face kept, `g`, at an inward
388/// corner — along the edge `e` the material between them turns more than
389/// a half turn — if there is one: `(f, g, e)`.
390///
391/// The opening of a face taken away is what its inner copy leaves of it,
392/// which assumes the inner copy lies inside the face. At an inward corner
393/// it does not: the wall kept there is thickened away from the face, and
394/// its end reaches past the face's edge. Decided halfway along the edge,
395/// from the faces' outward normals there — those of the surfaces of a
396/// [`BodySpec`] of a solid. A corner that could be tangent is no inward
397/// one: it is left to the check that walls cannot end on a smooth surface.
398fn inward_corner(spec: &BodySpec, removed: &[bool]) -> GeopResult<Option<(usize, usize, usize)>> {
399    for (e, at) in spec.edge_uses().iter().enumerate() {
400        let [(f, f_sense, f_pcurve), (g, _, g_pcurve)] = match *at.as_slice() {
401            [a, b] if removed[a.0] && !removed[b.0] => [a, b],
402            [a, b] if removed[b.0] && !removed[a.0] => [b, a],
403            _ => continue,
404        };
405        let curve = &spec.edges[e].curve;
406        let t = curve.mid_parameter()?;
407        let point = curve.evaluate(t)?;
408        let tangent = curve.tangent(t)?;
409        let along = match f_sense {
410            Sense::Forward => tangent,
411            Sense::Reversed => tangent.neg(),
412        };
413        let normal = |face: usize, pcurve: &Curve2| -> GeopResult<Vector3> {
414            let surface = &spec.faces[face].surface;
415            let seed = pcurve.evaluate(pcurve.mid_parameter()?)?;
416            let (u, v) = surface.project(
417                point,
418                seed[0].sharpen(),
419                seed[1].sharpen(),
420                PROJECT_ITERATIONS,
421            )?;
422            surface.normal(u, v)
423        };
424        let (n_f, n_g) = (normal(f, f_pcurve)?, normal(g, g_pcurve)?);
425        // Running into `f`, square to the edge: in front of `g` at an
426        // inward corner (see the fillet's bend).
427        if n_f
428            .prod_cross(&along)
429            .prod_dot(&n_g)
430            .definitely_greater(Scalar::ZERO)
431        {
432            return Ok(Some((f, g, e)));
433        }
434    }
435    Ok(None)
436}
437
438/// What the inner copy of an edge is.
439#[derive(Clone, Copy, Debug, PartialEq, Eq)]
440enum Copy {
441    /// A copy of its own.
442    Own,
443    /// None: the edge is of a collapsed face, across it — where a blend
444    /// meets a face square to it, or another collapsed one — and its ends'
445    /// copies are one point.
446    Vanishes,
447    /// The copy of the edge given: of a collapsed face's two edges along
448    /// it, where it is tangent to the faces it rounds, the second shares
449    /// the first's copy, where those faces' offsets meet.
450    As(usize),
451}
452
453/// The inner side's topology where faces collapse (see the module docs):
454/// `spec` without its collapsed faces, the edges across them contracted to
455/// points and the two along each merged into one. Each entity of `spec`
456/// is mapped to its copy here.
457struct Contracted {
458    spec: BodySpec,
459    names: BodyNames,
460    /// Per face of `spec`'s, whether the face it copies is kept.
461    kept: Vec<bool>,
462    /// Per vertex: its copy.
463    vertex: Vec<usize>,
464    /// Per edge: its copy, run the way given — `None` where it vanishes.
465    edge: Vec<Option<(usize, Sense)>>,
466    /// Per face: its copy, `None` where it collapses.
467    face: Vec<Option<usize>>,
468}
469
470/// What the copy of each edge of `spec` is, given which faces collapse —
471/// whose offset does — which invert — whose copy turns inside out — and
472/// which edges' copies shrink to nothing (see [`Copy`]).
473///
474/// An edge whose copy shrinks to nothing vanishes, its ends' copies one
475/// point, as does one between two faces gone. Of a face gone, the edges to
476/// faces that stay are along it: a blend narrower than the walls are thick
477/// leaves the two faces it rounds meeting in a sharp inner corner along it,
478/// and a chamfer turned inside out leaves the two it cuts meeting there.
479/// For a collapsed face, that is where it is tangent to them, and across it
480/// — where it meets a face square to it — nothing. For an inverted face,
481/// it is where its edges did not shrink. Of the two edges along it, the
482/// second takes the first's copy; with none, the face shrinks to a point.
483/// A face gone with other than two such edges or none, a free edge or a
484/// seam is refused: what would take its place is not that corner.
485fn collapse_edges(
486    spec: &BodySpec,
487    names: &BodyNames,
488    collapsed: &[bool],
489    inverted: &[bool],
490    shrunk: &[bool],
491) -> GeopResult<Vec<Copy>> {
492    let uses = spec.edge_uses();
493    let gone = |f: usize| collapsed[f] || inverted[f];
494    let mut copy = vec![Copy::Own; spec.edges.len()];
495    let mut along: Vec<Vec<usize>> = vec![Vec::new(); spec.faces.len()];
496    for (e, at) in uses.iter().enumerate() {
497        let [(f, ..), (g, ..)] = match *at.as_slice() {
498            [a, b] if a.0 != b.0 => [a, b],
499            _ => {
500                if at.iter().any(|&(f, ..)| gone(f)) || shrunk[e] {
501                    return Err(GeopError::new(format!(
502                        "the walls are too thick: face {} collapses, or the copy of its edge {} shrinks to nothing, and that edge is free or a seam, which is not supported",
503                        names.faces[at[0].0], names.edges[e]
504                    )));
505                }
506                continue;
507            }
508        };
509        if shrunk[e] {
510            copy[e] = Copy::Vanishes;
511            continue;
512        }
513        match (gone(f), gone(g)) {
514            (false, false) => {}
515            (true, true) => copy[e] = Copy::Vanishes,
516            _ => {
517                let x = if gone(f) { f } else { g };
518                if inverted[x] || smooth_along(spec, &spec.edges[e], f, g)? {
519                    along[x].push(e);
520                } else {
521                    copy[e] = Copy::Vanishes;
522                }
523            }
524        }
525    }
526    for (f, edges) in along.iter().enumerate() {
527        match *edges.as_slice() {
528            [] => {}
529            [first, second] => copy[second] = Copy::As(first),
530            _ => {
531                return Err(GeopError::new(format!(
532                    "the walls are too thick: face {} vanishes — its inner copy collapses or turns around — and {} of its edges to faces that stay are left, where a blend or a chamfer leaves two: not supported",
533                    names.faces[f],
534                    edges.len()
535                )));
536            }
537        }
538    }
539    Ok(copy)
540}
541
542/// Per face of `spec`, whether its copy turns inside out: a face kept,
543/// some of whose edges' copies `shrunk` to nothing, left with two edges or
544/// fewer by them — a chamfer narrower than the walls, between the two
545/// faces it cuts, its edges across it turned around; a cap beyond where a
546/// cone's offset closes, its rim on the offset's apex. What is left of it
547/// has no area: the faces either side of it meet, or it shrinks to a
548/// point. A face with holes is not decided so.
549fn inverted(spec: &BodySpec, kept: &[bool], shrunk: &[bool]) -> Vec<bool> {
550    (spec.faces.iter().enumerate())
551        .map(|(f, face)| {
552            let edges: Vec<usize> = (face.outer.iter())
553                .filter_map(|c| match c.on {
554                    CoedgeOn::Edge(e, _) => Some(e),
555                    CoedgeOn::Vertex(_) => None,
556                })
557                .collect();
558            kept[f]
559                && face.holes.is_empty()
560                && edges.iter().any(|&e| shrunk[e])
561                && edges.iter().filter(|&&e| !shrunk[e]).count() <= 2
562        })
563        .collect()
564}
565
566/// Whether `pcurve` runs along a pole of `surface`, every control point of
567/// it at one — so all of it, which lies in their hull, and the row is
568/// straight in `(u, v)`: a curve whose copy there is a point. The rim of a
569/// cone cut off short of its tip, on the offset closed in its apex.
570fn along_pole(surface: &NurbSurface3D, pcurve: &Curve2) -> GeopResult<bool> {
571    for p in &pcurve.control_points {
572        let p = p.dehomogenized()?;
573        if surface.pole_at(p[0], p[1]).is_none() {
574            return Ok(false);
575        }
576    }
577    Ok(true)
578}
579
580/// `spec` — the faces on their inner surfaces — contracted where faces
581/// collapse (see [`Contracted`]), the edges' copies `copy`, `kept` and
582/// `names` per entity of `spec`.
583fn contract(
584    spec: &BodySpec,
585    names: &BodyNames,
586    kept: &[bool],
587    gone: &[bool],
588    copy: &[Copy],
589) -> GeopResult<Contracted> {
590    // Vertices joined by an edge that vanishes share their copy.
591    let mut group: Vec<usize> = (0..spec.vertices.len()).collect();
592    fn root(group: &mut [usize], v: usize) -> usize {
593        let mut r = v;
594        while group[r] != r {
595            r = group[r];
596        }
597        group[v] = r;
598        r
599    }
600    for (e, edge) in spec.edges.iter().enumerate() {
601        if copy[e] == Copy::Vanishes {
602            let (a, b) = (root(&mut group, edge.start), root(&mut group, edge.end));
603            group[a.max(b)] = a.min(b);
604        }
605    }
606    let mut out = BodySpec::new(spec.solid);
607    let mut out_names = BodyNames {
608        solid: names.solid.clone(),
609        ..Default::default()
610    };
611    let mut index = vec![None; spec.vertices.len()];
612    let mut vertex = Vec::with_capacity(spec.vertices.len());
613    for v in 0..spec.vertices.len() {
614        let r = root(&mut group, v);
615        vertex.push(*index[r].get_or_insert_with(|| {
616            out.vertices.push(spec.vertices[r]);
617            out_names.vertices.push(names.vertices[r].clone());
618            out.vertices.len() - 1
619        }));
620    }
621    let mut edge = vec![None; spec.edges.len()];
622    for (e, original) in spec.edges.iter().enumerate() {
623        if copy[e] == Copy::Own {
624            edge[e] = Some((out.edges.len(), Sense::Forward));
625            out.edges.push(EdgeSpec {
626                curve: original.curve.clone(),
627                start: vertex[original.start],
628                end: vertex[original.end],
629            });
630            out_names.edges.push(names.edges[e].clone());
631        }
632    }
633    for (e, original) in spec.edges.iter().enumerate() {
634        if let Copy::As(first) = copy[e] {
635            let (i, _) = edge[first].expect("the first edge along a blend has its own copy");
636            let ends = (vertex[original.start], vertex[original.end]);
637            let sense = if ends == (out.edges[i].start, out.edges[i].end) {
638                Sense::Forward
639            } else if ends == (out.edges[i].end, out.edges[i].start) {
640                Sense::Reversed
641            } else {
642                return Err(GeopError::new(format!(
643                    "the walls are too thick: edges {} and {} along a collapsing blend do not end where each other's copies do: not supported",
644                    names.edges[first], names.edges[e]
645                )));
646            };
647            edge[e] = Some((i, sense));
648        }
649    }
650    let mut face = vec![None; spec.faces.len()];
651    let mut out_kept = Vec::new();
652    for (f, original) in spec.faces.iter().enumerate() {
653        if gone[f] {
654            continue;
655        }
656        let mut loops = Vec::new();
657        for lp in original.loops() {
658            let mut copied = Vec::new();
659            for c in lp {
660                let on = match c.on {
661                    // An edge that vanishes leaves its loop passing its
662                    // one vertex there: along the row of a pole, where the
663                    // face's surface has one (the rebuild keeps such a
664                    // coedge only there).
665                    CoedgeOn::Edge(e, sense) => match edge[e] {
666                        None => CoedgeOn::Vertex(vertex[spec.ends(c.on)?.0]),
667                        Some((i, along)) => CoedgeOn::Edge(
668                            i,
669                            if along == Sense::Forward {
670                                sense
671                            } else {
672                                sense.opposite()
673                            },
674                        ),
675                    },
676                    CoedgeOn::Vertex(v) => CoedgeOn::Vertex(vertex[v]),
677                };
678                copied.push(CoedgeSpec {
679                    on,
680                    pcurve: c.pcurve.clone(),
681                });
682            }
683            if !copied.iter().any(|c| matches!(c.on, CoedgeOn::Edge(..))) {
684                return Err(GeopError::new(format!(
685                    "the walls are too thick: every edge of a loop of face {} vanishes, and its inner copy with it",
686                    names.faces[f]
687                )));
688            }
689            loops.push(copied);
690        }
691        face[f] = Some(out.faces.len());
692        let mut loops = loops.into_iter();
693        out.faces.push(FaceSpec {
694            surface: original.surface.clone(),
695            outer: loops.next().expect("a face has an outer loop"),
696            holes: loops.collect(),
697        });
698        out_names.faces.push(names.faces[f].clone());
699        out_kept.push(kept[f]);
700    }
701    out.shells = spec
702        .shells
703        .iter()
704        .map(|shell| shell.iter().filter_map(|&f| face[f]).collect())
705        .collect();
706    Ok(Contracted {
707        spec: out,
708        names: out_names,
709        kept: out_kept,
710        vertex,
711        edge,
712        face,
713    })
714}
715
716/// Per face taken away, whether it is smoothly open: tangent along every
717/// edge to the face kept there. Its walls do not stand on it, they end
718/// where it was — each edge a free edge of the face kept, whose wall gets
719/// an end face along it as a sheet's border does — and nothing is left of
720/// it: the open top of a box whose top edges are rounded. A face taken
721/// away tangent to faces kept along some edges and not others, or one
722/// that meets another face taken away, is refused: its rim would end on a
723/// smooth surface.
724fn smoothly_open(spec: &BodySpec, names: &BodyNames, removed: &[bool]) -> GeopResult<Vec<bool>> {
725    let mut tangent = vec![0usize; spec.faces.len()];
726    let mut other = vec![0usize; spec.faces.len()];
727    for (e, at) in spec.edge_uses().iter().enumerate() {
728        let [a, b] = match *at.as_slice() {
729            [a, b] if a.0 != b.0 => [a.0, b.0],
730            _ => continue,
731        };
732        match (removed[a], removed[b]) {
733            (false, false) => {}
734            (true, true) => {
735                other[a] += 1;
736                other[b] += 1;
737            }
738            _ => {
739                let gone = if removed[a] { a } else { b };
740                if smooth_along(spec, &spec.edges[e], a, b)? {
741                    tangent[gone] += 1;
742                } else {
743                    other[gone] += 1;
744                }
745            }
746        }
747    }
748    (0..spec.faces.len())
749        .map(|f| {
750            if tangent[f] == 0 {
751                return Ok(false);
752            }
753            if other[f] > 0 {
754                return Err(GeopError::new(format!(
755                    "face {} is taken away tangent to a face kept along {} of its edges, and not along {}: the walls cannot end on a smooth surface, which is not supported",
756                    names.faces[f], tangent[f], other[f]
757                )));
758            }
759            Ok(true)
760        })
761        .collect()
762}
763
764/// A coedge of the solid: face `f`, its loop `l` (0 the outer one), and
765/// the `k`-th coedge of that.
766#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
767struct At {
768    f: usize,
769    l: usize,
770    k: usize,
771}
772
773/// What is left of an edge between two faces taken away: a piece at its
774/// start and one at its end, each only where a wall reaches it.
775type Pieces<T> = (Option<T>, Option<T>);
776
777/// Where the entities of the solid went in the result, by their index
778/// there: each walled vertex and its inner copy, each edge that is not
779/// open and its inner copy, and what is left of each open one.
780struct Numbering {
781    vertex: Vec<Option<usize>>,
782    inner_vertex: Vec<Option<usize>>,
783    edge: Vec<Option<usize>>,
784    inner_edge: Vec<Option<usize>>,
785    pieces: Vec<Pieces<usize>>,
786}
787
788/// The pieces in the order a coedge of `sense` comes across them.
789fn in_order<T>(pieces: (T, T), sense: Sense) -> (T, T) {
790    match sense {
791        Sense::Forward => pieces,
792        Sense::Reversed => (pieces.1, pieces.0),
793    }
794}
795
796/// The solid described as [`BodySpec`], and what hollowing it moves where.
797struct Hollow<'a> {
798    spec: &'a BodySpec,
799    names: &'a BodyNames,
800    /// Per face: whether it is taken away.
801    removed: Vec<bool>,
802    /// Per vertex: whether a face kept meets it. Where none does, the inner
803    /// copy of the vertex is the vertex itself, and nothing of the solid
804    /// is left around it.
805    walled: Vec<bool>,
806    /// Per edge: whether both its faces are taken away.
807    open_edge: Vec<bool>,
808    /// Per edge: whether it is free, used by one face only — an edge of a
809    /// sheet's border — or one of a face smoothly open: it gets a wall.
810    free_edge: Vec<bool>,
811    /// Per face: whether it is taken away smoothly open (see
812    /// [`smoothly_open`]): nothing is left of it, and it has no copy.
813    smooth: Vec<bool>,
814    /// The inner side: the solid rebuilt with every face kept on its offset
815    /// and every face taken away on its own surface, each with room around
816    /// it — its vertices, edges and pcurves where the walled ones go — and
817    /// contracted where faces collapse (see [`Contracted`]).
818    inner: BodySpec,
819    /// Where each entity of the solid's copy is in `inner`.
820    copies: Contracted,
821    /// Per open edge: what is left of it at its start and at its end, where
822    /// those are walled.
823    pieces: Vec<Pieces<Curve3>>,
824}
825
826impl<'a> Hollow<'a> {
827    fn new(
828        spec: &'a BodySpec,
829        names: &'a BodyNames,
830        removed: Vec<bool>,
831        thickness: &[Scalar],
832    ) -> GeopResult<Self> {
833        let mut walled = vec![false; spec.vertices.len()];
834        let mut open_edge = vec![true; spec.edges.len()];
835        let mut uses = vec![0usize; spec.edges.len()];
836        for (f, face) in spec.faces.iter().enumerate() {
837            for lp in face.loops() {
838                for c in lp {
839                    let (_, end) = spec.ends(c.on)?;
840                    walled[end] |= !removed[f];
841                    if let CoedgeOn::Edge(e, _) = c.on {
842                        open_edge[e] &= removed[f];
843                        uses[e] += 1;
844                    }
845                }
846            }
847        }
848        // The inner side is the solid rebuilt with every face kept on its
849        // offset, every face taken away on its own surface (see
850        // [`rebuilt`]). One taken away cannot run smoothly into one kept:
851        // the walls would have to end on a smooth surface.
852        let kept: Vec<bool> = removed.iter().map(|&r| !r).collect();
853        let smooth = smoothly_open(spec, names, &removed)?;
854        let mut offsets = spec.clone();
855        let mut collapsed = vec![false; spec.faces.len()];
856        for (f, face) in offsets.faces.iter_mut().enumerate() {
857            if kept[f] {
858                // The offset of the face given room, not the offset given
859                // room afterwards: a fitted offset's end span carried on is
860                // a polynomial extrapolated, and its width with it.
861                match room(face.surface.clone())?
862                    .offset(thickness[f].neg())
863                    .map_err(|e| e.with_context(format!("offsetting face {}", names.faces[f])))?
864                {
865                    Some(surface) => face.surface = surface,
866                    None => collapsed[f] = true,
867                }
868            }
869        }
870        // The edges whose copies shrink to nothing: along where an offset
871        // closes in a point, and those that turn around, found by placing
872        // the inner side's vertices. Each time some do, they vanish, and
873        // the faces they leave with no area with them, until none does.
874        let mut shrunk = vec![false; spec.edges.len()];
875        for (e, at) in offsets.edge_uses().iter().enumerate() {
876            for &(f, _, pcurve) in at {
877                if kept[f] && !collapsed[f] && along_pole(&offsets.faces[f].surface, pcurve)? {
878                    shrunk[e] = true;
879                }
880            }
881        }
882        let (copies, inner) = loop {
883            let inverted = inverted(spec, &kept, &shrunk);
884            let copy = collapse_edges(spec, names, &collapsed, &inverted, &shrunk)?;
885            let gone: Vec<bool> = (0..spec.faces.len())
886                .map(|f| collapsed[f] || inverted[f] || smooth[f])
887                .collect();
888            if (0..spec.faces.len()).all(|f| !kept[f] || gone[f]) {
889                return Err(GeopError::new(
890                    "the walls are too thick: the inner copy of every face kept turns around or shrinks to nothing",
891                ));
892            }
893            let copies = contract(&offsets, names, &kept, &gone, &copy)?;
894            let reseat = vec![false; copies.spec.vertices.len()];
895            let placed = Placed::new(&copies.spec, &copies.names, &copies.kept, &reseat).map_err(|e| {
896                // Where faces gave way to their neighbours, those may not
897                // meet either: the walls turn the inner side inside out
898                // across more than a chamfer.
899                match (shrunk.iter().position(|&t| t), inverted.iter().position(|&i| i)) {
900                    (Some(t), Some(f)) => GeopError::new(format!(
901                        "the walls are too thick: edge {} turns around, and the inner copy of face {} with it, but the faces either side of it do not meet in its place ({})",
902                        names.edges[t],
903                        names.faces[f],
904                        e.root_message()
905                    )),
906                    _ => e,
907                }
908                .with_context("placing the walls' inner side's vertices")
909            })?;
910            let now = placed.turned();
911            if now.is_empty() {
912                let inner = placed
913                    .rebuilt()
914                    .map_err(|e| e.with_context("building the walls' inner side"))?;
915                break (copies, inner);
916            }
917            for (e, copied) in copies.edge.iter().enumerate() {
918                if copied.is_some_and(|(i, _)| now.contains(&i)) {
919                    shrunk[e] = true;
920                }
921            }
922        };
923        let mut hollow = Self {
924            spec,
925            names,
926            removed,
927            walled,
928            open_edge,
929            // An edge of a face smoothly open is a free edge of the face kept
930            // there: its wall ends in an end face along it.
931            free_edge: (0..spec.edges.len())
932                .map(|e| uses[e] == 1 || spec.edge_uses()[e].iter().any(|&(f, ..)| smooth[f]))
933                .collect(),
934            smooth,
935            inner,
936            copies,
937            pieces: Vec::new(),
938        };
939        hollow.pieces = (0..spec.edges.len())
940            .map(|e| hollow.pieces_of(e))
941            .collect::<GeopResult<_>>()?;
942        Ok(hollow)
943    }
944
945    /// The coedges of the solid, each with where it is.
946    fn coedges(&self) -> impl Iterator<Item = (At, &'a CoedgeSpec)> + 'a {
947        let spec = self.spec;
948        spec.faces.iter().enumerate().flat_map(|(f, face)| {
949            face.loops()
950                .into_iter()
951                .enumerate()
952                .flat_map(move |(l, lp)| {
953                    lp.iter().enumerate().map(move |(k, c)| (At { f, l, k }, c))
954                })
955        })
956    }
957
958    /// The coedge before `at` in its loop.
959    fn previous(&self, at: At) -> At {
960        let n = self.spec.faces[at.f].loops()[at.l].len();
961        At {
962            k: (at.k + n - 1) % n,
963            ..at
964        }
965    }
966
967    fn inner_point(&self, v: usize) -> Vector3 {
968        self.inner.vertices[self.copies.vertex[v]]
969    }
970
971    /// The curve of the inner copy of edge `e`, run the way `e` runs, if it
972    /// has one.
973    fn inner_curve(&self, e: usize) -> Option<Curve3> {
974        self.copies.edge[e].map(|(i, sense)| sense.orient(&self.inner.edges[i].curve))
975    }
976
977    /// What is left of the open edge `e` at its start and at its end: from
978    /// a walled end to its inner copy, which lies on the edge.
979    fn pieces_of(&self, e: usize) -> GeopResult<Pieces<Curve3>> {
980        if !self.open_edge[e] {
981            return Ok((None, None));
982        }
983        let edge = &self.spec.edges[e];
984        let piece = |from: Vector3, to: Vector3| along(&edge.curve, from, to);
985        let (s, t) = (edge.start, edge.end);
986        Ok((
987            self.walled[s]
988                .then(|| piece(self.spec.vertices[s], self.inner_point(s)))
989                .transpose()?,
990            self.walled[t]
991                .then(|| piece(self.inner_point(t), self.spec.vertices[t]))
992                .transpose()?,
993        ))
994    }
995
996    /// The inner copy of the coedge `at`, on its face's inner surface —
997    /// its edge numbered as in `inner` — if it runs along an edge that has
998    /// one: the coedge of the copy's loop along the edge's copy, the same
999    /// way. Found by the edge, not by its place in the loop: the rebuild
1000    /// drops a coedge along a pole that the inner side no longer reaches.
1001    fn inner_coedge(&self, at: At) -> Option<CoedgeSpec> {
1002        let face = self.copies.face[at.f]?;
1003        let CoedgeOn::Edge(e, sense) = self.spec.faces[at.f].loops()[at.l][at.k].on else {
1004            return None;
1005        };
1006        let (i, along) = self.copies.edge[e]?;
1007        let sense = if along == Sense::Forward {
1008            sense
1009        } else {
1010            sense.opposite()
1011        };
1012        self.inner.faces[face].loops()[at.l]
1013            .iter()
1014            .find(|c| c.on == CoedgeOn::Edge(i, sense))
1015            .cloned()
1016    }
1017
1018    /// Where the inner copy of the coedge `at` ends on its face's inner
1019    /// surface: of the last coedge up to it that has a copy.
1020    fn inner_end(&self, at: At) -> GeopResult<Vector2> {
1021        let mut here = at;
1022        loop {
1023            if let Some(c) = self.inner_coedge(here) {
1024                return Ok(c.pcurve.ends()?.1);
1025            }
1026            here = self.previous(here);
1027            if here == at {
1028                return Err(GeopError::new("no coedge of the loop has a copy"));
1029            }
1030        }
1031    }
1032
1033    /// The hollowed solid, described whole, and the names of what it is
1034    /// made of.
1035    fn result(&self, namer: &Namer) -> GeopResult<(BodySpec, BodyNames)> {
1036        let spec = self.spec;
1037        let mut out = BodySpec::new(true);
1038        let mut names = BodyNames {
1039            solid: Some(namer.root()),
1040            ..Default::default()
1041        };
1042        let inner_name = |name: &str| namer.name(&[name]);
1043
1044        let mut at = Numbering {
1045            vertex: vec![None; spec.vertices.len()],
1046            inner_vertex: vec![None; self.inner.vertices.len()],
1047            edge: vec![None; spec.edges.len()],
1048            inner_edge: vec![None; self.inner.edges.len()],
1049            pieces: vec![(None, None); spec.edges.len()],
1050        };
1051        for v in 0..spec.vertices.len() {
1052            if self.walled[v] {
1053                at.vertex[v] = Some(out.vertices.len());
1054                out.vertices.push(spec.vertices[v]);
1055                names.vertices.push(self.names.vertices[v].clone());
1056            }
1057        }
1058        // The inner vertices, each once: vertices joined where a face
1059        // collapses share one, named after the first.
1060        for v in 0..spec.vertices.len() {
1061            let i = self.copies.vertex[v];
1062            if self.walled[v] && at.inner_vertex[i].is_none() {
1063                at.inner_vertex[i] = Some(out.vertices.len());
1064                out.vertices.push(self.inner.vertices[i]);
1065                names.vertices.push(inner_name(&self.names.vertices[v]));
1066            }
1067        }
1068        let inner_vertex_of = |v: usize| at.inner_vertex[self.copies.vertex[v]];
1069        let inner_vertex: Vec<Option<usize>> =
1070            (0..spec.vertices.len()).map(inner_vertex_of).collect();
1071        let vertex = &at.vertex;
1072        let mut push_edge = |curve: Curve3, start: usize, end: usize, name: String| {
1073            out.edges.push(EdgeSpec { curve, start, end });
1074            names.edges.push(name);
1075            Some(out.edges.len() - 1)
1076        };
1077        for (e, original) in spec.edges.iter().enumerate() {
1078            let name = &self.names.edges[e];
1079            let (s, t) = (original.start, original.end);
1080            if !self.open_edge[e] {
1081                at.edge[e] = push_edge(
1082                    original.curve.clone(),
1083                    vertex[s].unwrap(),
1084                    vertex[t].unwrap(),
1085                    name.clone(),
1086                );
1087                // Its inner copy, once for the edges sharing it.
1088                if let Some((i, Sense::Forward)) = self.copies.edge[e]
1089                    && at.inner_edge[i].is_none()
1090                {
1091                    let copy = &self.inner.edges[i];
1092                    at.inner_edge[i] = push_edge(
1093                        copy.curve.clone(),
1094                        at.inner_vertex[copy.start].unwrap(),
1095                        at.inner_vertex[copy.end].unwrap(),
1096                        inner_name(name),
1097                    );
1098                }
1099            }
1100            let (first, second) = &self.pieces[e];
1101            if let Some(curve) = first {
1102                at.pieces[e].0 = push_edge(
1103                    curve.clone(),
1104                    vertex[s].unwrap(),
1105                    inner_vertex[s].unwrap(),
1106                    namer.name(&[name, "start"]),
1107                );
1108            }
1109            if let Some(curve) = second {
1110                at.pieces[e].1 = push_edge(
1111                    curve.clone(),
1112                    inner_vertex[t].unwrap(),
1113                    vertex[t].unwrap(),
1114                    namer.name(&[name, "end"]),
1115                );
1116            }
1117        }
1118        let map = |on: CoedgeOn, edges: &[Option<usize>], vertices: &[Option<usize>]| match on {
1119            CoedgeOn::Edge(e, sense) => CoedgeOn::Edge(edges[e].unwrap(), sense),
1120            CoedgeOn::Vertex(v) => CoedgeOn::Vertex(vertices[v].unwrap()),
1121        };
1122        let inner_vertex = &inner_vertex;
1123
1124        let mut outer_faces = Vec::new();
1125        let mut inner_faces = Vec::new();
1126        for (f, face) in spec.faces.iter().enumerate() {
1127            let name = &self.names.faces[f];
1128            if self.smooth[f] {
1129                continue;
1130            }
1131            if self.removed[f] {
1132                let rims = self.rims(f, &at)?;
1133                let several = rims.len() > 1;
1134                for (k, rim) in rims.into_iter().enumerate() {
1135                    outer_faces.push(out.faces.len());
1136                    out.faces.push(rim);
1137                    names.faces.push(if several {
1138                        namer.name(&[name, &k.to_string()])
1139                    } else {
1140                        namer.name(&[name])
1141                    });
1142                }
1143                continue;
1144            }
1145            let remap = |lp: &Vec<CoedgeSpec>| -> Vec<CoedgeSpec> {
1146                lp.iter()
1147                    .map(|c| CoedgeSpec {
1148                        on: map(c.on, &at.edge, &at.vertex),
1149                        pcurve: c.pcurve.clone(),
1150                    })
1151                    .collect()
1152            };
1153            outer_faces.push(out.faces.len());
1154            out.faces.push(FaceSpec {
1155                surface: face.surface.clone(),
1156                outer: remap(&face.outer),
1157                holes: face.holes.iter().map(remap).collect(),
1158            });
1159            names.faces.push(name.clone());
1160
1161            let Some(copy) = self.copies.face[f] else {
1162                continue;
1163            };
1164            let copy = &self.inner.faces[copy];
1165            let inner_loop = |lp: &Vec<CoedgeSpec>| -> Vec<CoedgeSpec> {
1166                lp.iter()
1167                    .map(|c| CoedgeSpec {
1168                        on: map(c.on, &at.inner_edge, &at.inner_vertex),
1169                        pcurve: c.pcurve.clone(),
1170                    })
1171                    .collect()
1172            };
1173            let inner = FaceSpec {
1174                surface: copy.surface.clone(),
1175                outer: inner_loop(&copy.outer),
1176                holes: copy.holes.iter().map(inner_loop).collect(),
1177            };
1178            inner_faces.push(out.faces.len());
1179            out.faces.push(inner.reversed());
1180            names.faces.push(inner_name(name));
1181        }
1182        // Along every free edge, a wall from the edge to its inner copy,
1183        // with a straight edge from each of its vertices to that vertex's
1184        // inner copy, `side[v]`.
1185        let mut side: Vec<Option<usize>> = vec![None; spec.vertices.len()];
1186        let mut walls = Vec::new();
1187        for (here, c) in self.coedges() {
1188            let CoedgeOn::Edge(e, sense) = c.on else {
1189                continue;
1190            };
1191            if !self.free_edge[e] || self.removed[here.f] {
1192                continue;
1193            }
1194            let (x, y) = spec.ends(c.on)?;
1195            for v in [x, y] {
1196                if side[v].is_none() {
1197                    side[v] = Some(out.edges.len());
1198                    out.edges.push(EdgeSpec {
1199                        curve: NurbCurve3D::line(spec.vertices[v], self.inner_point(v))?,
1200                        start: at.vertex[v].unwrap(),
1201                        end: inner_vertex[v].unwrap(),
1202                    });
1203                    names
1204                        .edges
1205                        .push(namer.name(&[&self.names.vertices[v], "side"]));
1206                }
1207            }
1208            let outer = sense.orient(&spec.edges[e].curve);
1209            let inner = sense.orient(&self.inner_curve(e).expect("a free edge has a copy"));
1210            let (copy, along) = self.copies.edge[e].expect("a free edge has a copy");
1211            let copy_sense = if along == Sense::Forward {
1212                sense
1213            } else {
1214                sense.opposite()
1215            };
1216            // From the inner copy, at `v = 0`, out to the edge, at `v = 1`:
1217            // running round it counter-clockwise, the wall runs the edge
1218            // the other way than its face does, and faces away from it.
1219            let lp = [
1220                CoedgeOn::Edge(at.inner_edge[copy].unwrap(), copy_sense),
1221                CoedgeOn::Edge(side[y].unwrap(), Sense::Reversed),
1222                CoedgeOn::Edge(at.edge[e].unwrap(), sense.opposite()),
1223                CoedgeOn::Edge(side[x].unwrap(), Sense::Forward),
1224            ];
1225            walls.push(out.faces.len());
1226            let surface = NurbSurface3D::ruled(&inner, &outer).map_err(|err| {
1227                err.with_context(format!("the wall along edge {}", self.names.edges[e]))
1228            })?;
1229            out.faces.push(FaceSpec::on_unit_square(surface, lp)?);
1230            names
1231                .faces
1232                .push(namer.name(&[&self.names.edges[e], "side"]));
1233        }
1234        out.shells = if self.removed.iter().any(|&r| r) || !walls.is_empty() {
1235            vec![
1236                outer_faces
1237                    .into_iter()
1238                    .chain(inner_faces)
1239                    .chain(walls)
1240                    .collect(),
1241            ]
1242        } else {
1243            vec![outer_faces, inner_faces]
1244        };
1245        Ok((out, names))
1246    }
1247
1248    /// The inner side alone, as a sheet of its own: the inner copy of every
1249    /// face, facing the way the face does, and of every edge and vertex,
1250    /// each copy of `X` named `N(X)` for `namer`'s `N` — of the first `X`
1251    /// where several share one.
1252    fn inner_side(&self, namer: &Namer) -> GeopResult<(BodySpec, BodyNames)> {
1253        let mut out = self.inner.clone();
1254        out.solid = false;
1255        out.shells = vec![(0..out.faces.len()).collect()];
1256        let names = BodyNames {
1257            vertices: self
1258                .copies
1259                .names
1260                .vertices
1261                .iter()
1262                .map(|n| namer.name(&[n]))
1263                .collect(),
1264            edges: self
1265                .copies
1266                .names
1267                .edges
1268                .iter()
1269                .map(|n| namer.name(&[n]))
1270                .collect(),
1271            faces: self
1272                .copies
1273                .names
1274                .faces
1275                .iter()
1276                .map(|n| namer.name(&[n]))
1277                .collect(),
1278            solid: None,
1279        };
1280        Ok((out, names))
1281    }
1282
1283    /// What is left of the face `f`, taken away: `f` without its inner copy
1284    /// (see the module docs), as one face or several, the result's entities
1285    /// numbered `at`.
1286    fn rims(&self, f: usize, at: &Numbering) -> GeopResult<Vec<FaceSpec>> {
1287        let spec = self.spec;
1288        let vertex = &at.vertex;
1289        let inner_vertex: Vec<Option<usize>> = (0..spec.vertices.len())
1290            .map(|v| at.inner_vertex[self.copies.vertex[v]])
1291            .collect();
1292        let surface = &spec.faces[f].surface;
1293        let unsupported = |v: usize| {
1294            GeopError::new(format!(
1295                "face {} is taken away at vertex {}, where its surface comes to a point: not supported",
1296                self.names.faces[f], self.names.vertices[v]
1297            ))
1298        };
1299        // Every coedge of the rim, with the result's vertices it runs from
1300        // and to.
1301        let mut coedges: Vec<(CoedgeSpec, usize, usize)> = Vec::new();
1302        for (here, c) in self.coedges().filter(|(here, _)| here.f == f) {
1303            let CoedgeOn::Edge(e, sense) = c.on else {
1304                let CoedgeOn::Vertex(v) = c.on else {
1305                    unreachable!()
1306                };
1307                if self.walled[v] {
1308                    return Err(unsupported(v));
1309                }
1310                continue;
1311            };
1312            let (x, y) = spec.ends(c.on)?;
1313            if !self.open_edge[e] {
1314                // `f`'s own boundary where a wall stands on it...
1315                coedges.push((
1316                    CoedgeSpec {
1317                        on: CoedgeOn::Edge(at.edge[e].unwrap(), sense),
1318                        pcurve: c.pcurve.clone(),
1319                    },
1320                    vertex[x].unwrap(),
1321                    vertex[y].unwrap(),
1322                ));
1323                // ...and its inner copy, the other way, where it has one.
1324                if let Some(inner) = self.inner_coedge(here) {
1325                    let CoedgeOn::Edge(copy, copy_sense) = inner.on else {
1326                        unreachable!("the copy of an edge");
1327                    };
1328                    let (from, to) = self.inner.ends(inner.on)?;
1329                    coedges.push((
1330                        CoedgeSpec {
1331                            on: CoedgeOn::Edge(at.inner_edge[copy].unwrap(), copy_sense.opposite()),
1332                            pcurve: inner.pcurve.reverse(),
1333                        },
1334                        at.inner_vertex[to].unwrap(),
1335                        at.inner_vertex[from].unwrap(),
1336                    ));
1337                }
1338                continue;
1339            }
1340            // An edge to another face taken away: what is left of it, from
1341            // `x` to its inner copy and from `y`'s to `y`.
1342            let (orig_start, orig_end) = c.pcurve.ends()?;
1343            let (start_piece, end_piece) = in_order(at.pieces[e], sense);
1344            let curves = &self.pieces[e];
1345            let (start_curve, end_curve) = in_order((&curves.0, &curves.1), sense);
1346            if let (Some(piece), Some(curve)) = (start_piece, start_curve) {
1347                let to = self.inner_end(self.previous(here))?;
1348                let curve = sense.orient(curve);
1349                coedges.push((
1350                    CoedgeSpec {
1351                        on: CoedgeOn::Edge(piece, sense),
1352                        pcurve: pcurve(surface, &c.pcurve, Some(&curve), orig_start, to)?,
1353                    },
1354                    vertex[x].unwrap(),
1355                    inner_vertex[x].unwrap(),
1356                ));
1357            }
1358            if let (Some(piece), Some(curve)) = (end_piece, end_curve) {
1359                let from = self.inner_end(here)?;
1360                let curve = sense.orient(curve);
1361                coedges.push((
1362                    CoedgeSpec {
1363                        on: CoedgeOn::Edge(piece, sense),
1364                        pcurve: pcurve(surface, &c.pcurve, Some(&curve), from, orig_end)?,
1365                    },
1366                    inner_vertex[y].unwrap(),
1367                    vertex[y].unwrap(),
1368                ));
1369            }
1370        }
1371
1372        // Follow the coedges end to end into loops.
1373        let mut starting: HashMap<usize, usize> = HashMap::new();
1374        for (i, (_, start, _)) in coedges.iter().enumerate() {
1375            if starting.insert(*start, i).is_some() {
1376                return Err(GeopError::new(format!(
1377                    "what is left of face {} touches itself at a vertex: not supported",
1378                    self.names.faces[f]
1379                )));
1380            }
1381        }
1382        let mut used = vec![false; coedges.len()];
1383        let mut loops: Vec<Vec<usize>> = Vec::new();
1384        for first in 0..coedges.len() {
1385            if used[first] {
1386                continue;
1387            }
1388            // On from each coedge to the one starting where it ends. One
1389            // running into a loop already followed runs round that loop
1390            // into itself.
1391            let around = walk(first, |&i| {
1392                let next = starting.get(&coedges[i].2).ok_or_else(|| {
1393                    GeopError::new(format!(
1394                        "what is left of face {} does not close up",
1395                        self.names.faces[f]
1396                    ))
1397                })?;
1398                Ok::<_, GeopError>(Some(*next))
1399            })?;
1400            if around.end != Walked::Closed {
1401                return Err(GeopError::new(format!(
1402                    "what is left of face {} runs into itself",
1403                    self.names.faces[f]
1404                )));
1405            }
1406            for &i in &around.nodes {
1407                used[i] = true;
1408            }
1409            loops.push(around.nodes);
1410        }
1411
1412        // Outer loops run counter-clockwise in `(u, v)`, holes clockwise;
1413        // each hole lies in one outer loop.
1414        let mut outers: Vec<(Vec<usize>, Scalar)> = Vec::new();
1415        let mut holes: Vec<Vec<usize>> = Vec::new();
1416        for lp in loops {
1417            let area = loop_signed_area(lp.iter().map(|&i| &coedges[i].0.pcurve))?;
1418            if area.definitely_greater(Scalar::ZERO) {
1419                outers.push((lp, area));
1420            } else if area.definitely_less(Scalar::ZERO) {
1421                holes.push(lp);
1422            } else {
1423                return Err(GeopError::new(format!(
1424                    "cannot tell which way a loop of what is left of face {} runs",
1425                    self.names.faces[f]
1426                )));
1427            }
1428        }
1429        let mut faces: Vec<FaceSpec> = outers
1430            .iter()
1431            .map(|(lp, _)| FaceSpec {
1432                surface: surface.clone(),
1433                outer: lp.iter().map(|&i| coedges[i].0.clone()).collect(),
1434                holes: Vec::new(),
1435            })
1436            .collect();
1437        // A part of the rim may lie inside another's outer loop — the ring
1438        // around a hole inside the ring around the walls — so a hole is the
1439        // innermost one's: of the outer loops around it, the smallest. Which
1440        // one a hole lies in is asked of a point in the middle of one of its
1441        // pcurves, by ray casting against the outer loop's pcurves.
1442        for lp in &holes {
1443            let first = &coedges[lp[0]].0.pcurve;
1444            let point = first.evaluate(first.mid_parameter()?)?;
1445            let mut owner: Option<usize> = None;
1446            for (i, (outer, area)) in outers.iter().enumerate() {
1447                let pcurves: Vec<&Curve2> = outer.iter().map(|&k| &coedges[k].0.pcurve).collect();
1448                let inside = match pcurves_contain(
1449                    surface,
1450                    &pcurves,
1451                    point,
1452                    MAX_NODES,
1453                    min_subdivision_size(),
1454                    SEED,
1455                )? {
1456                    PointClassification::Inside => true,
1457                    PointClassification::Outside => false,
1458                    PointClassification::OnVertex | PointClassification::OnCoedge => {
1459                        return Err(GeopError::new(format!(
1460                            "a hole of what is left of face {} meets one of its parts' outer loops",
1461                            self.names.faces[f]
1462                        )));
1463                    }
1464                };
1465                if inside && owner.is_none_or(|o| area.definitely_less(outers[o].1)) {
1466                    owner = Some(i);
1467                }
1468            }
1469            let owner = owner.ok_or_else(|| {
1470                GeopError::new(format!(
1471                    "a hole of what is left of face {} lies in none of its parts",
1472                    self.names.faces[f]
1473                ))
1474            })?;
1475            faces[owner]
1476                .holes
1477                .push(lp.iter().map(|&i| coedges[i].0.clone()).collect());
1478        }
1479        Ok(faces)
1480    }
1481}
1482
1483/// The first of the faces `faces`, indices into `surfaces`, whose surface
1484/// `curve` does not lie on all along, with a point that shows it: the
1485/// middle of the first stretch of the curve that is off the surface. `None`
1486/// if the curve could lie on all of them. A curve built for an edge between
1487/// moved faces is only right if it does.
1488///
1489/// Whether it lies on a surface is what [`curve_surface_overlaps`] finds:
1490/// a stretch of it on the surface runs between two points where the curve
1491/// or the surface ends, so all of the curve lies on it if one stretch runs
1492/// from the curve's start to its end.
1493pub fn surface_missed(
1494    curve: &NurbCurve3D,
1495    faces: impl IntoIterator<Item = usize>,
1496    surfaces: &[NurbSurface3D],
1497) -> GeopResult<Option<(usize, Vector3)>> {
1498    let (t0, t1) = curve.domain();
1499    for f in faces {
1500        let overlaps = curve_surface_overlaps(
1501            curve,
1502            &surfaces[f],
1503            Scalar::ZERO,
1504            MAX_NODES,
1505            min_subdivision_size(),
1506        )?;
1507        if overlaps
1508            .iter()
1509            .any(|o| o.start.t.could_be_equal(t0) && o.end.t.could_be_equal(t1))
1510        {
1511            continue;
1512        }
1513        // The first stretch off it: up to the first overlap, or after it.
1514        let (from, to) = match overlaps.first() {
1515            Some(o) if o.start.t.could_be_equal(t0) => (
1516                o.end.t.upper(),
1517                overlaps.get(1).map_or(t1, |n| n.start.t.lower()),
1518            ),
1519            Some(o) => (t0, o.start.t.lower()),
1520            None => (t0, t1),
1521        };
1522        let off = curve.evaluate(from.add(to).div(Scalar::TWO)?.sharpen())?;
1523        return Ok(Some((f, off)));
1524    }
1525    Ok(None)
1526}
1527
1528/// The pcurve on `surface` of a coedge that runs along `curve` — or sits at
1529/// a vertex, without one — from `(u, v)` `start` to `end`. Exact where it
1530/// can be: on a surface affine in `(u, v)`, a plane, the curve mapped
1531/// ([`NurbSurface3D::affine_pcurve`]); and straight where the coedge it is
1532/// a copy of ran along an iso-line, `original`, and its copy does too —
1533/// the line keeps its `u` (or `v`) — as a cylinder's rim or seam does.
1534/// Fitted otherwise: a sphere's meridian cut by a plane is straight, but
1535/// its inner copy, where the offset plane cuts the offset sphere, is a
1536/// small circle no straight pcurve follows.
1537pub fn pcurve(
1538    surface: &NurbSurface3D,
1539    original: &Curve2,
1540    curve: Option<&NurbCurve3D>,
1541    start: Vector2,
1542    end: Vector2,
1543) -> GeopResult<Curve2> {
1544    let Some(curve) = curve else {
1545        return NurbCurve2D::line(start, end);
1546    };
1547    if let Some(mut mapped) = surface.affine_pcurve(curve)? {
1548        // Its ends and the junctions' `(u, v)` both enclose the ends' foot
1549        // points: so does what they share.
1550        let (a, b) = mapped.ends()?;
1551        if a.could_be_equal(&start) && b.could_be_equal(&end) {
1552            let last = mapped.control_points.len() - 1;
1553            for (k, at, given) in [(0, a, start), (last, b, end)] {
1554                let w = mapped.control_points[k][2];
1555                let pinned = |c: usize| {
1556                    let x = at[c].intersect(given[c]);
1557                    if w.is_exactly(Scalar::ONE) {
1558                        x
1559                    } else {
1560                        x.mul(w)
1561                    }
1562                };
1563                mapped.control_points[k] = Vector3::from_array([pinned(0), pinned(1), w]);
1564            }
1565            return Ok(mapped);
1566        }
1567    }
1568    let straight = original.degree == 1 && original.control_points.len() == 2 && {
1569        let (a, b) = original.ends()?;
1570        let keeps = |k: usize| a[k].could_be_equal(b[k]) && start[k].could_be_equal(end[k]);
1571        keeps(0) || keeps(1)
1572    };
1573    if straight {
1574        return NurbCurve2D::line(start, end);
1575    }
1576    surface.fit_pcurve(
1577        curve,
1578        Some(start),
1579        Some(end),
1580        MAX_NODES,
1581        min_subdivision_size(),
1582    )
1583}
1584
1585#[cfg(test)]
1586mod tests;