Skip to main content

geop_core_topology/
build.rs

1//! Building a whole body at once from a description of it ([`BodySpec`]),
2//! and describing part of a model the same way ([`Model::body_spec`]).
3//!
4//! A sweep comes in many shapes (one or several spans, closed around an
5//! axis or not, with poles, holes, caps or none). Grown one entity at a
6//! time, each would need its own sequence of steps, each keeping the model
7//! valid. A sweep is far simpler said whole: these vertices, these edges
8//! between them, these faces, each bounded by these coedges.
9//! [`Model::build_body`] takes exactly that, checks it is topologically
10//! consistent, and only then inserts it. A description without faces is a
11//! [`Wire`]: edges and vertices standing on their own.
12
13use std::collections::HashMap;
14
15use serde::{Deserialize, Serialize};
16
17use geop_core_geometry::{contains::surface::boundary_curve, nurb_surface::NurbSurface3D};
18use geop_core_math::{
19    geop_error::{GeopError, GeopResult},
20    primitives::Pose,
21    scalars::Scalar,
22    vector::{Vector2, Vector3},
23};
24
25use crate::{
26    Body, Coedge, CoedgeGeometry, CoedgeId, Curve2, Curve3, Edge, EdgeId, Face, FaceId, Model,
27    Sense, Shell, ShellId, Solid, SolidId, Vertex, VertexId, Wire, WireId, boundary::BoundaryType,
28};
29
30/// A body to build, its entities referring to each other by their index in
31/// the lists here: a solid, a sheet, or — with no faces — a wire. It
32/// serializes exactly (see [`geop_core_math::exact`]): a body kept to be
33/// built again is the body that was described.
34#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
35pub struct BodySpec {
36    #[serde(with = "geop_core_math::exact")]
37    pub vertices: Vec<Vector3>,
38    pub edges: Vec<EdgeSpec>,
39    pub faces: Vec<FaceSpec>,
40    /// Each shell's faces. Every face is in exactly one.
41    pub shells: Vec<Vec<usize>>,
42    /// Whether the shells bound a solid — then they must be closed, every
43    /// edge used by exactly two faces — or are each a sheet of their own.
44    pub solid: bool,
45}
46
47/// An edge: its curve, running from vertex `start` to vertex `end`.
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct EdgeSpec {
50    #[serde(with = "geop_core_math::exact")]
51    pub curve: Curve3,
52    pub start: usize,
53    pub end: usize,
54}
55
56/// A face: its surface, bounded by its `outer` loop and with its `holes`.
57#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
58pub struct FaceSpec {
59    #[serde(with = "geop_core_math::exact")]
60    pub surface: NurbSurface3D,
61    pub outer: Vec<CoedgeSpec>,
62    pub holes: Vec<Vec<CoedgeSpec>>,
63}
64
65/// One coedge of a loop, in the order the loop runs, with its pcurve on the
66/// loop's face.
67#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
68pub struct CoedgeSpec {
69    pub on: CoedgeOn,
70    #[serde(with = "geop_core_math::exact")]
71    pub pcurve: Curve2,
72}
73
74/// What a [`CoedgeSpec`] runs along: an edge, in a sense, or — a degenerate
75/// coedge, see [`CoedgeGeometry::Vertex`] — a single vertex.
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
77pub enum CoedgeOn {
78    Edge(usize, Sense),
79    Vertex(usize),
80}
81
82/// The faces around a vertex of a [`BodySpec`], each once, with where the
83/// vertex is on the face's surface (see [`BodySpec::vertex_faces`]).
84pub type FacesAround = Vec<(usize, Vector2)>;
85
86/// The ids [`Model::build_body`] gave what a [`BodySpec`] describes, index
87/// for index.
88#[derive(Clone, Debug)]
89pub struct BuiltBody {
90    pub vertices: Vec<VertexId>,
91    pub edges: Vec<EdgeId>,
92    pub faces: Vec<FaceId>,
93    pub shells: Vec<ShellId>,
94    pub solid: Option<SolidId>,
95    /// The wire, for a description without faces.
96    pub wire: Option<WireId>,
97}
98
99impl BuiltBody {
100    /// The body built: its solid, or else its first shell as a sheet. A
101    /// description without faces built a wire, which is no body.
102    pub fn body(&self) -> GeopResult<Body> {
103        match (self.solid, self.shells.first()) {
104            (Some(solid), _) => Ok(Body::Solid(solid)),
105            (None, Some(&shell)) => Ok(Body::Sheet(shell)),
106            (None, None) => Err(GeopError::new("built a wire, which is no body")),
107        }
108    }
109}
110
111/// Where each entity of a [`BodySpec`] made by [`Model::body_spec`] came
112/// from, index for index.
113#[derive(Clone, Debug)]
114pub struct SpecSources {
115    pub vertices: Vec<VertexId>,
116    pub edges: Vec<EdgeId>,
117    pub faces: Vec<FaceId>,
118}
119
120impl FaceSpec {
121    /// A face on `surface` over the unit square of its parameters, bounded
122    /// by `sides` counter-clockwise from `(0, 0)`: through `(1, 0)`,
123    /// `(1, 1)` and `(0, 1)`, each side a straight pcurve.
124    pub fn on_unit_square(surface: NurbSurface3D, sides: [CoedgeOn; 4]) -> GeopResult<Self> {
125        let corner = |k: usize| {
126            let (u, v) = [
127                (Scalar::ZERO, Scalar::ZERO),
128                (Scalar::ONE, Scalar::ZERO),
129                (Scalar::ONE, Scalar::ONE),
130                (Scalar::ZERO, Scalar::ONE),
131            ][k % 4];
132            Vector2::from_array([u, v])
133        };
134        let outer = sides
135            .into_iter()
136            .enumerate()
137            .map(|(k, on)| {
138                let pcurve = Curve2::line(corner(k), corner(k + 1))?;
139                Ok(CoedgeSpec { on, pcurve })
140            })
141            .collect::<GeopResult<_>>()?;
142        Ok(Self {
143            surface,
144            outer,
145            holes: Vec::new(),
146        })
147    }
148
149    /// Its loops, the outer one first.
150    pub fn loops(&self) -> Vec<&Vec<CoedgeSpec>> {
151        std::iter::once(&self.outer).chain(&self.holes).collect()
152    }
153
154    /// The same face with its material side turned around, exactly as
155    /// [`Model::reverse_face`] turns a face of a model around: the surface
156    /// mirrored in `u`, each pcurve mirrored with it, and every loop run the
157    /// other way.
158    pub fn reversed(&self) -> Self {
159        let (u_lo, u_hi) = self.surface.domain_u();
160        let span = u_lo.add(u_hi);
161        let reverse = |lp: &Vec<CoedgeSpec>| {
162            lp.iter()
163                .rev()
164                .map(|c| {
165                    let mut pcurve = c.pcurve.clone();
166                    crate::edit::reverse_face::mirror_u(&mut pcurve, span);
167                    CoedgeSpec {
168                        on: match c.on {
169                            CoedgeOn::Edge(e, sense) => CoedgeOn::Edge(e, sense.opposite()),
170                            on @ CoedgeOn::Vertex(_) => on,
171                        },
172                        pcurve: pcurve.reverse(),
173                    }
174                })
175                .collect()
176        };
177        Self {
178            surface: self.surface.reverse_u(),
179            outer: reverse(&self.outer),
180            holes: self.holes.iter().map(reverse).collect(),
181        }
182    }
183}
184
185impl BodySpec {
186    /// An empty description, to add a body's entities to: of a solid if
187    /// `solid`, else of sheets or a wire.
188    pub fn new(solid: bool) -> Self {
189        Self {
190            vertices: Vec::new(),
191            edges: Vec::new(),
192            faces: Vec::new(),
193            shells: Vec::new(),
194            solid,
195        }
196    }
197
198    /// A sheet of one face on `surface` over the whole of its domain,
199    /// bounded by the surface's four sides, each its boundary row (see
200    /// [`boundary_curve`]): what cuts a body along a surface, as a split
201    /// does. Fails for a surface not clamped at a side, or one a side of
202    /// which collapses to a point — a pole has no edge.
203    pub fn sheet_on(surface: NurbSurface3D) -> GeopResult<Self> {
204        let ((u0, u1), (v0, v1)) = (surface.domain_u(), surface.domain_v());
205        let corners = [(u0, v0), (u1, v0), (u1, v1), (u0, v1)];
206        let mut spec = BodySpec::new(false);
207        for &(u, v) in &corners {
208            spec.vertices.push(surface.evaluate(u, v)?);
209        }
210        // Counter-clockwise in `(u, v)` from `(u0, v0)`: each side as
211        // `boundary_curve` runs it — along the free parameter, upward — and
212        // whether the loop runs it the other way.
213        let sides = [
214            (false, true, false),
215            (true, false, false),
216            (false, false, true),
217            (true, true, true),
218        ];
219        let mut outer = Vec::new();
220        for (k, &(u_fixed, first, reversed)) in sides.iter().enumerate() {
221            let (from, to) = (k, (k + 1) % 4);
222            if spec.vertices[from].could_be_equal(&spec.vertices[to]) {
223                return Err(GeopError::new(format!(
224                    "a sheet on a surface whose side {k} collapses to a point, {:?}, is not built",
225                    spec.vertices[from]
226                )));
227            }
228            let curve = boundary_curve(&surface, u_fixed, first).ok_or_else(|| {
229                GeopError::new(format!(
230                    "a sheet on a surface not clamped at its side {k} is not built"
231                ))
232            })?;
233            let (start, end) = if reversed { (to, from) } else { (from, to) };
234            spec.edges.push(EdgeSpec { curve, start, end });
235            let corner = |i: usize| Vector2::from_array([corners[i].0, corners[i].1]);
236            outer.push(CoedgeSpec {
237                on: CoedgeOn::Edge(
238                    k,
239                    if reversed {
240                        Sense::Reversed
241                    } else {
242                        Sense::Forward
243                    },
244                ),
245                pcurve: Curve2::line(corner(from), corner(to))?,
246            });
247        }
248        spec.faces.push(FaceSpec {
249            surface,
250            outer,
251            holes: Vec::new(),
252        });
253        spec.shells = vec![vec![0]];
254        Ok(spec)
255    }
256
257    /// A face on `surface` over the unit square of its parameters (see
258    /// [`FaceSpec::on_unit_square`]), its corners at the vertices `corners`:
259    /// each side along the edge — or at the vertex — `sides` gives, each
260    /// edge in the sense it runs from one corner to the next. Fails for an
261    /// edge that does not join its side's corners.
262    pub fn square_face(
263        &self,
264        surface: NurbSurface3D,
265        corners: [usize; 4],
266        sides: [CoedgeOn; 4],
267    ) -> GeopResult<FaceSpec> {
268        let mut oriented = sides;
269        for (k, on) in oriented.iter_mut().enumerate() {
270            if let CoedgeOn::Edge(e, _) = *on {
271                let edge = &self.edges[e];
272                let (from, to) = (corners[k], corners[(k + 1) % 4]);
273                let sense = if (edge.start, edge.end) == (from, to) {
274                    Sense::Forward
275                } else if (edge.start, edge.end) == (to, from) {
276                    Sense::Reversed
277                } else {
278                    return Err(GeopError::new(format!(
279                        "a face's side runs from vertex {from} to {to} along edge {e}, between {} and {}",
280                        edge.start, edge.end
281                    )));
282                };
283                *on = CoedgeOn::Edge(e, sense);
284            }
285        }
286        FaceSpec::on_unit_square(surface, oriented)
287    }
288
289    /// The same body moved by the rigid motion `pose`: its vertices, edge
290    /// curves and surfaces moved, its pcurves — on surfaces moved with the
291    /// same parametrization — as they are.
292    pub fn placed(&self, pose: &Pose) -> Self {
293        Self {
294            vertices: self.vertices.iter().map(|p| pose.apply(p)).collect(),
295            edges: self
296                .edges
297                .iter()
298                .map(|e| EdgeSpec {
299                    curve: e.curve.transform(&pose.motion()),
300                    ..e.clone()
301                })
302                .collect(),
303            faces: self
304                .faces
305                .iter()
306                .map(|f| FaceSpec {
307                    surface: f.surface.transform(&pose.motion()),
308                    ..f.clone()
309                })
310                .collect(),
311            shells: self.shells.clone(),
312            solid: self.solid,
313        }
314    }
315
316    /// Takes the vertex `v` apart: the edge ends `moved` — an edge, and
317    /// whether its end (or else its start) — go to a new vertex at `point`,
318    /// and a new edge on `curve`, from `v` to the new vertex, runs between
319    /// them in every loop the move breaks, its pcurve on face `f`, run in
320    /// `sense`, `pcurve(f, sense)`. The new vertex and the new edge.
321    pub fn split_vertex(
322        &mut self,
323        v: usize,
324        moved: &[(usize, bool)],
325        point: Vector3,
326        curve: Curve3,
327        mut pcurve: impl FnMut(usize, Sense) -> GeopResult<Curve2>,
328    ) -> GeopResult<(usize, usize)> {
329        let w = self.vertices.len();
330        self.vertices.push(point);
331        for &(e, at_end) in moved {
332            let edge = (self.edges.get_mut(e))
333                .ok_or_else(|| GeopError::new(format!("BodySpec: no edge {e}")))?;
334            let end = if at_end {
335                &mut edge.end
336            } else {
337                &mut edge.start
338            };
339            if *end != v {
340                return Err(GeopError::new(format!(
341                    "BodySpec: edge {e} does not {} at vertex {v}",
342                    if at_end { "end" } else { "start" }
343                )));
344            }
345            *end = w;
346        }
347        let new = self.edges.len();
348        self.edges.push(EdgeSpec {
349            curve,
350            start: v,
351            end: w,
352        });
353        for f in 0..self.faces.len() {
354            let face = &self.faces[f];
355            let mut loops = Vec::with_capacity(1 + face.holes.len());
356            for lp in face.loops() {
357                let mut out = Vec::with_capacity(lp.len() + 1);
358                for (k, c) in lp.iter().enumerate() {
359                    out.push(c.clone());
360                    let (_, end) = self.ends(c.on)?;
361                    let (start, _) = self.ends(lp[(k + 1) % lp.len()].on)?;
362                    let sense = match (end, start) {
363                        (a, b) if a == b => continue,
364                        (a, b) if (a, b) == (v, w) => Sense::Forward,
365                        (a, b) if (a, b) == (w, v) => Sense::Reversed,
366                        (a, b) => {
367                            return Err(GeopError::new(format!(
368                                "BodySpec: a loop of face {f} runs to vertex {a} and on from vertex {b}"
369                            )));
370                        }
371                    };
372                    out.push(CoedgeSpec {
373                        on: CoedgeOn::Edge(new, sense),
374                        pcurve: pcurve(f, sense)?,
375                    });
376                }
377                loops.push(out);
378            }
379            let mut loops = loops.into_iter();
380            let face = &mut self.faces[f];
381            face.outer = loops.next().unwrap_or_default();
382            face.holes = loops.collect();
383        }
384        Ok((w, new))
385    }
386
387    /// The vertex a coedge running along `on` starts at, and the one it
388    /// ends at.
389    pub fn ends(&self, on: CoedgeOn) -> GeopResult<(usize, usize)> {
390        match on {
391            CoedgeOn::Edge(e, sense) => {
392                let edge = self
393                    .edges
394                    .get(e)
395                    .ok_or_else(|| GeopError::new(format!("BodySpec: no edge {e}")))?;
396                Ok(match sense {
397                    Sense::Forward => (edge.start, edge.end),
398                    Sense::Reversed => (edge.end, edge.start),
399                })
400            }
401            CoedgeOn::Vertex(v) => Ok((v, v)),
402        }
403    }
404
405    /// Per edge, every use of it by a face's coedge — the face, the
406    /// coedge's sense and its pcurve — in the order of the faces.
407    pub fn edge_uses(&self) -> Vec<Vec<(usize, Sense, &Curve2)>> {
408        let mut uses = vec![Vec::new(); self.edges.len()];
409        for (f, face) in self.faces.iter().enumerate() {
410            for lp in face.loops() {
411                for c in lp {
412                    if let CoedgeOn::Edge(e, sense) = c.on {
413                        uses[e].push((f, sense, &c.pcurve));
414                    }
415                }
416            }
417        }
418        uses
419    }
420
421    /// Per vertex, every face around it once, in the order of the faces,
422    /// with where the vertex is on the face's surface: where the first of
423    /// the face's coedges arriving there ends.
424    pub fn vertex_faces(&self) -> GeopResult<Vec<FacesAround>> {
425        let mut around: Vec<FacesAround> = vec![Vec::new(); self.vertices.len()];
426        for (f, face) in self.faces.iter().enumerate() {
427            for lp in face.loops() {
428                for c in lp {
429                    let (_, end) = self.ends(c.on)?;
430                    if !around[end].iter().any(|&(g, _)| g == f) {
431                        around[end].push((f, c.pcurve.ends()?.1));
432                    }
433                }
434            }
435        }
436        Ok(around)
437    }
438
439    /// Whether it describes a wire: vertices, but no faces.
440    pub fn is_wire(&self) -> bool {
441        self.faces.is_empty() && !self.vertices.is_empty()
442    }
443
444    /// Checks that the description is a consistent body: every index
445    /// names something, every loop closes up — each coedge ends where the
446    /// next one starts — every face is in exactly one shell, every edge and
447    /// vertex is used, and an edge is used at most twice, then in opposite
448    /// senses — and, for a solid, exactly twice. A wire's edges are used by
449    /// no face, and its vertices need not be used at all: a point on its
450    /// own is a vertex of the wire.
451    pub fn check(&self) -> GeopResult<()> {
452        let fail = |what: String| Err(GeopError::new(format!("BodySpec: {what}")));
453        for (e, edge) in self.edges.iter().enumerate() {
454            if edge.start >= self.vertices.len() || edge.end >= self.vertices.len() {
455                return fail(format!("edge {e} runs between vertices that do not exist"));
456            }
457        }
458        if self.is_wire() {
459            if self.solid || !self.shells.is_empty() {
460                return fail("a solid or a shell without faces".to_string());
461            }
462            return Ok(());
463        }
464        let mut vertex_used = vec![false; self.vertices.len()];
465        for edge in &self.edges {
466            vertex_used[edge.start] = true;
467            vertex_used[edge.end] = true;
468        }
469        for (f, face) in self.faces.iter().enumerate() {
470            for (l, lp) in face.loops().into_iter().enumerate() {
471                if lp.is_empty() {
472                    return fail(format!("loop {l} of face {f} is empty"));
473                }
474                for (k, coedge) in lp.iter().enumerate() {
475                    let (_, end) = self.ends(coedge.on)?;
476                    let (start, _) = self.ends(lp[(k + 1) % lp.len()].on)?;
477                    if end != start {
478                        return fail(format!(
479                            "loop {l} of face {f}: coedge {k} ends at vertex {end}, but the next one starts at vertex {start}"
480                        ));
481                    }
482                    if let CoedgeOn::Vertex(v) = coedge.on {
483                        match vertex_used.get_mut(v) {
484                            Some(used) => *used = true,
485                            None => {
486                                return fail(format!(
487                                    "face {f} sits at vertex {v}, which does not exist"
488                                ));
489                            }
490                        }
491                    }
492                }
493            }
494        }
495        // Every edge index names an edge: `ends` checked each above.
496        for (e, uses) in self.edge_uses().iter().enumerate() {
497            let senses: Vec<Sense> = uses.iter().map(|&(_, sense, _)| sense).collect();
498            match senses[..] {
499                [] => return fail(format!("edge {e} is used by no face")),
500                [_] if self.solid => {
501                    return fail(format!(
502                        "edge {e} is used by one face only, so the solid is open"
503                    ));
504                }
505                [_] => {}
506                [a, b] if a != b => {}
507                [_, _] => return fail(format!("edge {e} is used twice in the same sense")),
508                // Where two lumps of a solid touch along an edge, it is
509                // used by four faces, as many one way as the other.
510                _ if senses.len().is_multiple_of(2)
511                    && 2 * senses.iter().filter(|&&s| s == Sense::Forward).count()
512                        == senses.len() => {}
513                _ => {
514                    return fail(format!(
515                        "edge {e} is used {} times, not as often one way as the other",
516                        uses.len()
517                    ));
518                }
519            }
520        }
521        if let Some(v) = vertex_used.iter().position(|used| !used) {
522            return fail(format!("vertex {v} is used by nothing"));
523        }
524        let mut shell_of = vec![None; self.faces.len()];
525        for (s, faces) in self.shells.iter().enumerate() {
526            for &f in faces {
527                match shell_of.get_mut(f) {
528                    None => return fail(format!("shell {s} lists face {f}, which does not exist")),
529                    Some(Some(other)) => {
530                        return fail(format!("face {f} is in both shell {other} and shell {s}"));
531                    }
532                    Some(slot) => *slot = Some(s),
533                }
534            }
535        }
536        if let Some(f) = shell_of.iter().position(Option::is_none) {
537            return fail(format!("face {f} is in no shell"));
538        }
539        Ok(())
540    }
541}
542
543impl Model {
544    /// Adds the body `spec` describes, once [`BodySpec::check`] has found
545    /// it consistent — nothing is added otherwise. One without faces is
546    /// added as a [`Wire`].
547    pub fn build_body(&mut self, spec: BodySpec) -> GeopResult<BuiltBody> {
548        spec.check()?;
549        let BodySpec {
550            vertices,
551            edges,
552            faces,
553            shells,
554            solid,
555        } = spec;
556        let vertices: Vec<VertexId> = vertices
557            .into_iter()
558            .map(|point| self.insert_vertex(Vertex { point }))
559            .collect();
560        let edges: Vec<EdgeId> = edges
561            .into_iter()
562            .map(|e| {
563                self.insert_edge(Edge {
564                    curve: e.curve,
565                    start_vertex: vertices[e.start],
566                    end_vertex: vertices[e.end],
567                })
568            })
569            .collect();
570        let shell_ids: Vec<ShellId> = shells
571            .iter()
572            .map(|faces| {
573                self.insert_shell(Shell {
574                    faces: Vec::with_capacity(faces.len()),
575                    solid: None,
576                })
577            })
578            .collect();
579        let mut shell_of = vec![ShellId(0); faces.len()];
580        for (s, members) in shells.iter().enumerate() {
581            for &f in members {
582                shell_of[f] = shell_ids[s];
583            }
584        }
585
586        let mut face_ids = Vec::with_capacity(faces.len());
587        for (f, face) in faces.into_iter().enumerate() {
588            let face_id = self.insert_face(Face {
589                surface: face.surface,
590                // Replaced by the real outer loop just below.
591                outer: BoundaryType::Vertex(vertices[0]),
592                holes: Vec::new(),
593                shell: shell_of[f],
594            });
595            let insert_loop = |model: &mut Self, lp: Vec<CoedgeSpec>| {
596                let ids: Vec<CoedgeId> = lp
597                    .into_iter()
598                    .map(|c| {
599                        let (geometry, sense) = match c.on {
600                            CoedgeOn::Edge(e, sense) => (CoedgeGeometry::Edge(edges[e]), sense),
601                            CoedgeOn::Vertex(v) => {
602                                (CoedgeGeometry::Vertex(vertices[v]), Sense::Forward)
603                            }
604                        };
605                        model.insert_coedge(Coedge {
606                            geometry,
607                            sense,
608                            pcurve: c.pcurve,
609                            // Linked just below, once every id exists.
610                            next: CoedgeId(0),
611                            prev: CoedgeId(0),
612                            face: face_id,
613                        })
614                    })
615                    .collect();
616                let n = ids.len();
617                for (k, &id) in ids.iter().enumerate() {
618                    let coedge = model.coedges.get_mut(&id).expect("just inserted");
619                    coedge.next = ids[(k + 1) % n];
620                    coedge.prev = ids[(k + n - 1) % n];
621                }
622                BoundaryType::Loop(ids[0])
623            };
624            let outer = insert_loop(self, face.outer);
625            let holes = face
626                .holes
627                .into_iter()
628                .map(|h| insert_loop(self, h))
629                .collect();
630            let built = self.faces.get_mut(&face_id).expect("just inserted");
631            built.outer = outer;
632            built.holes = holes;
633            self.get_shell_mut(shell_of[f])?.faces.push(face_id);
634            face_ids.push(face_id);
635        }
636
637        let solid = solid.then(|| {
638            self.insert_solid(Solid {
639                shells: shell_ids.clone(),
640            })
641        });
642        for &shell in &shell_ids {
643            self.get_shell_mut(shell)?.solid = solid;
644        }
645        let wire = (face_ids.is_empty() && !vertices.is_empty()).then(|| {
646            self.insert_wire(Wire {
647                vertices: vertices.clone(),
648                edges: edges.clone(),
649            })
650        });
651        Ok(BuiltBody {
652            vertices,
653            edges,
654            faces: face_ids,
655            shells: shell_ids,
656            solid,
657            wire,
658        })
659    }
660
661    /// `faces` described as a body of one shell — a solid's if `solid` —
662    /// with every vertex and edge they use, copied: building it makes a
663    /// copy of them, sharing nothing with the originals. Also says where in
664    /// the model each entity of the description came from.
665    pub fn body_spec(&self, faces: &[FaceId], solid: bool) -> GeopResult<(BodySpec, SpecSources)> {
666        let mut vertex_index: HashMap<VertexId, usize> = HashMap::new();
667        let mut edge_index: HashMap<EdgeId, usize> = HashMap::new();
668        let mut sources = SpecSources {
669            vertices: Vec::new(),
670            edges: Vec::new(),
671            faces: faces.to_vec(),
672        };
673        let mut spec = BodySpec {
674            vertices: Vec::new(),
675            edges: Vec::new(),
676            faces: Vec::with_capacity(faces.len()),
677            shells: vec![(0..faces.len()).collect()],
678            solid,
679        };
680        let mut vertex = |spec: &mut BodySpec, sources: &mut SpecSources, id: VertexId| {
681            *vertex_index.entry(id).or_insert_with(|| {
682                spec.vertices.push(self.vertices[&id].point);
683                sources.vertices.push(id);
684                spec.vertices.len() - 1
685            })
686        };
687        for &face_id in faces {
688            let face = self.get_face(face_id)?;
689            let mut loops = Vec::new();
690            for boundary in face.boundaries() {
691                let BoundaryType::Loop(anchor) = boundary else {
692                    return Err(GeopError::new(format!(
693                        "Model::body_spec: face {face_id} is bounded by a bare vertex"
694                    )));
695                };
696                let mut lp = Vec::new();
697                for coedge_id in self.iterate_loop_coedges(anchor) {
698                    let coedge = self.get_coedge(coedge_id)?;
699                    let on = match coedge.geometry {
700                        CoedgeGeometry::Edge(e) => {
701                            let index = match edge_index.get(&e) {
702                                Some(&index) => index,
703                                None => {
704                                    let edge = self.get_edge(e)?;
705                                    let start = vertex(&mut spec, &mut sources, edge.start_vertex);
706                                    let end = vertex(&mut spec, &mut sources, edge.end_vertex);
707                                    spec.edges.push(EdgeSpec {
708                                        curve: edge.curve.clone(),
709                                        start,
710                                        end,
711                                    });
712                                    sources.edges.push(e);
713                                    edge_index.insert(e, spec.edges.len() - 1);
714                                    spec.edges.len() - 1
715                                }
716                            };
717                            CoedgeOn::Edge(index, coedge.sense)
718                        }
719                        CoedgeGeometry::Vertex(v) => {
720                            CoedgeOn::Vertex(vertex(&mut spec, &mut sources, v))
721                        }
722                    };
723                    lp.push(CoedgeSpec {
724                        on,
725                        pcurve: coedge.pcurve.clone(),
726                    });
727                }
728                loops.push(lp);
729            }
730            let outer = loops.remove(0);
731            spec.faces.push(FaceSpec {
732                surface: face.surface.clone(),
733                outer,
734                holes: loops,
735            });
736        }
737        Ok((spec, sources))
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use geop_core_math::{scalars::Scalar, vector::Vector3};
744
745    use super::{BodySpec, EdgeSpec};
746    use crate::{
747        Body, CoedgeGeometry, Model,
748        test_fixtures::test_cube_solid,
749        validation::{ValidationParameters, validate, validate_manifold},
750    };
751
752    fn point(p: [f64; 3]) -> Vector3 {
753        Vector3::from_array(p.map(Scalar::from_f64))
754    }
755
756    fn line(a: [f64; 3], b: [f64; 3]) -> crate::Curve3 {
757        let p = |p: [f64; 3]| Vector3::from_array(p.map(Scalar::from_f64));
758        crate::Curve3::line(p(a), p(b)).unwrap()
759    }
760
761    /// Two lines from one corner, and a point on its own, beside the unit
762    /// cube.
763    fn wire_spec() -> BodySpec {
764        let corners = [[2.0, 0.0, 0.0], [3.0, 0.0, 0.0], [2.0, 1.0, 0.0]];
765        BodySpec {
766            vertices: corners
767                .iter()
768                .map(|&p| point(p))
769                .chain([point([2.0, 0.0, 1.0])])
770                .collect(),
771            edges: vec![
772                EdgeSpec {
773                    curve: line(corners[0], corners[1]),
774                    start: 0,
775                    end: 1,
776                },
777                EdgeSpec {
778                    curve: line(corners[0], corners[2]),
779                    start: 0,
780                    end: 2,
781                },
782            ],
783            faces: Vec::new(),
784            shells: Vec::new(),
785            solid: false,
786        }
787    }
788
789    /// A description without faces builds a wire: its edges used by no
790    /// face, a point on its own among its vertices — valid beside a solid,
791    /// whose manifold check it leaves alone.
792    #[test]
793    fn a_spec_without_faces_builds_a_wire() {
794        let mut model = Model::new();
795        test_cube_solid(&mut model);
796        let built = model.build_body(wire_spec()).unwrap();
797        let wire = built.wire.expect("a wire");
798        assert_eq!(built.solid, None);
799        let built_wire = &model.wires[&wire];
800        assert_eq!(built_wire.vertices.len(), 4);
801        assert_eq!(built_wire.edges.len(), 2);
802        assert!(built_wire.vertices.contains(&built.vertices[3]));
803        assert_eq!(model.wire_of_edge(built.edges[1]), Some(wire));
804        let params = ValidationParameters::default();
805        validate(&params, &model).unwrap();
806        validate_manifold(&params, &model).unwrap();
807    }
808
809    /// Deleting a solid deletes what only its faces reached, and leaves a
810    /// wire as it is.
811    #[test]
812    fn a_wire_outlives_a_deleted_solid() {
813        let mut model = Model::new();
814        let solid = test_cube_solid(&mut model);
815        model.build_body(wire_spec()).unwrap();
816        model.assemble_solid(&[Body::Solid(solid)], &[]).unwrap();
817        assert_eq!(model.vertices.len(), 4);
818        assert_eq!(model.edges.len(), 2);
819        validate(&ValidationParameters::default(), &model).unwrap();
820    }
821
822    /// A face using an edge of a wire makes it no wire's: invalid.
823    #[test]
824    fn a_face_on_a_wire_edge_is_invalid() {
825        let mut model = Model::new();
826        test_cube_solid(&mut model);
827        let built = model.build_body(wire_spec()).unwrap();
828        let first = *model.coedges.keys().next().unwrap();
829        let coedge = model.coedges.get_mut(&first).unwrap();
830        coedge.geometry = CoedgeGeometry::Edge(built.edges[0]);
831        let errors = validate(&ValidationParameters::default(), &model).unwrap_err();
832        assert!(
833            errors
834                .iter()
835                .any(|e| format!("{e:?}").contains("which bounds no face")),
836            "{errors:?}"
837        );
838    }
839
840    /// Two edges of a wire that cross without a vertex there are invalid,
841    /// as two edges of a solid would be.
842    #[test]
843    fn crossing_wire_edges_are_invalid() {
844        let mut model = Model::new();
845        let mut spec = wire_spec();
846        spec.vertices.push(point([2.5, -1.0, 0.0]));
847        spec.vertices.push(point([2.5, 1.0, 0.0]));
848        spec.edges.push(EdgeSpec {
849            curve: line([2.5, -1.0, 0.0], [2.5, 1.0, 0.0]),
850            start: 4,
851            end: 5,
852        });
853        model.build_body(spec).unwrap();
854        assert!(validate(&ValidationParameters::default(), &model).is_err());
855    }
856}