Skip to main content

geop_lib_shell/
rebuild.rs

1//! [`rebuilt`]: a solid rebuilt face by face — some of its faces on new
2//! surfaces, every vertex they reach moved to where the surfaces around it
3//! now meet, every edge they reach to where its two faces now meet.
4//!
5//! A draft, a face moved or offset in place, a face replaced, a face
6//! deleted and its neighbours grown over the gap: each changes the
7//! surfaces some faces lie on and keeps (or, deleting, edits first) the
8//! topology. What follows from the new surfaces is the same every time, and
9//! is done here, for any NURBS surfaces:
10//!
11//! - **Room.** A face's boundary may move past where its patch ends, so
12//!   every surface touched gets room around it ([`extended`]).
13//! - **Vertices.** A vertex moves to the point nearest where it was that
14//!   lies on the smooth surfaces around it ([`meet`]), one condition per
15//!   smooth surface ([`tangent_groups`]). Where only two smooth surfaces
16//!   meet, the vertex is the end of an edge lying within one of them — a
17//!   seam between two pieces of it — and it stays on that edge's curve: the
18//!   curve, carried on past its ends, crossing the other surface.
19//! - **Edges.** An edge between two smooth surfaces is traced where they
20//!   meet between its moved vertices, following the edge it was: each
21//!   point of it where the surfaces meet in the plane square to the old
22//!   edge there, the old point shifted with its vertices as the seed, and
23//!   a cubic through them widened to enclose the curve between them. A
24//!   straight edge that stays on both surfaces is kept straight. An edge
25//!   within one smooth surface, a seam, is its old curve carried on or cut
26//!   back to its moved ends.
27//! - **Pcurves.** Every coedge that changed is fitted anew on its face's
28//!   surface between `(u, v)` worked out once per junction, so the coedges
29//!   meeting there agree on it exactly.
30//!
31//! Every curve built is checked to lie on its faces' surfaces, and a curve
32//! traced to keep running the way the old edge ran: an edge that turns
33//! around — a face moved so far that a neighbour vanishes, or two of them
34//! now cross — is refused, by name, as is a vertex whose surfaces no longer
35//! meet near it. The topology never changes here.
36
37use geop_core_geometry::{
38    intersection::{Intersections, crossing_points, curve_curve_intersect},
39    meet::{Offset, meet, tangent_groups},
40    nurb_curve::{NurbCurve, NurbCurve3D, true_point_fractions},
41    nurb_surface::NurbSurface3D,
42    search::{MAX_NODES, PROJECT_ITERATIONS, min_subdivision_size},
43};
44use geop_core_math::{geop_error::WithContext, with_context};
45use geop_core_math::{
46    geop_error::{GeopError, GeopResult},
47    scalars::Scalar,
48    solvers::refine::{Refine, Refined, refine},
49    vector::{Vector2, Vector3},
50};
51use geop_core_topology::{
52    Curve2, Curve3, Sense,
53    build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec, FacesAround},
54    contains::face::{PointClassification, pcurves_contain},
55};
56use geop_core_topology::{FaceId, SolidId};
57use geop_ops_model::{BodyNames, NamedModel, Namer};
58
59use crate::shell::{pcurve, surface_missed};
60
61/// The legs a traced edge is fitted over at first, and at most: doubled
62/// while the fit's enclosure is wider than [`TRACE_DRIFT`].
63const FIRST_LEGS: usize = 8;
64const MOST_LEGS: usize = 256;
65
66/// How wide a traced edge's enclosure may come out before its legs are
67/// split finer: how fast the fit converges, not what it means — the fit
68/// encloses the curve between its points at every resolution.
69const TRACE_DRIFT: f64 = 1e-5;
70
71/// Seed of the ray casting that tells whether a hole lies inside its
72/// face's outer loop: any gives the same answer (see `pcurves_contain`).
73const SEED: u64 = 0x2EB0_0000_0000_0001;
74
75/// Whether [`replace_faces`] takes along the faces a face it puts on a new
76/// surface runs smoothly into, or refuses.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum Smooth {
79    Refuse,
80    Follow,
81}
82
83/// Puts the faces `faces` of one solid of `part` on new surfaces —
84/// `surface(name, old)` gives the one for the face named `name`, on the
85/// surface `old` — and rebuilds the solid around them ([`rebuilt`]): the
86/// result replaces it, named `namer`'s root, and every face, edge and
87/// vertex keeps its name. What a draft, a face moved, offset or replaced
88/// do. A new surface's normal points out of the solid, as the old one's
89/// did.
90///
91/// A face that runs smoothly into a neighbour — a piece of a cylinder
92/// beside the next, a wall into its fillet — takes it along if `smooth`
93/// says to follow: the faces the new surfaces are made for are then every
94/// face the picked ones run smoothly into, on and on. Otherwise that is
95/// refused (see [`tangent_to_kept`]).
96///
97/// Refused, by name: faces of several solids, or of none.
98pub fn replace_faces(
99    part: &mut NamedModel,
100    namer: &Namer,
101    faces: &[FaceId],
102    smooth: Smooth,
103    mut surface: impl FnMut(&str, &NurbSurface3D) -> GeopResult<NurbSurface3D>,
104) -> GeopResult<SolidId> {
105    let ctx = with_context!("replace_faces({}, faces={faces:?})", namer.root());
106    let Some(&first) = faces.first() else {
107        return Err(GeopError::new("pick the faces")).with_context(ctx);
108    };
109    let solid = part
110        .solid_of_face(first, "only faces of a solid can be moved")
111        .with_context(ctx)?;
112    let model = part.topology();
113    let all = model.solid_faces(solid).with_context(ctx)?;
114    if let Some(&f) = faces.iter().find(|f| !all.contains(f)) {
115        return Err(GeopError::new(format!(
116            "face {} is not of the same solid as face {}: pick the faces of one solid at a time",
117            part.name(f)?,
118            part.name(first)?
119        )))
120        .with_context(ctx);
121    }
122    let (mut spec, sources) = model.body_spec(&all, true).with_context(ctx)?;
123    let names = BodyNames {
124        solid: Some(namer.root()),
125        ..part.source_names(&sources)?
126    };
127    let mut replaced: Vec<bool> = sources.faces.iter().map(|f| faces.contains(f)).collect();
128    while let Some((f, g, e)) = tangent_to_kept(&spec, &replaced).with_context(ctx)? {
129        if smooth == Smooth::Follow {
130            replaced[g] = true;
131            continue;
132        }
133        return Err(GeopError::new(format!(
134            "face {} meets face {} tangentially along edge {} — a fillet or a smooth blend: moving a face next to one, without it, is not supported",
135            names.faces[f], names.faces[g], names.edges[e]
136        )))
137        .with_context(ctx);
138    }
139    for (f, face) in spec.faces.iter_mut().enumerate() {
140        if replaced[f] {
141            let roomy = room(face.surface.clone()).with_context(ctx)?;
142            face.surface = surface(&names.faces[f], &roomy).with_context(ctx)?;
143        }
144    }
145    let reseat = vec![false; spec.vertices.len()];
146    let result = rebuilt(&spec, &names, &replaced, &reseat).with_context(ctx)?;
147    part.replace_with_solid(&[solid.into()], result, names)
148        .with_context(ctx)
149}
150
151/// The body `spec` rebuilt: the faces marked `replaced` lie on the
152/// surfaces `spec` gives them now — made from the old ones given room
153/// first ([`room`]): a fitted offset given room after, its end span of
154/// many carried on, is extrapolated and widened, and an exact one given
155/// room before and after is widened twice — every vertex they reach — and every
156/// vertex marked `reseat`, at the point `spec` gives as a seed — moves to
157/// where the surfaces around it meet, every edge they reach to where its
158/// faces' surfaces meet (see the module docs). `spec`'s edge curves are
159/// where the edges were: the guides the new ones follow. `names` name its
160/// entities, for the errors.
161pub fn rebuilt(
162    spec: &BodySpec,
163    names: &BodyNames,
164    replaced: &[bool],
165    reseat: &[bool],
166) -> GeopResult<BodySpec> {
167    Rebuild::new(spec, names, replaced, reseat)?.result()
168}
169
170/// The body `spec` with its vertices moved as [`rebuilt`] moves them, its
171/// edges not yet rebuilt: what a caller asks of before it rebuilds them,
172/// whether an edge turns around ([`Placed::turned`]).
173pub(crate) struct Placed<'a>(Rebuild<'a>);
174
175impl<'a> Placed<'a> {
176    /// The vertices of `spec` moved (see [`rebuilt`]).
177    pub(crate) fn new(
178        spec: &'a BodySpec,
179        names: &'a BodyNames,
180        replaced: &'a [bool],
181        reseat: &[bool],
182    ) -> GeopResult<Self> {
183        Rebuild::new(spec, names, replaced, reseat).map(Self)
184    }
185
186    /// The edges whose moved ends lie the other way round than they did:
187    /// moved this far, the edge shrank through nothing, and a face it
188    /// bounds may vanish with it. Decided from the ends alone, against the
189    /// chord they had: an edge between two moved planes is parallel to
190    /// what it was, so its ends swap exactly when it turns around. A closed
191    /// edge, its ends one vertex, never does. One that could have shrunk to
192    /// a point exactly is not counted: [`rebuilt`] refuses it by name.
193    pub(crate) fn turned(&self) -> Vec<usize> {
194        let rebuild = &self.0;
195        (rebuild.spec.edges.iter().enumerate())
196            .filter(|(_, edge)| {
197                edge.start != edge.end && (rebuild.moved[edge.start] || rebuild.moved[edge.end])
198            })
199            .filter(|(_, edge)| {
200                let was = rebuild.spec.vertices[edge.end].sub(&rebuild.spec.vertices[edge.start]);
201                let now = rebuild.points[edge.end].sub(&rebuild.points[edge.start]);
202                now.prod_dot(&was).definitely_less(Scalar::ZERO)
203            })
204            .map(|(e, _)| e)
205            .collect()
206    }
207
208    /// The body rebuilt (see [`rebuilt`]).
209    pub(crate) fn rebuilt(self) -> GeopResult<BodySpec> {
210        self.0.result()
211    }
212}
213
214/// A face to be put on another surface, `f`, that meets a face kept, `g`,
215/// tangentially along the edge `e` — a fillet or a smooth blend along it —
216/// if there is one: `(f, g, e)`. Asked of `spec` before the surfaces change.
217/// A face kept that runs smoothly into one that moves would have to move
218/// with it, which [`rebuilt`] does not do: they would no longer meet along
219/// the edge, or cross where they should touch.
220pub fn tangent_to_kept(
221    spec: &BodySpec,
222    replaced: &[bool],
223) -> GeopResult<Option<(usize, usize, usize)>> {
224    for (e, uses) in spec.edge_uses().iter().enumerate() {
225        let (f, g) = match uses.as_slice() {
226            [(f, ..), (g, ..)] if replaced[*f] && !replaced[*g] => (*f, *g),
227            [(g, ..), (f, ..)] if replaced[*f] && !replaced[*g] => (*f, *g),
228            _ => continue,
229        };
230        if smooth_along(spec, &spec.edges[e], f, g)? {
231            return Ok(Some((f, g, e)));
232        }
233    }
234    Ok(None)
235}
236
237/// Whether the faces `f` and `g` of `spec` are pieces of one smooth surface
238/// along `edge`: their normals halfway along it could be parallel.
239pub(crate) fn smooth_along(
240    spec: &BodySpec,
241    edge: &EdgeSpec,
242    f: usize,
243    g: usize,
244) -> GeopResult<bool> {
245    let t = edge.curve.mid_parameter()?;
246    let point = edge.curve.evaluate(t)?;
247    let normal = |f: usize| {
248        spec.faces[f]
249            .surface
250            .normal_at(&point, MAX_NODES, min_subdivision_size())
251    };
252    Ok(normal(f)?
253        .prod_cross(&normal(g)?)
254        .norm_sq()
255        .could_be_equal(Scalar::ZERO))
256}
257
258/// The body described as a [`BodySpec`], and where rebuilding it moves what.
259struct Rebuild<'a> {
260    spec: &'a BodySpec,
261    names: &'a BodyNames,
262    replaced: &'a [bool],
263    /// Per face: the surface it lies on afterwards, with room around it
264    /// wherever its boundary may move.
265    surfaces: Vec<NurbSurface3D>,
266    /// Per face: whether a vertex of it moves, or it is replaced.
267    touched: Vec<bool>,
268    /// Per edge: the curve it lies on, carried on past its ends, if it is a
269    /// seam within one smooth surface that changes (see [`Rebuild::seam`]).
270    seams: Vec<Option<Curve3>>,
271    /// Per vertex: whether it moves, and where to.
272    moved: Vec<bool>,
273    points: Vec<Vector3>,
274    /// Per edge: its new curve, if it changes.
275    curves: Vec<Option<Curve3>>,
276}
277
278impl<'a> Rebuild<'a> {
279    fn new(
280        spec: &'a BodySpec,
281        names: &'a BodyNames,
282        replaced: &'a [bool],
283        reseat: &[bool],
284    ) -> GeopResult<Self> {
285        let around = spec.vertex_faces()?;
286        let moved: Vec<bool> = (around.iter().enumerate())
287            .map(|(v, faces)| reseat[v] || faces.iter().any(|&(f, _)| replaced[f]))
288            .collect();
289        let touched: Vec<bool> = (0..spec.faces.len())
290            .map(|f| {
291                replaced[f]
292                    || (around.iter().enumerate())
293                        .any(|(v, faces)| moved[v] && faces.iter().any(|&(g, _)| g == f))
294            })
295            .collect();
296        // A face put on a new surface came with room: the new surface was
297        // made from the old with room around it (see `room`), not given
298        // room after.
299        let surfaces = (spec.faces.iter().zip(&touched).zip(replaced))
300            .map(|((face, &touched), &replaced)| {
301                if touched && !replaced {
302                    room(face.surface.clone())
303                } else {
304                    Ok(face.surface.clone())
305                }
306            })
307            .collect::<GeopResult<Vec<_>>>()?;
308        let mut rebuild = Self {
309            spec,
310            names,
311            replaced,
312            surfaces,
313            touched,
314            seams: Vec::new(),
315            moved,
316            points: spec.vertices.clone(),
317            curves: Vec::new(),
318        };
319        let along: Vec<Vec<(usize, Sense, &Curve2)>> = spec.edge_uses();
320        rebuild.seams = (0..spec.edges.len())
321            .map(|e| {
322                rebuild
323                    .seam(e, &along[e])
324                    .map_err(|err| err.with_context(format!("edge {}", names.edges[e])))
325            })
326            .collect::<GeopResult<_>>()?;
327        for (v, faces) in around.iter().enumerate() {
328            if rebuild.moved[v] {
329                rebuild.points[v] = rebuild
330                    .vertex(v, faces)
331                    .map_err(|e| e.with_context(format!("moving vertex {}", names.vertices[v])))?;
332            }
333        }
334        Ok(rebuild)
335    }
336
337    /// Where the vertex `v`, with the faces `faces` around it, goes (see the
338    /// module docs).
339    fn vertex(&self, v: usize, faces: &FacesAround) -> GeopResult<Vector3> {
340        let seed = self.spec.vertices[v];
341        let mut pieces = Vec::with_capacity(faces.len());
342        for &(f, uv) in faces {
343            let surface = &self.surfaces[f];
344            // Where it was on the face, if the new surface still has a pole
345            // there, or else its foot point on the new surface.
346            let at_pole = surface.pole_at(uv[0], uv[1]).is_some();
347            let (u, w) = if self.replaced[f] && !at_pole {
348                let (u, w) = surface
349                    .project_in_domain(seed, uv[0].sharpen(), uv[1].sharpen(), PROJECT_ITERATIONS)
350                    .map_err(|e| {
351                        e.with_context(format!(
352                            "projecting {seed:?} onto the new surface of face {} from {uv:?}",
353                            self.names.faces[f]
354                        ))
355                    })?;
356                (u.sharpen(), w.sharpen())
357            } else {
358                (uv[0], uv[1])
359            };
360            pieces.push((surface, (u, w)));
361        }
362        // A vertex at a pole of its faces — on the axis of a revolved face —
363        // is the pole of their new surfaces, where those are one point: the
364        // topology puts it there. Poles apart are coordinates, not where
365        // the faces meet — the walls of a pyramid, each ruled into its tip,
366        // offset: their poles move each along its own normal. A face with a
367        // pole among faces without — a rounded box's corner patch at the
368        // blends' ends — is no such vertex either. Then the surfaces say
369        // where it goes, and the conditions below read a pole's normal as
370        // its limit.
371        let poles: Vec<_> = (pieces.iter())
372            .map(|(s, (u, w))| s.pole_at(*u, *w))
373            .collect();
374        if let Some(pole) = poles.iter().flatten().next()
375            && poles
376                .iter()
377                .all(|p| p.is_some_and(|p| p.could_be_equal(pole)))
378        {
379            return Ok(*pole);
380        }
381        let groups = tangent_groups(&pieces)
382            .map_err(|e| e.with_context(format!("grouping its faces' surfaces at {pieces:?}")))?;
383        // On the border of a sheet, where an edge bounds one face only,
384        // nothing but the surfaces holds the vertex: where they meet nearest
385        // to where it was is as good as any point of them, a free choice.
386        let on_border = (self.spec.edge_uses().iter().zip(&self.spec.edges))
387            .any(|(uses, edge)| uses.len() == 1 && (edge.start == v || edge.end == v));
388        let fewer = |determined: GeopResult<Vector3>| match determined {
389            Err(_) if on_border => nearest(seed, &pieces, &groups),
390            determined => determined,
391        };
392        match groups.len() {
393            0 | 1 => fewer(self.within(faces)),
394            2 => fewer(self.on_seam(v, faces, &pieces, &groups)),
395            // Three surfaces meet in a point; four or more only where they
396            // happen to. Elsewhere the vertex would open into an edge,
397            // which the rebuild does not do: refused by its faces' names.
398            n => nearest(seed, &pieces, &groups).map_err(|e| {
399                let names: Vec<&str> = faces.iter().map(|&(f, _)| self.names.faces[f].as_str()).collect();
400                GeopError::new(format!(
401                    "the {n} smooth surfaces of faces {names:?} around it do not meet in one point: it would open into an edge, which is not supported ({})",
402                    e.root_message()
403                ))
404            }),
405        }
406        .map_err(|e| e.with_context(format!("where its {} smooth surfaces meet", groups.len())))
407    }
408
409    /// Where a vertex goes that lies within one smooth surface, its faces
410    /// `faces` — pieces of it, where seams cross — all put on new surfaces that
411    /// keep their parametrization, as a shift or an offset does: where each
412    /// is at the vertex's `(u, v)`, all enclosures of one point, so their
413    /// intersection is too. Not determined otherwise.
414    fn within(&self, faces: &FacesAround) -> GeopResult<Vector3> {
415        let mut point: Option<Vector3> = None;
416        for &(f, uv) in faces {
417            let here = self.surfaces[f].evaluate(uv[0], uv[1])?;
418            point = Some(match point {
419                None => here,
420                Some(p) if p.could_be_equal(&here) => {
421                    Vector3::from_array(std::array::from_fn(|k| p[k].intersect(here[k])))
422                }
423                Some(p) => {
424                    return Err(GeopError::new(format!(
425                        "it lies within one smooth surface, whose pieces now put it at {p:?} and at {here:?}: where it goes is not determined"
426                    )));
427                }
428            });
429        }
430        point.ok_or_else(|| GeopError::new("no face meets there"))
431    }
432
433    /// Where the vertex `v` goes where only two smooth surfaces meet, the
434    /// `groups` of `pieces`, the surfaces of `faces` around it: on the
435    /// curve of an edge from it within one of them, a seam that stays,
436    /// where that curve, carried on past its ends, crosses the other.
437    fn on_seam(
438        &self,
439        v: usize,
440        faces: &FacesAround,
441        pieces: &[(&NurbSurface3D, (Scalar, Scalar))],
442        groups: &[Vec<usize>],
443    ) -> GeopResult<Vector3> {
444        let group_of = |f: usize| {
445            let i = faces.iter().position(|&(g, _)| g == f)?;
446            groups.iter().position(|group| group.contains(&i))
447        };
448        let uses = self.spec.edge_uses();
449        let seams: Vec<(usize, &Curve3, usize)> = (self.spec.edges.iter().enumerate())
450            .filter(|(_, edge)| edge.start == v || edge.end == v)
451            .filter_map(|(e, _)| {
452                let curve = self.seams[e].as_ref()?;
453                let [(f, ..), (g, ..)] = uses[e].as_slice() else {
454                    return None;
455                };
456                let group = group_of(*f)?;
457                (group_of(*g) == Some(group)).then_some((e, curve, group))
458            })
459            .collect();
460        let seed = self.spec.vertices[v];
461        // Every seam from it must cross the other surface at one point:
462        // seams of both surfaces meeting there, which a move takes apart,
463        // leave no vertex where both end.
464        let mut found: Option<(usize, Vector3)> = None;
465        for &(e, curve, group) in &seams {
466            let point = self.crossing(curve, &groups[1 - group], pieces, seed)?;
467            match found {
468                Some((first, at)) if !at.could_be_equal(&point) => {
469                    return Err(GeopError::new(format!(
470                        "it ends the seams {} and {}, which no longer meet: they cross the other surface at {at:?} and at {point:?} — taking them apart is not supported",
471                        self.names.edges[first], self.names.edges[e]
472                    )));
473                }
474                Some(_) => {}
475                None => found = Some((e, point)),
476            }
477        }
478        found.map(|(_, p)| p).ok_or_else(|| {
479            GeopError::new(
480                "only two smooth surfaces meet there, and no edge from it runs within one of them: where it goes is not determined",
481            )
482        })
483    }
484
485    /// Where `curve`, a seam carried on, crosses the surface `other`, a
486    /// group of `pieces`, nearest to `seed`.
487    fn crossing(
488        &self,
489        curve: &Curve3,
490        other: &[usize],
491        pieces: &[(&NurbSurface3D, (Scalar, Scalar))],
492        seed: Vector3,
493    ) -> GeopResult<Vector3> {
494        let mut best: Option<(Vector3, f64)> = None;
495        for &i in other {
496            let (surface, _) = pieces[i];
497            let points = crossing_points(curve, surface).map_err(|e| {
498                e.with_context(format!("the seam from it crossing surface {i}, moved"))
499            })?;
500            for (point, _) in points {
501                let distance = point.sub(&seed).norm_sq().to_f64();
502                if best.is_none_or(|(_, d)| distance < d) {
503                    best = Some((point, distance));
504                }
505            }
506        }
507        best.map(|(p, _)| p).ok_or_else(|| {
508            GeopError::new(format!(
509                "the seam from it, carried on, does not cross the moved surface: the face around {seed:?} no longer reaches it"
510            ))
511        })
512    }
513
514    /// The new curve of `edge`, on the border of a sheet, bounding only
515    /// the face `f` of the use `(f, sense, pcurve)`: its pcurve there, its
516    /// ends shifted to where its moved vertices are on the face's surface,
517    /// on that surface — straight, where it was straight and that lies on
518    /// the surface.
519    fn border(
520        &self,
521        edge: &EdgeSpec,
522        (f, sense, pcurve): (usize, Sense, &Curve2),
523    ) -> GeopResult<Curve3> {
524        let (start, end) = (self.points[edge.start], self.points[edge.end]);
525        let surface = &self.surfaces[f];
526        let mut curve = None;
527        if edge.curve.degree == 1 && edge.curve.control_points.len() == 2 {
528            let line = NurbCurve3D::line(start, end)?;
529            if surface_missed(&line, [f], &self.surfaces)?.is_none() {
530                curve = Some(line);
531            }
532        }
533        let curve = match curve {
534            Some(line) => line,
535            None => {
536                // Each point of the edge carried to its foot point on the
537                // new surface, the way its ends were (see `Rebuild::vertex`):
538                // from the edge's own curve, not its pcurve, which is as
539                // wide as its fit to the curve — evaluated along it, a
540                // traced border's copy came out a hundred times wider than
541                // the border.
542                let along = match sense {
543                    Sense::Forward => pcurve.clone(),
544                    Sense::Reversed => pcurve.reverse(),
545                };
546                let (t0, t1) = edge.curve.domain();
547                let (p0, p1) = along.domain();
548                let at = |frac: Scalar| -> GeopResult<Vector3> {
549                    let point = edge
550                        .curve
551                        .evaluate(Scalar::interpolate(t0, t1, frac).sharpen())?;
552                    let seed = along.evaluate(Scalar::interpolate(p0, p1, frac).sharpen())?;
553                    let (u, v) = surface.project_in_domain(
554                        point,
555                        seed[0].sharpen(),
556                        seed[1].sharpen(),
557                        PROJECT_ITERATIONS,
558                    )?;
559                    surface.evaluate(u, v)
560                };
561                fitted(at, (start, end), |_| Ok(()))
562                    .map_err(|e| e.with_context("fitting it on its face's new surface"))?
563            }
564        };
565        if let Some((_, middle)) = surface_missed(&curve, [f], &self.surfaces)? {
566            return Err(GeopError::new(format!(
567                "its new curve from {start:?} to {end:?} is not on face {} at {middle:?}",
568                self.names.faces[f]
569            )));
570        }
571        Ok(curve)
572    }
573
574    /// The new curve of edge `e`, used by the faces `uses`, if it changes.
575    fn curve(&self, e: usize, uses: &[(usize, Sense, &Curve2)]) -> GeopResult<Option<Curve3>> {
576        let edge = &self.spec.edges[e];
577        let changed = self.moved[edge.start]
578            || self.moved[edge.end]
579            || uses.iter().any(|&(f, ..)| self.replaced[f]);
580        if !changed {
581            return Ok(None);
582        }
583        let (start, end) = (self.points[edge.start], self.points[edge.end]);
584        if edge.start != edge.end && start.could_be_equal(&end) {
585            return Err(GeopError::new(format!(
586                "moved this far, it shrinks to a point at {start:?}: a face it bounds would vanish"
587            )));
588        }
589        if let [border] = uses {
590            return self.border(edge, *border).map(Some);
591        }
592        let [(f, f_sense, f_pcurve), (g, g_sense, g_pcurve)] = uses else {
593            return Err(GeopError::new(format!(
594                "it bounds {} faces, not one or two: moving it is not supported",
595                uses.len()
596            )));
597        };
598        let faces = [*f, *g];
599        // Straight where it was, and the line between its moved ends lies
600        // on both faces.
601        let straight = (edge.curve.degree == 1 && edge.curve.control_points.len() == 2)
602            .then(|| NurbCurve3D::line(start, end))
603            .transpose()?
604            .filter(|line| {
605                matches!(surface_missed(line, faces, &self.surfaces), Ok(None))
606                    && turns_around(&edge.curve, &[start, end]).is_ok()
607            });
608        let curve = match (&self.seams[e], straight) {
609            // A seam that stays: its moved ends on it. One whose ends left it
610            // — two coplanar faces, the vertices at its ends put where other
611            // faces now cross — is any curve between them in the surface,
612            // and a straight one was straight.
613            (Some(seam), straight) => match (restricted(seam, start, end), straight) {
614                (Ok(along), _) => along,
615                (Err(_), Some(line)) => line,
616                (Err(err), None) => {
617                    return Err(err.with_context(format!(
618                        "it is a seam between faces {} and {}, pieces of one smooth surface",
619                        self.names.faces[*f], self.names.faces[*g]
620                    )));
621                }
622            },
623            (None, Some(line)) => line,
624            (None, None) => {
625                let uses = [(*f, f_sense, *f_pcurve), (*g, g_sense, *g_pcurve)];
626                match self.on_iso_line(edge, start, end, uses)? {
627                    Some(iso) => iso,
628                    None => self.traced(&edge.curve, start, end, uses)?,
629                }
630            }
631        };
632        if let Some((face, middle)) = surface_missed(&curve, faces, &self.surfaces)? {
633            return Err(GeopError::new(format!(
634                "its new curve from {start:?} to {end:?} is not on face {} at {middle:?}",
635                self.names.faces[face]
636            )));
637        }
638        Ok(Some(curve))
639    }
640
641    /// The new curve of `edge`, from `start` to `end`, if it runs along an
642    /// iso-line of a face it bounds, as it did — a cylinder's rim, a wall's
643    /// edge on a cap — and that iso-line, on the face's new surface,
644    /// passes through both moved ends and lies on the other face's surface
645    /// too: then it is that iso-line, exactly ([`iso_line`]), not a trace.
646    fn on_iso_line(
647        &self,
648        edge: &EdgeSpec,
649        start: Vector3,
650        end: Vector3,
651        uses: [(usize, &Sense, &Curve2); 2],
652    ) -> GeopResult<Option<Curve3>> {
653        let faces = uses.map(|(f, ..)| f);
654        for (f, sense, pcurve) in uses {
655            let along = match sense {
656                Sense::Forward => pcurve.clone(),
657                Sense::Reversed => pcurve.reverse(),
658            };
659            let Some(line) = iso_line(&self.surfaces[f], &along)? else {
660                continue;
661            };
662            let Ok(curve) = restricted(&line, start, end) else {
663                continue;
664            };
665            if surface_missed(&curve, faces, &self.surfaces)?.is_none()
666                && turns_around(&edge.curve, &[start, end]).is_ok()
667            {
668                return Ok(Some(curve));
669            }
670        }
671        Ok(None)
672    }
673
674    /// The curve the edge `e`, used by the faces `uses`, lies on if it is a
675    /// seam — its faces pieces of one smooth surface — carried on past its
676    /// ends, so a moved end can be found on it: the edge's own curve where
677    /// both faces stay, the new surface along the edge's pcurve where both
678    /// are put on new surfaces (that keep their parametrization, as a shift
679    /// or an offset does). None for an edge that is no seam, or that
680    /// nothing moves.
681    fn seam(&self, e: usize, uses: &[(usize, Sense, &Curve2)]) -> GeopResult<Option<Curve3>> {
682        let [(f, f_sense, f_pcurve), (g, _, g_pcurve)] = uses else {
683            return Ok(None);
684        };
685        if !self.touched[*f] && !self.touched[*g] {
686            return Ok(None);
687        }
688        let normal = |f: usize, pcurve: &Curve2| -> GeopResult<Vector3> {
689            let uv = pcurve.evaluate(pcurve.mid_parameter()?)?;
690            self.surfaces[f].normal(uv[0], uv[1])
691        };
692        let smooth = normal(*f, f_pcurve)?
693            .prod_cross(&normal(*g, g_pcurve)?)
694            .norm_sq()
695            .could_be_equal(Scalar::ZERO);
696        if !smooth {
697            return Ok(None);
698        }
699        match (self.replaced[*f], self.replaced[*g]) {
700            (false, false) => Ok(Some(carried_on(&self.spec.edges[e].curve, |_| Ok(true))?)),
701            (true, true) => {
702                let along = match f_sense {
703                    Sense::Forward => (*f_pcurve).clone(),
704                    Sense::Reversed => f_pcurve.reverse(),
705                };
706                let surface = &self.surfaces[*f];
707                if let Some(line) = iso_line(surface, &along)? {
708                    return Ok(Some(line));
709                }
710                // Only as far as the surface goes: its ends in its domain.
711                // And only as far as the seam stays as narrow as its fit
712                // may drift: carrying a pcurve on extrapolates its end
713                // span, whose width grows as the span's length over the
714                // room to the power of its degree. A pcurve fitted in
715                // twenty spans, carried on by its whole domain, took the
716                // `7e-11` it has on a block at `(1e5, -1e5, 0)` to a seam
717                // `3.3e-4` wide where it was to end.
718                let ((u0, u1), (v0, v1)) = (surface.domain_u(), surface.domain_v());
719                let pcurve = carried_on(&along, |longer| {
720                    let (a, b) = longer.ends()?;
721                    for p in [a, b] {
722                        let inside = !p[0].definitely_less(u0)
723                            && !p[0].definitely_greater(u1)
724                            && !p[1].definitely_less(v0)
725                            && !p[1].definitely_greater(v1);
726                        if !inside {
727                            return Ok(false);
728                        }
729                        let point = surface.evaluate(p[0], p[1])?;
730                        if (0..3).any(|k| point[k].width().to_f64() > TRACE_DRIFT) {
731                            return Ok(false);
732                        }
733                    }
734                    Ok(true)
735                })?;
736                let (t0, t1) = pcurve.domain();
737                let at = |frac: Scalar| -> GeopResult<Vector3> {
738                    let uv = pcurve.evaluate(Scalar::interpolate(t0, t1, frac).sharpen())?;
739                    surface.evaluate(uv[0], uv[1])
740                };
741                let ends = (at(Scalar::ZERO)?, at(Scalar::ONE)?);
742                Ok(Some(fitted(at, ends, |_| Ok(())).map_err(|err| {
743                    err.with_context("fitting the seam on its new surface")
744                })?))
745            }
746            _ => Err(GeopError::new(format!(
747                "faces {} and {} are pieces of one smooth surface, and only one of them is put on another surface: not supported",
748                self.names.faces[*f], self.names.faces[*g]
749            ))),
750        }
751    }
752
753    /// The curve where the surfaces of the two faces in `uses` meet, from
754    /// `start` to `end`, following `guide` (see the module docs). Each use
755    /// gives the face, the sense its coedge runs the edge in and its old
756    /// pcurve, whose `(u, v)` seed the search on the face's surface.
757    fn traced(
758        &self,
759        guide: &Curve3,
760        start: Vector3,
761        end: Vector3,
762        uses: [(usize, &Sense, &Curve2); 2],
763    ) -> GeopResult<Curve3> {
764        let (t0, t1) = guide.domain();
765        let (a, b) = guide.ends()?;
766        let (shift0, shift1) = (start.sub(&a), end.sub(&b));
767        // Where the edge is `frac` of the way along: on both surfaces, in
768        // the plane square to the guide there through the guide's point
769        // shifted with the edge's ends.
770        let on_both = |frac: Scalar| -> GeopResult<Vector3> {
771            let t = Scalar::interpolate(t0, t1, frac).sharpen();
772            let seed = guide
773                .evaluate(t)?
774                .add(&shift0.prod_scalar(Scalar::ONE.sub(frac)))
775                .add(&shift1.prod_scalar(frac))
776                .sharpen();
777            let normal = guide.tangent(t)?.normalize()?.sharpen();
778            let mut offsets = Vec::with_capacity(2);
779            for (f, sense, pcurve) in uses {
780                let (p0, p1) = pcurve.domain();
781                let along = match sense {
782                    Sense::Forward => frac,
783                    Sense::Reversed => Scalar::ONE.sub(frac),
784                };
785                let uv = pcurve.evaluate(Scalar::interpolate(p0, p1, along).sharpen())?;
786                offsets.push(Offset::new(
787                    &self.surfaces[f],
788                    (uv[0].sharpen(), uv[1].sharpen()),
789                    Scalar::ZERO,
790                ));
791            }
792            meet(seed, &mut offsets, &[(normal, seed)])
793        };
794        fitted(on_both, (start, end), |points| turns_around(guide, points))
795            .map_err(|e| e.with_context("tracing it where its faces meet"))
796    }
797
798    /// The rebuilt body, described whole: its edges rebuilt between the
799    /// moved vertices.
800    fn result(mut self) -> GeopResult<BodySpec> {
801        let spec = self.spec;
802        let along = spec.edge_uses();
803        let curves = (0..spec.edges.len())
804            .map(|e| {
805                self.curve(e, &along[e])
806                    .map_err(|err| err.with_context(format!("edge {}", self.names.edges[e])))
807            })
808            .collect::<GeopResult<_>>()?;
809        self.curves = curves;
810        let edges = (spec.edges.iter().zip(&self.curves))
811            .map(|(edge, curve)| EdgeSpec {
812                curve: curve.clone().unwrap_or_else(|| edge.curve.clone()),
813                start: edge.start,
814                end: edge.end,
815            })
816            .collect();
817        let faces = (spec.faces.iter().enumerate())
818            .map(|(f, face)| {
819                let lp = |coedges: &Vec<CoedgeSpec>| self.face_loop(f, coedges);
820                let rebuilt_face = FaceSpec {
821                    surface: self.surfaces[f].clone(),
822                    outer: lp(&face.outer)?,
823                    holes: face.holes.iter().map(lp).collect::<GeopResult<_>>()?,
824                };
825                // Moved so far that a wall's two sides cross, the face
826                // between them runs into its own hole: a hole's pcurves
827                // cross the outer loop's, or it has left it altogether.
828                for hole in &rebuilt_face.holes {
829                    if !hole_inside(&rebuilt_face.surface, &rebuilt_face.outer, hole)? {
830                        return Err(GeopError::new(format!(
831                            "face {} runs into its own hole: the sides of a wall would cross",
832                            self.names.faces[f]
833                        )));
834                    }
835                }
836                Ok(rebuilt_face)
837            })
838            .collect::<GeopResult<_>>()?;
839        Ok(BodySpec {
840            vertices: self.points.clone(),
841            edges,
842            faces,
843            shells: spec.shells.clone(),
844            solid: spec.solid,
845        })
846    }
847
848    /// A loop of face `f`, its pcurves on the face's new surface: each
849    /// coedge that changed fitted anew between `(u, v)` worked out once
850    /// per junction, so the coedges meeting there agree on it exactly.
851    fn face_loop(&self, f: usize, coedges: &[CoedgeSpec]) -> GeopResult<Vec<CoedgeSpec>> {
852        let surface = &self.surfaces[f];
853        let replaced = self.replaced[f];
854        // A coedge along a pole whose vertex no longer is the pole of the
855        // new surface has nothing left to run along: the loop passes the
856        // vertex inside the patch now, and the coedges either side of it
857        // meet there.
858        let at_pole = |c: &CoedgeSpec| -> GeopResult<bool> {
859            let CoedgeOn::Vertex(v) = c.on else {
860                return Ok(true);
861            };
862            let end = c.pcurve.ends()?.1;
863            Ok(surface
864                .pole_at(end[0], end[1])
865                .is_some_and(|pole| pole.could_be_equal(&self.points[v])))
866        };
867        let mut kept = Vec::with_capacity(coedges.len());
868        for c in coedges {
869            if at_pole(c)? {
870                kept.push(c.clone());
871            }
872        }
873        let coedges = &kept[..];
874        let mut junctions = Vec::with_capacity(coedges.len());
875        for c in coedges {
876            let (_, end) = self.spec.ends(c.on)?;
877            let old = c.pcurve.ends()?.1;
878            let kept = |old: Vector2| -> GeopResult<bool> {
879                Ok(surface
880                    .evaluate(old[0], old[1])?
881                    .could_be_equal(&self.points[end]))
882            };
883            junctions.push(if (self.moved[end] || replaced) && !kept(old)? {
884                let (u, v) = surface.project_in_domain(
885                    self.points[end],
886                    old[0].sharpen(),
887                    old[1].sharpen(),
888                    PROJECT_ITERATIONS,
889                )?;
890                Vector2::from_array([u, v])
891            } else {
892                old
893            });
894        }
895        let n = coedges.len();
896        let mut out = Vec::with_capacity(n);
897        for (k, c) in coedges.iter().enumerate() {
898            let changed = match c.on {
899                CoedgeOn::Edge(e, _) => self.curves[e].is_some(),
900                CoedgeOn::Vertex(v) => self.moved[v],
901            };
902            let pcurve = if !changed && !replaced {
903                c.pcurve.clone()
904            } else {
905                let (start, end) = (junctions[(k + n - 1) % n], junctions[k]);
906                let curve = match c.on {
907                    CoedgeOn::Edge(e, sense) => Some(
908                        sense.orient(self.curves[e].as_ref().unwrap_or(&self.spec.edges[e].curve)),
909                    ),
910                    CoedgeOn::Vertex(_) => None,
911                };
912                pcurve(surface, &c.pcurve, curve.as_ref(), start, end).map_err(|e| {
913                    e.with_context(format!("the pcurve on face {}", self.names.faces[f]))
914                })?
915            };
916            out.push(CoedgeSpec { on: c.on, pcurve });
917        }
918        Ok(out)
919    }
920}
921
922/// The iso-line of `surface` that `along`, a pcurve on it, runs along —
923/// straight, its ends at one sharp `u` (or `v`) — over the whole of the
924/// surface's other parameter, running the way `along` does: the surface's
925/// row there, exactly, where a straight pcurve is only as good as the
926/// surface's parametrization is affine. None for any other pcurve.
927fn iso_line(surface: &NurbSurface3D, along: &Curve2) -> GeopResult<Option<Curve3>> {
928    if along.degree != 1 || along.control_points.len() != 2 {
929        return Ok(None);
930    }
931    // Read off the control points: at a weight of one, as the numbers they
932    // are, which a division would round outwards.
933    let end = |i: usize| -> GeopResult<Vector2> {
934        let p = along.control_points[i];
935        if p[2].is_exactly(Scalar::ONE) {
936            Ok(Vector2::from_array([p[0], p[1]]))
937        } else {
938            p.dehomogenized()
939        }
940    };
941    let (a, b) = (end(0)?, end(1)?);
942    let Some(k) = (0..2).find(|&k| a[k].is_exactly(b[k]) && a[k].is_sharp()) else {
943        return Ok(None);
944    };
945    let u_fixed = k == 0;
946    let at = a[k];
947    let (lo, hi) = if u_fixed {
948        surface.domain_u()
949    } else {
950        surface.domain_v()
951    };
952    let ends = at.is_exactly(lo) || at.is_exactly(hi);
953    let inside = at.definitely_greater(lo) && at.definitely_less(hi);
954    if !(ends || inside) {
955        return Ok(None);
956    }
957    let row = surface.iso_curve(u_fixed, at)?;
958    Ok(if b[1 - k].definitely_greater(a[1 - k]) {
959        Some(row)
960    } else if b[1 - k].definitely_less(a[1 - k]) {
961        Some(row.reverse())
962    } else {
963        None
964    })
965}
966
967/// Fails if the points `points`, in order along an edge rebuilt from
968/// `guide`, do not keep running the way the guide does: the edge has
969/// turned around — the faces at its ends now cross — or shrunk to nothing.
970fn turns_around(guide: &Curve3, points: &[Vector3]) -> GeopResult<()> {
971    let (t0, t1) = guide.domain();
972    let n = points.len() - 1;
973    for (k, pair) in points.windows(2).enumerate() {
974        let frac = Scalar::from_ratio(2 * k as i64 + 1, 2 * n as i64)?;
975        let tangent = guide.tangent(Scalar::interpolate(t0, t1, frac).sharpen())?;
976        if !pair[1]
977            .sub(&pair[0])
978            .prod_dot(&tangent)
979            .definitely_greater(Scalar::ZERO)
980        {
981            return Err(GeopError::new(format!(
982                "moved this far, it turns around between {:?} and {:?}: a face it bounds would vanish, or two would cross",
983                pair[0], pair[1]
984            )));
985        }
986    }
987    Ok(())
988}
989
990/// `surface` with room around it: carried on past each side
991/// ([`NurbSurface3D::extended`]) by as much of its parameter as its end
992/// span is long that way — or by a half, a quarter or an eighth of that
993/// where more would carry it too far: an arc round past where its weights
994/// stay positive, a cone through its apex, a free-form patch past where it
995/// folds back on itself and has no normal. Not past a side that is a single
996/// point, a pole: the pole would end up inside the patch, where it has no
997/// normal. How much room is a free choice; the patch's own size is room
998/// enough unless a face moves as far as it is wide.
999///
1000/// A face's copy can reach past the face where they meet at an inward
1001/// corner — or, for a plane cut off by the box around the profile, past
1002/// where the patch ends — so its surface needs room around the face.
1003pub fn room(surface: NurbSurface3D) -> GeopResult<NurbSurface3D> {
1004    let mut surface = surface;
1005    // The straight directions first, then the others: a straight
1006    // direction is carried on at both sides at once, from the rows it had,
1007    // each side by the width, halved as often as it has to be.
1008    let straight = |s: &NurbSurface3D, along_u| -> GeopResult<bool> {
1009        Ok(straight_extended(s, along_u, [None, None])?.is_some())
1010    };
1011    for along_u in [true, false] {
1012        if straight(&surface, along_u)? {
1013            let mut halved = [None, None];
1014            for (side, at_end) in [false, true].into_iter().enumerate() {
1015                if side_is_pole(&surface, along_u, at_end)? {
1016                    continue;
1017                }
1018                for k in 0..4 {
1019                    let mut one = [None, None];
1020                    one[side] = Some(k);
1021                    if let Some(longer) = straight_extended(&surface, along_u, one)?
1022                        && longer.has_normal_everywhere()?
1023                    {
1024                        halved[side] = Some(k);
1025                        break;
1026                    }
1027                }
1028            }
1029            if let Some(longer) = straight_extended(&surface, along_u, halved)? {
1030                surface = longer;
1031            }
1032        }
1033    }
1034    for along_u in [true, false] {
1035        if straight(&surface, along_u)? {
1036            continue;
1037        }
1038        for at_end in [false, true] {
1039            if side_is_pole(&surface, along_u, at_end)? {
1040                continue;
1041            }
1042            // By as much as the end span is long: carried further, a short
1043            // end span of a curved patch extrapolates far, its control
1044            // points running off by the cube of how much further.
1045            let breaks = if along_u {
1046                surface.breakpoints_u()
1047            } else {
1048                surface.breakpoints_v()
1049            };
1050            let n = breaks.len();
1051            let mut by = if at_end {
1052                breaks[n - 1].sub(breaks[n - 2])
1053            } else {
1054                breaks[1].sub(breaks[0])
1055            };
1056            for _ in 0..4 {
1057                if let Ok(longer) = surface.extended(along_u, at_end, by.sharpen())
1058                    && longer.has_normal_everywhere()?
1059                {
1060                    surface = longer;
1061                    break;
1062                }
1063                by = by.div(Scalar::TWO)?;
1064            }
1065        }
1066    }
1067    Ok(surface)
1068}
1069
1070/// `surface` carried on past its sides at the start and the end of `u`
1071/// (`along_u`) or `v`, if it is straight that way — degree 1, two rows of
1072/// control points of equal weights — at each by its width halved as often
1073/// as `halved` says, or not where it says `None`: its end rows moved on
1074/// along the straight lines, so the same points stay at the same `(u, v)`
1075/// and the patch is still one span. `None` where it is not straight that
1076/// way.
1077fn straight_extended(
1078    surface: &NurbSurface3D,
1079    along_u: bool,
1080    halved: [Option<u32>; 2],
1081) -> GeopResult<Option<NurbSurface3D>> {
1082    let (degree, num, other) = if along_u {
1083        (surface.degree_u, surface.num_u, surface.num_v)
1084    } else {
1085        (surface.degree_v, surface.num_v, surface.num_u)
1086    };
1087    if degree != 1 || num != 2 {
1088        return Ok(None);
1089    }
1090    let index = |row: usize, j: usize| {
1091        if along_u {
1092            row * surface.num_v + j
1093        } else {
1094            j * surface.num_v + row
1095        }
1096    };
1097    let cps = &surface.control_points;
1098    if !(0..other).all(|j| cps[index(0, j)][3].could_be_equal(cps[index(1, j)][3])) {
1099        return Ok(None);
1100    }
1101    let knots = if along_u {
1102        &surface.knot_vector_u
1103    } else {
1104        &surface.knot_vector_v
1105    };
1106    let (mut lo, mut hi) = (knots[1], knots[2]);
1107    let width = hi.sub(lo);
1108    let mut control_points = cps.clone();
1109    for (end, k) in halved.into_iter().enumerate() {
1110        let Some(k) = k else {
1111            continue;
1112        };
1113        let fraction = Scalar::from_ratio(1, 1 << k)?;
1114        for j in 0..other {
1115            let (a, b) = (cps[index(1 - end, j)], cps[index(end, j)]);
1116            // Carried on by the whole width, the end row is `2 b - a`.
1117            control_points[index(end, j)] = if k == 0 {
1118                b.prod_scalar(Scalar::TWO).sub(&a)
1119            } else {
1120                b.add(&b.sub(&a).prod_scalar(fraction))
1121            };
1122        }
1123        let by = if k == 0 { width } else { width.mul(fraction) };
1124        if end == 1 {
1125            hi = hi.add(by);
1126        } else {
1127            lo = lo.sub(by);
1128        }
1129    }
1130    let knots = vec![lo, lo, hi, hi];
1131    Ok(Some(if along_u {
1132        NurbSurface3D::try_new(
1133            1,
1134            surface.degree_v,
1135            control_points,
1136            knots,
1137            surface.knot_vector_v.clone(),
1138        )?
1139    } else {
1140        NurbSurface3D::try_new(
1141            surface.degree_u,
1142            1,
1143            control_points,
1144            surface.knot_vector_u.clone(),
1145            knots,
1146        )?
1147    }))
1148}
1149
1150/// Whether the side of `surface` at the end (`at_end`) or the start of its
1151/// `u` (`along_u`) or `v` parameter is a single point: every control point
1152/// of that boundary row the same.
1153fn side_is_pole(surface: &NurbSurface3D, along_u: bool, at_end: bool) -> GeopResult<bool> {
1154    let (nu, nv) = (surface.num_u, surface.num_v);
1155    let row: Vec<usize> = if along_u {
1156        let i = if at_end { nu - 1 } else { 0 };
1157        (0..nv).map(|j| i * nv + j).collect()
1158    } else {
1159        let j = if at_end { nv - 1 } else { 0 };
1160        (0..nu).map(|i| i * nv + j).collect()
1161    };
1162    let first = surface.control_points[row[0]].dehomogenized()?;
1163    for &k in &row[1..] {
1164        if !surface.control_points[k]
1165            .dehomogenized()?
1166            .could_be_equal(&first)
1167        {
1168            return Ok(false);
1169        }
1170    }
1171    Ok(true)
1172}
1173
1174/// The point nearest `seed` where the smooth surfaces `groups` of `pieces`
1175/// all meet (see [`meet`]). Where it was is only where to start looking —
1176/// a free choice, so sharp: a seed as wide as the union of points joined
1177/// across a gap would carry its width into the answer.
1178fn nearest(
1179    seed: Vector3,
1180    pieces: &[(&NurbSurface3D, (Scalar, Scalar))],
1181    groups: &[Vec<usize>],
1182) -> GeopResult<Vector3> {
1183    let mut offsets: Vec<Offset> = groups
1184        .iter()
1185        .map(|group| Offset {
1186            pieces: group.iter().map(|&i| pieces[i]).collect(),
1187            offset: Scalar::ZERO,
1188        })
1189        .collect();
1190    meet(seed.sharpen(), &mut offsets, &[])
1191}
1192
1193/// The part of `curve`, carried on past its ends or cut back, from `start`
1194/// to `end`, both on it: what is left of an edge whose ends move along it.
1195pub(crate) fn along(curve: &Curve3, start: Vector3, end: Vector3) -> GeopResult<Curve3> {
1196    restricted(&carried_on(curve, |_| Ok(true))?, start, end)
1197}
1198
1199/// The part of `longer`, a seam carried on past its ends, from `start` to
1200/// `end`, both on it.
1201fn restricted(longer: &Curve3, start: Vector3, end: Vector3) -> GeopResult<Curve3> {
1202    let at = |p: &Vector3| -> GeopResult<Scalar> {
1203        let (t, _) = longer.closest_point(p)?;
1204        if !longer.evaluate(t)?.could_be_equal(p) {
1205            return Err(GeopError::new(format!(
1206                "the seam, carried on, does not pass through its moved end {p:?}"
1207            )));
1208        }
1209        Ok(t)
1210    };
1211    let (s, t) = (at(&start)?, at(&end)?);
1212    if !t.definitely_greater(s) {
1213        return Err(GeopError::new(format!(
1214            "its moved ends {start:?} and {end:?} lie the other way round along it: a face it bounds has vanished"
1215        )));
1216    }
1217    longer.sub_curve(s, t)
1218}
1219
1220/// A cubic from `start` to `end` through the points `at(frac)` of a curve
1221/// in between — `frac` the fraction of the way along it — widened to
1222/// enclose the curve between them: fitted over [`FIRST_LEGS`] legs, split
1223/// finer while the fit is wider than [`TRACE_DRIFT`]. `check` is asked of
1224/// each set of points before they are fitted.
1225fn fitted(
1226    at: impl Fn(Scalar) -> GeopResult<Vector3>,
1227    (start, end): (Vector3, Vector3),
1228    check: impl Fn(&[Vector3]) -> GeopResult<()>,
1229) -> GeopResult<Curve3> {
1230    let fraction = |i: usize, n: usize| Scalar::from_ratio(i as i64, n as i64);
1231    let refined = refine(FIRST_LEGS, usize::BITS as usize, |legs| -> GeopResult<_> {
1232        let mut points = vec![start];
1233        for i in 1..legs {
1234            points.push(at(fraction(i, legs)?)?);
1235        }
1236        points.push(end);
1237        check(&points)?;
1238        let mut between = Vec::with_capacity(legs);
1239        for i in 0..legs {
1240            let mut inside = Vec::new();
1241            for &(p, q) in true_point_fractions(i, legs) {
1242                let frac = fraction(i, legs)?.add(Scalar::from_ratio(p, q * legs as i64)?);
1243                inside.push(at(frac)?);
1244            }
1245            between.push(inside);
1246        }
1247        let curve = NurbCurve3D::interpolate_enclosing(&points, &between, 3)?;
1248        let wide = curve
1249            .control_points
1250            .iter()
1251            .any(|p| (0..3).any(|k| p[k].width().to_f64() > TRACE_DRIFT));
1252        if wide && 2 * legs <= MOST_LEGS {
1253            return Ok(Refine::Finer(2 * legs));
1254        }
1255        Ok(Refine::Done(curve))
1256    })?;
1257    match refined {
1258        Refined::Done(curve) => Ok(curve),
1259        Refined::Exhausted(legs) => Err(GeopError::new(format!(
1260            "its legs were split past every cap, at {legs}"
1261        ))),
1262    }
1263}
1264
1265/// `curve` carried on past both its ends by as much as its domain is long,
1266/// or by less where that would carry it too far — an arc round past where
1267/// its weights stay positive, or past what `within` accepts — as far as a
1268/// half, a quarter or an eighth of that: room for an end to move along it.
1269/// How much is a free choice.
1270fn carried_on<const D: usize>(
1271    curve: &NurbCurve<D>,
1272    within: impl Fn(&NurbCurve<D>) -> GeopResult<bool>,
1273) -> GeopResult<NurbCurve<D>> {
1274    let (t0, t1) = curve.domain();
1275    let width = t1.sub(t0);
1276    let mut out = curve.clone();
1277    for at_end in [false, true] {
1278        let mut by = width;
1279        for _ in 0..4 {
1280            if let Ok(longer) = out.extended(at_end, by.sharpen())
1281                && within(&longer)?
1282            {
1283                out = longer;
1284                break;
1285            }
1286            by = by.div(Scalar::TWO)?;
1287        }
1288    }
1289    Ok(out)
1290}
1291
1292/// Whether the loop `hole` lies inside the loop `outer` on `surface`: no
1293/// pcurve of the one meets one of the other, and a point of the hole is
1294/// inside the outer loop.
1295fn hole_inside(
1296    surface: &NurbSurface3D,
1297    outer: &[CoedgeSpec],
1298    hole: &[CoedgeSpec],
1299) -> GeopResult<bool> {
1300    for h in hole {
1301        for o in outer {
1302            match curve_curve_intersect(&h.pcurve, &o.pcurve, 1, MAX_NODES, min_subdivision_size())?
1303            {
1304                Intersections::Found(found) if found.is_empty() => {}
1305                _ => return Ok(false),
1306            }
1307        }
1308    }
1309    let first = &hole[0].pcurve;
1310    let point = first.evaluate(first.mid_parameter()?)?;
1311    let pcurves: Vec<_> = outer.iter().map(|c| &c.pcurve).collect();
1312    Ok(pcurves_contain(
1313        surface,
1314        &pcurves,
1315        point,
1316        MAX_NODES,
1317        min_subdivision_size(),
1318        SEED,
1319    )? == PointClassification::Inside)
1320}
1321
1322#[cfg(test)]
1323mod tests;