Skip to main content

geop_lib_sweep/
skin.rs

1//! A body skinned over sections: the topology a sweep, a loft and a blend's
2//! tool share, built into a [`BodySpec`] with a name for everything (see
3//! [`skin`]).
4//!
5//! A skinned body is made of *strips*, each a section loop — joints joined
6//! by curves — carried through the stations of a path: every joint sits at
7//! every station (a vertex), every curve lies at every station (an edge),
8//! every joint travels along every span between two stations (a *lateral*
9//! edge), and every curve sweeps a *wall* face through every span. Where
10//! the path is open, the strips may be closed off by flat caps at either
11//! end. Where a strip's geometry comes from — sections placed by frames,
12//! or control rows given outright — is its own ([`StripGeometry`]); the
13//! grid is the same.
14//!
15//! Some of the grid collapses. A *pole* is a joint that stays put — on a
16//! revolve's axis — and is one vertex at every station. A joint's two ends
17//! of a span being one vertex, it has no lateral edge there, and the walls
18//! beside it close over it with a coedge sitting at the vertex (see
19//! [`CoedgeOn::Vertex`]). A curve *on the axis* runs between two poles,
20//! sweeps no wall, and is one edge at every station — or none, where no cap
21//! bounds it. An *apex* is a station where the whole loop collapses to one
22//! point, as a loft to a point ends: one vertex for every joint, no edges
23//! there, and each wall closes over it with a coedge sitting at the vertex.
24//! And a strip may be joined to what was built before it: a
25//! joint at a station to a vertex already in the spec, or a whole station
26//! to a station of an earlier strip, with its vertices and its curves'
27//! edges ([`Glue`]).
28//!
29//! Each wall is parametrized `u` along its span and `v` along its curve
30//! (see [`StripGeometry::wall`]); its sides in those parameters are the
31//! curve at the span's first station, run backwards along `u = 0`, the path
32//! of the curve's start along `v = 0`, the curve at the span's last station
33//! along `u = 1` and the path of its end, backwards along `v = 1`.
34
35use geop_core_geometry::{
36    nurb_curve::{NurbCurve2D, NurbCurve3D},
37    nurb_surface::NurbSurface3D,
38};
39use geop_core_math::{
40    geop_error::{GeopError, GeopResult},
41    scalars::Scalar,
42    vector::{Vector2, Vector3},
43};
44use geop_core_topology::{
45    Sense,
46    build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
47};
48
49use geop_ops_model::{BodyNames, Namer};
50
51/// Where a strip's entities are: its joints, curves, laterals and walls,
52/// by station `s`, span `j`, joint or curve `i`.
53pub trait StripGeometry {
54    /// Joint `i` at station `s`.
55    fn joint(&self, s: usize, i: usize) -> GeopResult<Vector3>;
56    /// Curve `i` at station `s`, from joint `i` to the next.
57    fn curve(&self, s: usize, i: usize) -> GeopResult<NurbCurve3D>;
58    /// The path of joint `i` through span `j`, from its first station to
59    /// its last.
60    fn lateral(&self, j: usize, i: usize) -> GeopResult<NurbCurve3D>;
61    /// The wall curve `i` sweeps through span `j`: `u` along the span, `v`
62    /// along the curve, both over `[0, 1]`.
63    fn wall(&self, j: usize, i: usize) -> GeopResult<NurbSurface3D>;
64}
65
66/// What a strip is joined to that was built before it.
67#[derive(Clone, Debug)]
68pub enum Glue {
69    /// Joint `joint` at station `station` is the spec's vertex `vertex`.
70    Vertex {
71        station: usize,
72        joint: usize,
73        vertex: usize,
74    },
75    /// Station `station` is station `at` of the earlier strip `strip`: its
76    /// joint `i` that one's `joints[i]`, and each of its curves the curve
77    /// there between the same vertices, either way round.
78    Section {
79        station: usize,
80        strip: usize,
81        at: usize,
82        joints: Vec<usize>,
83    },
84}
85
86/// One strip of a skinned body (see the module docs): a loop of joints and
87/// the curves between them — curve `i` from joint `i` to the next, around
88/// to the first for a closed loop, which has as many curves as joints; an
89/// open chain has one fewer — through stations and the spans between consecutive ones,
90/// around to the first again on a `closed` path, which has as many spans as
91/// stations; an open one has one fewer.
92///
93/// Everything is named by `namer`, after the joints, curves, stations and
94/// spans:
95///
96/// | entity | name |
97/// |---|---|
98/// | vertex of joint `P` at station `s` | `N(P,s)`; `N(P)` for a pole |
99/// | curve `X` at station `s` | `N(X,s)`; `N(X)` for a curve on the axis |
100/// | lateral edge of `P` through span `q` | `N(P,q)`, or `N(P)` for an unnamed span |
101/// | wall swept by `X` through span `q` | `N(X,q)`, or `N(X)` |
102pub struct Strip<'a> {
103    pub namer: &'a Namer,
104    pub joint_names: &'a [String],
105    pub curve_names: &'a [String],
106    pub station_names: &'a [String],
107    pub span_names: &'a [Option<String>],
108    pub closed: bool,
109    /// Per joint: whether it is a pole.
110    pub poles: &'a [bool],
111    /// Per curve: whether it lies on the axis, between two poles.
112    pub on_axis: &'a [bool],
113    /// Per station, or none for no station: whether the whole loop
114    /// collapses to a point there — an *apex*, as a loft to a point has.
115    pub apex: &'a [bool],
116    pub glued: Vec<Glue>,
117    pub geometry: &'a dyn StripGeometry,
118}
119
120impl Strip<'_> {
121    fn joints(&self) -> usize {
122        self.joint_names.len()
123    }
124
125    /// The joint curve `i` ends at.
126    fn next(&self, i: usize) -> usize {
127        (i + 1) % self.joints()
128    }
129
130    /// The station after station `j`, around a closed path.
131    fn station_after(&self, j: usize) -> usize {
132        (j + 1) % self.station_names.len()
133    }
134
135    /// Whether the loop collapses to a point at station `s`.
136    fn is_apex(&self, s: usize) -> bool {
137        self.apex.get(s).copied().unwrap_or(false)
138    }
139
140    /// `name` qualified by the span `j`'s name, if it has one.
141    fn qualified(&self, name: &str, j: usize) -> String {
142        match &self.span_names[j] {
143            Some(q) => self.namer.name(&[name, q]),
144            None => self.namer.name(&[name]),
145        }
146    }
147
148    fn check(&self) -> GeopResult<()> {
149        let (stations, spans) = (self.station_names.len(), self.span_names.len());
150        let expected = if self.closed {
151            stations
152        } else {
153            stations.saturating_sub(1)
154        };
155        if spans == 0 || spans != expected {
156            return Err(GeopError::new(format!(
157                "skin: a {} path of {stations} stations cannot have {spans} spans",
158                if self.closed { "closed" } else { "open" }
159            )));
160        }
161        let curves = self.curve_names.len();
162        if self.poles.len() != self.joints()
163            || self.on_axis.len() != curves
164            || !(curves == self.joints() || curves + 1 == self.joints())
165        {
166            return Err(GeopError::new(format!(
167                "skin: a loop of {} joints, {} curves, {} pole flags and {} axis flags",
168                self.joints(),
169                self.curve_names.len(),
170                self.poles.len(),
171                self.on_axis.len()
172            )));
173        }
174        if !self.apex.is_empty() {
175            let poles = self.poles.iter().any(|&p| p) || self.on_axis.iter().any(|&a| a);
176            if self.apex.len() != stations || (poles && self.apex.iter().any(|&a| a)) {
177                return Err(GeopError::new(format!(
178                    "skin: {} apex flags for {stations} stations, or an apex with poles",
179                    self.apex.len()
180                )));
181            }
182        }
183        Ok(())
184    }
185}
186
187/// A flat cap closing off strips at one station: on `surface`, its loops
188/// one per strip, the outer first, each the strip's curves there by their
189/// pcurves — each running the way its curve does. At the start of the
190/// strips (`forward`), it runs every loop as the curves do; at the end,
191/// backwards: the walls run them the other way.
192pub struct StripCap {
193    pub name: String,
194    pub station: usize,
195    pub forward: bool,
196    pub surface: NurbSurface3D,
197    pub loops: Vec<(usize, Vec<NurbCurve2D>)>,
198}
199
200/// What [`skin`] built, by strip, station and joint or curve: the vertices,
201/// and the curves' edges, each with whether it runs against its curve —
202/// none for a curve on the axis that no cap bounds.
203pub struct Skinned {
204    pub vertices: Vec<Vec<Vec<usize>>>,
205    pub edges: Vec<Vec<Vec<CurveEdge>>>,
206}
207
208/// The edge of a curve at a station: its index, and whether it runs
209/// against the curve; none for a curve on the axis that no cap bounds.
210pub type CurveEdge = Option<(usize, bool)>;
211
212/// A wall's sides in its own parameters (see the module docs).
213fn wall_pcurves() -> GeopResult<[NurbCurve2D; 4]> {
214    let p = |u: f64, v: f64| Vector2::from_array([Scalar::from_f64(u), Scalar::from_f64(v)]);
215    Ok([
216        NurbCurve2D::line(p(0.0, 1.0), p(0.0, 0.0))?,
217        NurbCurve2D::line(p(0.0, 0.0), p(1.0, 0.0))?,
218        NurbCurve2D::line(p(1.0, 0.0), p(1.0, 1.0))?,
219        NurbCurve2D::line(p(1.0, 1.0), p(0.0, 1.0))?,
220    ])
221}
222
223/// `sense`, the other way where an edge runs `against` its curve.
224fn along(sense: Sense, against: bool) -> Sense {
225    match (sense, against) {
226        (Sense::Forward, true) => Sense::Reversed,
227        (Sense::Reversed, true) => Sense::Forward,
228        (sense, false) => sense,
229    }
230}
231
232/// Adds `strips` and their `caps` to `spec`, named in `names` (see the
233/// module docs): first each strip's vertices, station curves and lateral
234/// edges, then every strip's walls, then the caps.
235pub fn skin(
236    spec: &mut BodySpec,
237    names: &mut BodyNames,
238    strips: &[Strip<'_>],
239    caps: &[StripCap],
240) -> GeopResult<Skinned> {
241    let mut built = Skinned {
242        vertices: Vec::new(),
243        edges: Vec::new(),
244    };
245    // Per strip, joint and span: its lateral edge, or the vertex it
246    // collapses to.
247    let mut laterals: Vec<Vec<Vec<Result<usize, usize>>>> = Vec::new();
248    for (l, strip) in strips.iter().enumerate() {
249        strip.check()?;
250        let stations = strip.station_names.len();
251        let mut glued_vertex = vec![vec![None; strip.joints()]; stations];
252        let mut glued_section = vec![None; stations];
253        for glue in &strip.glued {
254            match glue {
255                Glue::Vertex {
256                    station,
257                    joint,
258                    vertex,
259                } => glued_vertex[*station][*joint] = Some(*vertex),
260                Glue::Section {
261                    station,
262                    strip: m,
263                    at,
264                    joints,
265                } => {
266                    let earlier = built.vertices.get(*m).filter(|_| *m < l).ok_or_else(|| {
267                        GeopError::new(format!(
268                            "skin: strip {l} is joined to strip {m}, which is not built before it"
269                        ))
270                    })?;
271                    for (i, &k) in joints.iter().enumerate() {
272                        glued_vertex[*station][i] = Some(earlier[*at][k]);
273                    }
274                    glued_section[*station] = Some((*m, *at));
275                }
276            }
277        }
278
279        let mut vertex = vec![vec![0; strip.joints()]; stations];
280        // An apex: one vertex for every joint, named after its station.
281        let mut apex_vertex = vec![None; stations];
282        for (s, at) in apex_vertex.iter_mut().enumerate() {
283            if strip.is_apex(s) {
284                spec.vertices.push(strip.geometry.joint(s, 0)?);
285                names
286                    .vertices
287                    .push(strip.namer.name(&[&strip.station_names[s]]));
288                *at = Some(spec.vertices.len() - 1);
289            }
290        }
291        for i in 0..strip.joints() {
292            let name = &strip.joint_names[i];
293            if strip.poles[i] {
294                spec.vertices.push(strip.geometry.joint(0, i)?);
295                names.vertices.push(strip.namer.name(&[name]));
296                for row in vertex.iter_mut() {
297                    row[i] = spec.vertices.len() - 1;
298                }
299                continue;
300            }
301            for s in 0..stations {
302                vertex[s][i] = match apex_vertex[s].or(glued_vertex[s][i]) {
303                    Some(v) => v,
304                    None => {
305                        spec.vertices.push(strip.geometry.joint(s, i)?);
306                        names
307                            .vertices
308                            .push(strip.namer.name(&[name, &strip.station_names[s]]));
309                        spec.vertices.len() - 1
310                    }
311                };
312            }
313        }
314
315        let capped = caps.iter().any(|c| c.loops.iter().any(|(m, _)| *m == l));
316        let mut edges = vec![vec![None; strip.curve_names.len()]; stations];
317        for (i, name) in strip.curve_names.iter().enumerate() {
318            let next = strip.next(i);
319            if strip.on_axis[i] {
320                // One edge for every station — or none, without caps to
321                // bound: no wall uses it.
322                if capped {
323                    spec.edges.push(EdgeSpec {
324                        curve: strip.geometry.curve(0, i)?,
325                        start: vertex[0][i],
326                        end: vertex[0][next],
327                    });
328                    names.edges.push(strip.namer.name(&[name]));
329                    for row in edges.iter_mut() {
330                        row[i] = Some((spec.edges.len() - 1, false));
331                    }
332                }
333                continue;
334            }
335            for s in (0..stations).filter(|&s| !strip.is_apex(s)) {
336                let (from, to) = (vertex[s][i], vertex[s][next]);
337                edges[s][i] = Some(match glued_section[s] {
338                    Some((m, at)) => built.edges[m][at]
339                        .iter()
340                        .flatten()
341                        .find_map(|&(e, _)| {
342                            let edge = &spec.edges[e];
343                            if (edge.start, edge.end) == (from, to) {
344                                Some((e, false))
345                            } else if (edge.start, edge.end) == (to, from) {
346                                Some((e, true))
347                            } else {
348                                None
349                            }
350                        })
351                        .ok_or_else(|| {
352                            GeopError::new(format!(
353                                "skin: strip {l} at station {s} is joined to a section with no curve {name} of its own"
354                            ))
355                        })?,
356                    None => {
357                        spec.edges.push(EdgeSpec {
358                            curve: strip.geometry.curve(s, i)?,
359                            start: from,
360                            end: to,
361                        });
362                        names
363                            .edges
364                            .push(strip.namer.name(&[name, &strip.station_names[s]]));
365                        (spec.edges.len() - 1, false)
366                    }
367                });
368            }
369        }
370
371        let spans = strip.span_names.len();
372        let mut paths = Vec::with_capacity(strip.joints());
373        for (i, joint) in strip.joint_names.iter().enumerate() {
374            let mut path = Vec::with_capacity(spans);
375            for j in 0..spans {
376                let (from, to) = (vertex[j][i], vertex[strip.station_after(j)][i]);
377                if from == to {
378                    path.push(Err(from));
379                    continue;
380                }
381                spec.edges.push(EdgeSpec {
382                    curve: strip.geometry.lateral(j, i)?,
383                    start: from,
384                    end: to,
385                });
386                names.edges.push(strip.qualified(joint, j));
387                path.push(Ok(spec.edges.len() - 1));
388            }
389            paths.push(path);
390        }
391        built.vertices.push(vertex);
392        built.edges.push(edges);
393        laterals.push(paths);
394    }
395
396    let [first_back, start_side, last_forward, end_side] = wall_pcurves()?;
397    for (l, strip) in strips.iter().enumerate() {
398        for (i, name) in strip.curve_names.iter().enumerate() {
399            if strip.on_axis[i] {
400                continue;
401            }
402            let next = strip.next(i);
403            #[expect(
404                clippy::needless_range_loop,
405                reason = "`j` is a span: it picks the paths of two joints, a wall and the stations \
406                          either side, not the items of one sequence"
407            )]
408            for j in 0..strip.span_names.len() {
409                // Along a joint's path, or at its vertex where it has none.
410                let side = |joint: usize, sense: Sense, pcurve: &NurbCurve2D| CoedgeSpec {
411                    on: match laterals[l][joint][j] {
412                        Ok(edge) => CoedgeOn::Edge(edge, sense),
413                        Err(vertex) => CoedgeOn::Vertex(vertex),
414                    },
415                    pcurve: pcurve.clone(),
416                };
417                // Along the curve at a station, the other way where its
418                // edge runs against it; at the apex, where it collapses.
419                let at = |s: usize, sense: Sense, pcurve: &NurbCurve2D| {
420                    let on = if strip.is_apex(s) {
421                        CoedgeOn::Vertex(built.vertices[l][s][i])
422                    } else {
423                        let (edge, against) =
424                            built.edges[l][s][i].expect("a curve off the axis has an edge");
425                        CoedgeOn::Edge(edge, along(sense, against))
426                    };
427                    CoedgeSpec {
428                        on,
429                        pcurve: pcurve.clone(),
430                    }
431                };
432                spec.faces.push(FaceSpec {
433                    surface: strip.geometry.wall(j, i)?,
434                    outer: vec![
435                        at(j, Sense::Reversed, &first_back),
436                        side(i, Sense::Forward, &start_side),
437                        at(strip.station_after(j), Sense::Forward, &last_forward),
438                        side(next, Sense::Reversed, &end_side),
439                    ],
440                    holes: Vec::new(),
441                });
442                names.faces.push(strip.qualified(name, j));
443            }
444        }
445    }
446
447    for cap in caps {
448        let mut boundaries = Vec::with_capacity(cap.loops.len());
449        for (l, pcurves) in &cap.loops {
450            let mut coedges = pcurves
451                .iter()
452                .enumerate()
453                .map(|(i, pcurve)| {
454                    let (edge, against) = built.edges[*l][cap.station][i]
455                        .ok_or_else(|| GeopError::new("skin: a cap's curves have edges"))?;
456                    Ok(if cap.forward {
457                        CoedgeSpec {
458                            on: CoedgeOn::Edge(edge, along(Sense::Forward, against)),
459                            pcurve: pcurve.clone(),
460                        }
461                    } else {
462                        CoedgeSpec {
463                            on: CoedgeOn::Edge(edge, along(Sense::Reversed, against)),
464                            pcurve: pcurve.reverse(),
465                        }
466                    })
467                })
468                .collect::<GeopResult<Vec<_>>>()?;
469            if !cap.forward {
470                coedges.reverse();
471            }
472            boundaries.push(coedges);
473        }
474        if boundaries.is_empty() {
475            return Err(GeopError::new(format!(
476                "skin: the cap {} has no loop",
477                cap.name
478            )));
479        }
480        let outer = boundaries.remove(0);
481        spec.faces.push(FaceSpec {
482            surface: cap.surface.clone(),
483            outer,
484            holes: boundaries,
485        });
486        names.faces.push(cap.name.clone());
487    }
488    Ok(built)
489}