Skip to main content

geop_ops_surface/
knit.rs

1//! [`Knit`]: sheets whose edges meet, joined into one — and into a solid,
2//! once they close up.
3//!
4//! Joining is done on the sheets' description ([`BodySpec`]), which is then
5//! built anew as one body:
6//!
7//! - **vertices** of different sheets at one point become one;
8//! - **edges** then running between the same two vertices become one where
9//!   their curves do too — the middle of each on the other. That an edge
10//!   joins the same vertices is the topological question; the curves are
11//!   asked only to tell apart edges that share both ends but run
12//!   differently, as the two halves of a circle do;
13//! - **faces** are turned where a shared edge would otherwise be run the
14//!   same way by both: a body's faces wind consistently, each edge run one
15//!   way by one face and the other way by the other.
16//!
17//! What is left used by one face only is an open edge. With none, the
18//! sheets close up into a solid, its faces turned to point out of it.
19//!
20//! An edge that runs along two of another sheet — a T-junction, where a
21//! vertex of that sheet lies inside the edge — is first split there
22//! (`split_at_junctions`), so the edges meet end to end. Otherwise edges
23//! that meet only in part are not joined: they are open edges, and a solid
24//! asked for is refused, naming them.
25//!
26//! Sheets made to some accuracy — another system's, a file's — meet only
27//! to within it. The user can state it, as a gap: then what is within the
28//! gap of each other is joined, and, once the sheets close up, every vertex
29//! joined is moved to where the surfaces around it meet and every edge
30//! from it to where its two faces do (`geop_lib_shell::rebuilt`). The gap is
31//! never guessed, nor grown until things join.
32
33use geop_core_editor::{
34    edit_form::{EditForm, FormState},
35    editable::{Context, OperationGroup, OperationInfo, OperationTier},
36    editing::{Number, Unit},
37    part_operation,
38    part_operation::PartOperation,
39};
40use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
41use std::collections::HashMap;
42
43use geop_core_geometry::{
44    contains::curve::curve_could_contain,
45    nurb_surface::NurbSurface3D,
46    search::{MAX_NODES, PROJECT_ITERATIONS, min_subdivision_size},
47};
48use geop_core_math::{
49    geop_error::{GeopError, GeopResult},
50    scalars::Scalar,
51    solvers::walk::{Take, flood},
52    union_find::UnionFind,
53    vector::{Vector3, Vector4},
54};
55use geop_core_topology::{
56    Body, Curve2, Curve3, Sense, ShellId,
57    build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
58    validation::{Outward, ValidationParameters, normal_points_outward},
59};
60use geop_lib_shell::rebuilt;
61use geop_ops_model::{BodyNames, Face, ModelOperation, NamedModel, Namer, build_model};
62use serde::{Deserialize, Serialize};
63
64/// Joins the sheets the faces named `faces` stand in into one, along the
65/// edges where they meet, for the operation `K` — see the module docs.
66/// Closed up, the result is a solid named `knit(K)`; with `solid`, it must
67/// close up, and open edges are refused by name. Every face, edge and
68/// vertex keeps its name, of two joined into one the first sheet's.
69#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
70pub struct Knit;
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73pub struct KnitArgs {
74    /// A face of each sheet to join.
75    pub faces: Vec<ElementReference<NamedModel, Face>>,
76    /// Whether the result must close up into a solid.
77    #[serde(default)]
78    pub solid: bool,
79    /// How far apart edges and vertices of the sheets may be and still be
80    /// joined: the accuracy the sheets were made to. None, to join only
81    /// what meets.
82    #[serde(default)]
83    pub gap: f64,
84}
85
86part_operation!(Knit(KnitArgs));
87
88impl PartOperation for Knit {
89    const KIND: &'static str = "knit";
90    const INFO: OperationInfo = OperationInfo::new(
91        "Knit",
92        "Join faces standing on their own along the edges where they meet, into a solid once they close up.",
93        OperationGroup::Surface,
94        OperationTier::Menu,
95    )
96    .experimental()
97    .icon(crate::icons::KNIT);
98    type Args = KnitArgs;
99    type Session = ();
100
101    /// Nothing picked yet; a solid wanted.
102    fn new_args(_: &Context) -> KnitArgs {
103        KnitArgs {
104            faces: Vec::new(),
105            solid: true,
106            gap: 0.0,
107        }
108    }
109
110    fn run(ctx: &mut StepContext, args: &KnitArgs) -> GeopResult<()> {
111        let _step = ctx.step();
112        build_model::<Self>(ctx, args)
113    }
114
115    /// The sheets, picked by a face of each, and whether they must close.
116    fn form<'a>(
117        context: &'a Context<'a>,
118        args: &KnitArgs,
119        _: &FormState<()>,
120    ) -> EditForm<'a, KnitArgs> {
121        let mut f = EditForm::<KnitArgs>::default();
122        f.pick(
123            "faces",
124            "sheets",
125            &context.before,
126            &args.faces,
127            true,
128            |edit, faces| edit.args.faces = faces,
129        );
130        f.checkbox("solid", "close into a solid", args.solid, |edit, solid| {
131            edit.args.solid = solid
132        });
133        let gap = Number::new("gap", args.gap, Unit::Length).range(0.0, 1.0);
134        f.number("gap", gap, |edit, gap| edit.args.gap = gap);
135        f
136    }
137}
138
139impl ModelOperation for Knit {
140    fn build(model: &mut NamedModel, _: &Reader, step: &str, args: &KnitArgs) -> GeopResult<()> {
141        let namer = Namer::new("knit", step)?;
142        let mut sheets: Vec<ShellId> = Vec::new();
143        for face in &args.faces {
144            let sheet = model.sheet_of_face(model.face_id(face.name())?, "sheets are knit")?;
145            if !sheets.contains(&sheet) {
146                sheets.push(sheet);
147            }
148        }
149        if !(args.gap.is_finite() && args.gap >= 0.0) {
150            return Err(GeopError::new(format!(
151                "a gap is a distance of zero or more, not {}",
152                args.gap
153            )));
154        }
155        knit(
156            model,
157            &namer,
158            &sheets,
159            args.solid,
160            Scalar::from_f64(args.gap),
161        )?;
162        Ok(())
163    }
164}
165
166/// Joins `sheets` (see the module docs), what is within `gap` of each
167/// other as one, and returns what they became: a solid named `namer`'s
168/// root once they close up, a sheet otherwise — an error naming the open
169/// edges, if `solid` asks for one.
170pub fn knit(
171    part: &mut NamedModel,
172    namer: &Namer,
173    sheets: &[ShellId],
174    solid: bool,
175    gap: Scalar,
176) -> GeopResult<Body> {
177    if sheets.is_empty() {
178        return Err(GeopError::new(
179            "knitting needs faces standing on their own: pick the sheets to join",
180        ));
181    }
182    if sheets.len() < 2 && !solid {
183        return Err(GeopError::new("knitting needs two sheets at least"));
184    }
185    let model = part.topology();
186    let mut faces = Vec::new();
187    let mut sheet_of_face = Vec::new();
188    for (k, &sheet) in sheets.iter().enumerate() {
189        for face in model.body_faces(Body::Sheet(sheet))? {
190            faces.push(face);
191            sheet_of_face.push(k);
192        }
193    }
194    let (mut spec, sources) = model.body_spec(&faces, false)?;
195    let BodyNames {
196        vertices: mut vertex_names,
197        edges: mut edge_names,
198        faces: face_names,
199        ..
200    } = part.source_names(&sources)?;
201    let near = Near { gap };
202    split_at_junctions(
203        &mut spec,
204        &sheet_of_face,
205        &mut vertex_names,
206        &mut edge_names,
207        namer,
208        near,
209    )?;
210
211    let joined = Joined::new(&spec, &sheet_of_face, &vertex_names, &edge_names, near)?;
212    let (mut out, uses) = joined.spec(&spec)?;
213    let open: Vec<&str> = uses
214        .iter()
215        .enumerate()
216        .filter(|&(_, &n)| n == 1)
217        .map(|(e, _)| edge_names[joined.edge_source[e]].as_str())
218        .collect();
219    if solid && !open.is_empty() {
220        return Err(GeopError::new(format!(
221            "the sheets do not close up into a solid: edges {open:?} are open — each is used by one face only, with no edge of another sheet running along it from end to end"
222        )));
223    }
224    out.solid = open.is_empty();
225    let names = BodyNames {
226        vertices: joined
227            .vertex_source
228            .iter()
229            .map(|&v| vertex_names[v].clone())
230            .collect(),
231        edges: joined
232            .edge_source
233            .iter()
234            .map(|&e| edge_names[e].clone())
235            .collect(),
236        faces: face_names,
237        solid: out.solid.then(|| namer.root()),
238    };
239    turn_consistently(&mut out, &names)?;
240    // Joined across a gap, a vertex is the union of the points joined and
241    // an edge the first of the curves: wide, and off the faces by as much
242    // as the gap. Closed up, every vertex joined moves to where the
243    // surfaces around it meet, every edge from it to where its faces do
244    // (see `geop_lib_shell::rebuilt`), and the width comes back down to
245    // how well the surfaces agree.
246    if gap.definitely_greater(Scalar::ZERO) && out.solid {
247        let reseat: Vec<bool> = (0..out.vertices.len())
248            .map(|v| (joined.vertex.iter()).filter(|&&w| w == v).count() > 1)
249            .collect();
250        let kept = vec![false; out.faces.len()];
251        out = rebuilt(&out, &names, &kept, &reseat).map_err(|e| {
252            e.with_context("moving what was joined across the gap onto where the surfaces meet")
253        })?;
254    }
255    let consumed: Vec<Body> = sheets.iter().map(|&s| Body::Sheet(s)).collect();
256    part.delete_bodies(&consumed)?;
257    let built = part.build_body(out, names)?;
258    let Some(solid_id) = built.solid else {
259        return built.body();
260    };
261    // The faces wind consistently, but which way round is a guess: turned
262    // all, if they point into the solid.
263    let params = ValidationParameters::default();
264    let model = part.topology();
265    let mut inward = None;
266    for &f in &built.faces {
267        if let Some(verdict) = normal_points_outward(&params, model, solid_id, built.shells[0], f)?
268        {
269            inward = Some(matches!(verdict, Outward::No(_)));
270            break;
271        }
272    }
273    match inward {
274        Some(true) => {
275            for &f in &built.faces {
276                part.reverse_face(f)?;
277            }
278        }
279        Some(false) => {}
280        None => {
281            return Err(GeopError::new(
282                "the sheets close up, but no face tells which side of them the solid is on",
283            ));
284        }
285    }
286    Ok(Body::Solid(solid_id))
287}
288
289/// How the entities of the sheets' description join: each vertex's and
290/// edge's representative, and which of them stay.
291struct Joined {
292    /// Per vertex of the description, the index of the vertex it becomes.
293    vertex: Vec<usize>,
294    /// Per vertex kept, the vertex of the description it is.
295    vertex_source: Vec<usize>,
296    /// Per edge of the description, the edge it becomes, and whether it
297    /// runs the other way than that one.
298    edge: Vec<(usize, bool)>,
299    /// Per edge kept, the edge of the description it is.
300    edge_source: Vec<usize>,
301}
302
303impl Joined {
304    fn new(
305        spec: &BodySpec,
306        sheet_of_face: &[usize],
307        vertex_names: &[String],
308        edge_names: &[String],
309        near: Near,
310    ) -> GeopResult<Self> {
311        // The sheet each vertex and edge is of.
312        let mut vertex_sheet = vec![usize::MAX; spec.vertices.len()];
313        let mut edge_sheet = vec![usize::MAX; spec.edges.len()];
314        for (f, face) in spec.faces.iter().enumerate() {
315            for c in std::iter::once(&face.outer).chain(&face.holes).flatten() {
316                match c.on {
317                    CoedgeOn::Edge(e, _) => {
318                        edge_sheet[e] = sheet_of_face[f];
319                        vertex_sheet[spec.edges[e].start] = sheet_of_face[f];
320                        vertex_sheet[spec.edges[e].end] = sheet_of_face[f];
321                    }
322                    CoedgeOn::Vertex(v) => vertex_sheet[v] = sheet_of_face[f],
323                }
324            }
325        }
326
327        // Vertices at one point, of different sheets, joined.
328        let n = spec.vertices.len();
329        let mut groups = UnionFind::new(n);
330        for i in 0..n {
331            for j in i + 1..n {
332                if vertex_sheet[i] != vertex_sheet[j]
333                    && near.points(&spec.vertices[i], &spec.vertices[j])
334                {
335                    groups.union(i, j);
336                }
337            }
338        }
339        let mut vertex = vec![0; n];
340        let mut vertex_source = Vec::new();
341        for group in groups.groups() {
342            let mut sheets: Vec<usize> = group.iter().map(|&v| vertex_sheet[v]).collect();
343            sheets.sort();
344            if sheets.windows(2).any(|w| w[0] == w[1]) {
345                let names: Vec<&str> = group.iter().map(|&v| vertex_names[v].as_str()).collect();
346                return Err(GeopError::new(format!(
347                    "vertices {names:?} could all be at one point, two of them of the same sheet: which to join is ambiguous"
348                )));
349            }
350            for &v in &group {
351                vertex[v] = vertex_source.len();
352            }
353            vertex_source.push(group[0]);
354        }
355
356        // Edges between the same two vertices, of different sheets, whose
357        // curves coincide, joined.
358        let ends = |e: usize| {
359            let (a, b) = (vertex[spec.edges[e].start], vertex[spec.edges[e].end]);
360            (a.min(b), a.max(b))
361        };
362        let mut between: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
363        for e in 0..spec.edges.len() {
364            between.entry(ends(e)).or_default().push(e);
365        }
366        let mut partner: Vec<Option<usize>> = vec![None; spec.edges.len()];
367        let mut keys: Vec<_> = between.keys().copied().collect();
368        keys.sort();
369        for key in keys {
370            let candidates = &between[&key];
371            for (i, &a) in candidates.iter().enumerate() {
372                for &b in &candidates[i + 1..] {
373                    if edge_sheet[a] == edge_sheet[b]
374                        || !coincide(&spec.edges[a], &spec.edges[b], near)?
375                    {
376                        continue;
377                    }
378                    if let Some(other) = partner[a].or(partner[b]) {
379                        return Err(GeopError::new(format!(
380                            "edges {}, {} and {} could all run along one another: which to join is ambiguous",
381                            edge_names[a], edge_names[b], edge_names[other]
382                        )));
383                    }
384                    partner[a] = Some(b);
385                    partner[b] = Some(a);
386                }
387            }
388        }
389        let mut edge = vec![(0, false); spec.edges.len()];
390        let mut edge_source = Vec::new();
391        for e in 0..spec.edges.len() {
392            match partner[e] {
393                Some(first) if first < e => {
394                    let (kept, this) = (&spec.edges[first], &spec.edges[e]);
395                    let turned = vertex[kept.start] != vertex[this.start]
396                        || (kept.start == kept.end) && turned_closed(kept, this)?;
397                    edge[e] = (edge[first].0, turned);
398                }
399                _ => {
400                    edge[e] = (edge_source.len(), false);
401                    edge_source.push(e);
402                }
403            }
404        }
405        Ok(Self {
406            vertex,
407            vertex_source,
408            edge,
409            edge_source,
410        })
411    }
412
413    /// The description of the joined body, every coedge on the vertices and
414    /// edges kept, and how many coedges use each edge kept.
415    fn spec(&self, spec: &BodySpec) -> GeopResult<(BodySpec, Vec<usize>)> {
416        let mut uses = vec![0; self.edge_source.len()];
417        let mut faces = Vec::with_capacity(spec.faces.len());
418        for face in &spec.faces {
419            let mut remap = |lp: &Vec<geop_core_topology::build::CoedgeSpec>| {
420                lp.iter()
421                    .map(|c| {
422                        let mut c = c.clone();
423                        c.on = match c.on {
424                            CoedgeOn::Edge(e, sense) => {
425                                let (kept, turned) = self.edge[e];
426                                uses[kept] += 1;
427                                CoedgeOn::Edge(kept, if turned { sense.opposite() } else { sense })
428                            }
429                            CoedgeOn::Vertex(v) => CoedgeOn::Vertex(self.vertex[v]),
430                        };
431                        c
432                    })
433                    .collect::<Vec<_>>()
434            };
435            let outer = remap(&face.outer);
436            let holes = face.holes.iter().map(&mut remap).collect();
437            faces.push(FaceSpec {
438                surface: face.surface.clone(),
439                outer,
440                holes,
441            });
442        }
443        let out = BodySpec {
444            vertices: self
445                .vertex_source
446                .iter()
447                .map(|&v| {
448                    // One point, the union of every vertex joined into it.
449                    (0..spec.vertices.len())
450                        .filter(|&w| self.vertex[w] == self.vertex[v])
451                        .map(|w| spec.vertices[w])
452                        .reduce(|a, b| a.union(&b))
453                        .expect("the vertex itself")
454                })
455                .collect(),
456            edges: self
457                .edge_source
458                .iter()
459                .map(|&e| {
460                    let edge = &spec.edges[e];
461                    EdgeSpec {
462                        curve: edge.curve.clone(),
463                        start: self.vertex[edge.start],
464                        end: self.vertex[edge.end],
465                    }
466                })
467                .collect(),
468            faces,
469            shells: vec![(0..spec.faces.len()).collect()],
470            solid: false,
471        };
472        Ok((out, uses))
473    }
474}
475
476/// Whether two edges between the same vertices are one: the middle of each
477/// lies on the other's curve, as far as `near` tells.
478fn coincide(a: &EdgeSpec, b: &EdgeSpec, near: Near) -> GeopResult<bool> {
479    let middle = |e: &EdgeSpec| e.curve.evaluate(e.curve.mid_parameter()?);
480    let on = |e: &EdgeSpec, p| -> GeopResult<bool> { Ok(near.on_curve(&e.curve, &p)?.is_some()) };
481    Ok(on(b, middle(a)?)? && on(a, middle(b)?)?)
482}
483
484/// How far apart what is joined may be: the gap the sheets were made with,
485/// which the user states — nothing is guessed. Without one, only what
486/// could be at one point, as the enclosures say, is joined.
487#[derive(Clone, Copy, Debug)]
488struct Near {
489    gap: Scalar,
490}
491
492impl Near {
493    /// `p` grown by the gap in every direction: every point within it.
494    fn grown(&self, p: &Vector3) -> Vector3 {
495        Vector3::from_array(std::array::from_fn(|k| {
496            p[k].sub(self.gap).union(p[k].add(self.gap))
497        }))
498    }
499
500    /// Whether `a` and `b` could be within the gap of each other.
501    fn points(&self, a: &Vector3, b: &Vector3) -> bool {
502        self.grown(a).could_be_equal(b)
503    }
504
505    /// Where on `curve` a point within the gap of `p` could be, if any.
506    fn on_curve(&self, curve: &Curve3, p: &Vector3) -> GeopResult<Option<Scalar>> {
507        curve_could_contain(curve, &self.grown(p), MAX_NODES, min_subdivision_size())
508    }
509}
510
511/// Splits every edge of one sheet at each vertex of another that lies on
512/// it, inside it — a T-junction: one sheet's edge running along the edges
513/// of two of another — so the edges meet end to end and join. The split
514/// point is the edge's own, refined to its point nearest the vertex: the
515/// edge's two pieces run on exactly as it did, their shared end a vertex
516/// of its own sheet, which joins the other's vertex as any two do. The
517/// new vertex and the piece from it on are named `K(E,V)` for the knit
518/// `K`, the edge `E` and the vertex `V`; the edge keeps its name for the
519/// piece up to it.
520fn split_at_junctions(
521    spec: &mut BodySpec,
522    sheet_of_face: &[usize],
523    vertex_names: &mut Vec<String>,
524    edge_names: &mut Vec<String>,
525    namer: &Namer,
526    near: Near,
527) -> GeopResult<()> {
528    let mut edge_sheet = vec![usize::MAX; spec.edges.len()];
529    let mut vertex_sheet = vec![usize::MAX; spec.vertices.len()];
530    for (f, face) in spec.faces.iter().enumerate() {
531        for c in face.loops().into_iter().flatten() {
532            let (a, b) = spec.ends(c.on)?;
533            vertex_sheet[a] = sheet_of_face[f];
534            vertex_sheet[b] = sheet_of_face[f];
535            if let CoedgeOn::Edge(e, _) = c.on {
536                edge_sheet[e] = sheet_of_face[f];
537            }
538        }
539    }
540    let mut e = 0;
541    while e < spec.edges.len() {
542        let mut split = None;
543        for (v, &point) in spec.vertices.iter().enumerate() {
544            let edge = &spec.edges[e];
545            if vertex_sheet[v] == edge_sheet[e]
546                || near.points(&point, &spec.vertices[edge.start])
547                || near.points(&point, &spec.vertices[edge.end])
548            {
549                continue;
550            }
551            let Some(t) = near.on_curve(&edge.curve, &point)? else {
552                continue;
553            };
554            let t = edge.curve.closest_parameter_from(&point, t.sharpen())?;
555            let (t0, t1) = edge.curve.domain();
556            if t.definitely_greater(t0)
557                && t.definitely_less(t1)
558                && near.points(&edge.curve.evaluate(t)?, &point)
559            {
560                split = Some((v, t));
561                break;
562            }
563        }
564        match split {
565            Some((v, t)) => {
566                let name = namer.piece(&edge_names[e], &vertex_names[v]);
567                split_edge(spec, e, t)?;
568                vertex_sheet.push(edge_sheet[e]);
569                vertex_names.push(name.clone());
570                edge_sheet.push(edge_sheet[e]);
571                edge_names.push(name);
572                // The piece up to the vertex may meet another.
573            }
574            None => e += 1,
575        }
576    }
577    Ok(())
578}
579
580/// Splits the edge `e` of `spec` at its parameter `t`, sharp: a new vertex
581/// there, `e` running up to it and a new edge on from it, and every coedge
582/// along `e` two, its pcurve split where it passes the vertex's foot point
583/// on its face.
584fn split_edge(spec: &mut BodySpec, e: usize, t: Scalar) -> GeopResult<()> {
585    let edge = spec.edges[e].clone();
586    let (t0, t1) = edge.curve.domain();
587    let (first, second) = (edge.curve.sub_curve(t0, t)?, edge.curve.sub_curve(t, t1)?);
588    let point = edge.curve.evaluate(t)?;
589    let v = spec.vertices.len();
590    spec.vertices.push(point);
591    spec.edges[e] = EdgeSpec {
592        curve: first,
593        start: edge.start,
594        end: v,
595    };
596    let next = spec.edges.len();
597    spec.edges.push(EdgeSpec {
598        curve: second,
599        start: v,
600        end: edge.end,
601    });
602    for face in &mut spec.faces {
603        let surface = face.surface.clone();
604        for lp in std::iter::once(&mut face.outer).chain(face.holes.iter_mut()) {
605            let Some(k) = lp
606                .iter()
607                .position(|c| matches!(c.on, CoedgeOn::Edge(x, _) if x == e))
608            else {
609                continue;
610            };
611            let coedge = lp[k].clone();
612            let CoedgeOn::Edge(_, sense) = coedge.on else {
613                unreachable!("found on an edge")
614            };
615            let (before, after) = split_pcurve(&surface, &coedge.pcurve, &point)?;
616            let pieces = match sense {
617                Sense::Forward => [(e, before), (next, after)],
618                Sense::Reversed => [(next, before), (e, after)],
619            };
620            lp.splice(
621                k..=k,
622                pieces.map(|(edge, pcurve)| CoedgeSpec {
623                    on: CoedgeOn::Edge(edge, sense),
624                    pcurve,
625                }),
626            );
627        }
628    }
629    Ok(())
630}
631
632/// `pcurve`, on `surface`, split where it passes `point`'s foot point: at
633/// its parameter nearest that `(u, v)`, found as a curve in space lying in
634/// the plane `z = 0`.
635fn split_pcurve(
636    surface: &NurbSurface3D,
637    pcurve: &Curve2,
638    point: &Vector3,
639) -> GeopResult<(Curve2, Curve2)> {
640    let (s0, s1) = pcurve.domain();
641    let middle = pcurve.evaluate(pcurve.mid_parameter()?)?;
642    let (u, v) = surface.project(
643        *point,
644        middle[0].sharpen(),
645        middle[1].sharpen(),
646        PROJECT_ITERATIONS,
647    )?;
648    let lifted = Curve3::try_new(
649        pcurve.degree,
650        (pcurve.control_points.iter())
651            .map(|cp| Vector4::from_array([cp[0], cp[1], Scalar::ZERO, cp[2]]))
652            .collect(),
653        pcurve.knot_vector.clone(),
654    )?;
655    let target = Vector3::from_array([u, v, Scalar::ZERO]);
656    let s = lifted.closest_parameter_from(&target, pcurve.mid_parameter()?.sharpen())?;
657    if !(s.definitely_greater(s0) && s.definitely_less(s1)) {
658        return Err(GeopError::new(format!(
659            "the pcurve does not pass the split point's foot point {:?} inside it",
660            (u, v)
661        )));
662    }
663    Ok((pcurve.sub_curve(s0, s)?, pcurve.sub_curve(s, s1)?))
664}
665
666/// Whether the closed edge `b` runs round the other way than `a`, which it
667/// coincides with: their directions at their common start compared.
668fn turned_closed(a: &EdgeSpec, b: &EdgeSpec) -> GeopResult<bool> {
669    let ta = a.curve.tangent(a.curve.domain().0)?;
670    let tb = b.curve.tangent(b.curve.domain().0)?;
671    let along = ta.prod_dot(&tb);
672    if along.definitely_less(Scalar::ZERO) {
673        Ok(true)
674    } else if along.definitely_greater(Scalar::ZERO) {
675        Ok(false)
676    } else {
677        Err(GeopError::new(
678            "cannot tell which way round two closed edges run along one another",
679        ))
680    }
681}
682
683/// Turns faces of `spec` so that every edge two faces share is run one way
684/// by one and the other way by the other — keeping the first face of each
685/// connected part as it is. An error if the faces cannot be turned so (a
686/// one-sided surface), or do not all hang together.
687fn turn_consistently(spec: &mut BodySpec, names: &BodyNames) -> GeopResult<()> {
688    // Per edge, the faces using it and the sense they use it in.
689    let mut users: Vec<Vec<(usize, geop_core_topology::Sense)>> =
690        vec![Vec::new(); spec.edges.len()];
691    for (f, face) in spec.faces.iter().enumerate() {
692        for c in std::iter::once(&face.outer).chain(&face.holes).flatten() {
693            if let CoedgeOn::Edge(e, sense) = c.on {
694                users[e].push((f, sense));
695            }
696        }
697    }
698    let n = spec.faces.len();
699    let mut turned: Vec<Option<bool>> = vec![None; n];
700    turned[0] = Some(false);
701    flood([0], Take::Newest, |&f| {
702        let flip = turned[f].expect("visited");
703        let mut found = Vec::new();
704        for (e, at) in users.iter().enumerate() {
705            let [(a, sa), (b, sb)] = at.as_slice() else {
706                continue;
707            };
708            let (g, same) = if *a == f {
709                (*b, sa == sb)
710            } else if *b == f {
711                (*a, sa == sb)
712            } else {
713                continue;
714            };
715            // Run the same way by both, one of them has to turn.
716            let want = flip ^ same;
717            match turned[g] {
718                None => {
719                    turned[g] = Some(want);
720                    found.push(g);
721                }
722                Some(have) if have != want => {
723                    return Err(GeopError::new(format!(
724                        "faces {} and {} cannot be turned to wind the same way along their shared edge {}: the sheets make a one-sided surface",
725                        names.faces[f], names.faces[g], names.edges[e]
726                    )));
727                }
728                Some(_) => {}
729            }
730        }
731        Ok(found)
732    })?;
733    if let Some(f) = turned.iter().position(Option::is_none) {
734        return Err(GeopError::new(format!(
735            "face {} meets none of the others along an edge: the sheets do not hang together",
736            names.faces[f]
737        )));
738    }
739    for (f, t) in turned.into_iter().enumerate() {
740        if t == Some(true) {
741            spec.faces[f] = spec.faces[f].reversed();
742        }
743    }
744    Ok(())
745}
746
747#[cfg(test)]
748mod tests;