Skip to main content

geop_ops_model/
tangent.rs

1//! [`TangentChain`]: a tangent chain of edges — edges running on into one another
2//! between faces tangent across every vertex of it, as one smooth edge —
3//! found from any edge of it ([`tangent_chain`]), and the [`Link`]s it is
4//! made of: each edge the way the chain runs along it, with the faces
5//! either side.
6
7use geop_core_math::{
8    geop_error::{GeopError, GeopResult, WithContext},
9    scalars::Scalar,
10    solvers::walk::{Walked, walk_by},
11    vector::Vector3,
12    with_context,
13};
14use geop_core_topology::{EdgeId, FaceId, Model, VertexId};
15
16use crate::NamedModel;
17
18/// The faces either side of `edge`: an error for one that does not join
19/// two.
20fn faces(model: &Model, edge: EdgeId) -> GeopResult<[FaceId; 2]> {
21    model.faces_of_edge(edge).ok_or_else(|| {
22        GeopError::new(format!(
23            "edge {edge} does not join two faces, so it is in no chain"
24        ))
25    })
26}
27
28/// One edge of a chain, as the chain runs along it: along the edge's own
29/// direction, `forward`, or against it; with the faces on its left and
30/// right as it runs (seen from outside the solid).
31#[derive(Clone, Debug)]
32pub struct Link {
33    pub edge: EdgeId,
34    pub forward: bool,
35    pub faces: [FaceId; 2],
36}
37
38impl Link {
39    /// The unit tangent of the edge at `p`, where its faces' normals there
40    /// are `normals`: square to both, turned to run `along` the link. An
41    /// error where the faces could meet tangentially.
42    pub fn tangent(
43        &self,
44        normals: [Vector3; 2],
45        along: &Vector3,
46        p: &Vector3,
47    ) -> GeopResult<Vector3> {
48        let across = normals[0].prod_cross(&normals[1]);
49        if across.prod_dot(along).definitely_less(Scalar::ZERO) {
50            across.neg()
51        } else {
52            across
53        }
54        .normalize()
55        .map_err(|_| {
56            GeopError::new(format!(
57                "faces {} and {} could meet tangentially at {p:?} on edge {}: there is no corner to blend there (normals {:?} and {:?})",
58                self.faces[0], self.faces[1], self.edge, normals[0], normals[1]
59            ))
60        })
61    }
62
63    pub fn new(model: &Model, edge: EdgeId, forward: bool) -> GeopResult<Self> {
64        let [l, r] = faces(model, edge)?;
65        Ok(Link {
66            edge,
67            forward,
68            faces: if forward { [l, r] } else { [r, l] },
69        })
70    }
71
72    pub fn reversed(&self) -> Self {
73        Link {
74            edge: self.edge,
75            forward: !self.forward,
76            faces: [self.faces[1], self.faces[0]],
77        }
78    }
79
80    /// The vertex the chain enters the edge at, and the one it leaves at.
81    pub fn vertices(&self, model: &Model) -> GeopResult<[VertexId; 2]> {
82        let e = model.get_edge(self.edge)?;
83        Ok(if self.forward {
84            [e.start_vertex, e.end_vertex]
85        } else {
86            [e.end_vertex, e.start_vertex]
87        })
88    }
89
90    /// The edge's point `fraction` of the way along it as the chain runs —
91    /// of its parameter — and the unit tangent there, the way it runs.
92    pub fn at(&self, model: &Model, fraction: Scalar) -> GeopResult<(Vector3, Vector3)> {
93        let curve = &model.get_edge(self.edge)?.curve;
94        let (t0, t1) = curve.domain();
95        let f = if self.forward {
96            fraction
97        } else {
98            Scalar::ONE.sub(fraction)
99        };
100        let t = Scalar::interpolate(t0, t1, f);
101        let ctx = |e: GeopError| {
102            e.with_context(format!(
103                "Link::at(edge={}, fraction={fraction:?}, t={t:?}): control points {:?}, knots {:?}",
104                self.edge, curve.control_points, curve.knot_vector
105            ))
106        };
107        let tangent = curve.tangent(t).map_err(ctx)?.normalize().map_err(ctx)?;
108        Ok((
109            curve.evaluate(t)?,
110            if self.forward { tangent } else { tangent.neg() },
111        ))
112    }
113}
114
115/// The edges blended as one: a tangent chain (see the module docs), in the
116/// order it runs, around to the first again if `closed`.
117#[derive(Clone, Debug)]
118pub struct TangentChain {
119    pub links: Vec<Link>,
120    pub closed: bool,
121}
122
123impl TangentChain {
124    /// The chain's vertices in order: where each link starts, and — for an
125    /// open chain — where the last one ends.
126    pub fn vertices(&self, model: &Model) -> GeopResult<Vec<VertexId>> {
127        let mut vertices = Vec::new();
128        for link in &self.links {
129            vertices.push(link.vertices(model)?[0]);
130        }
131        if !self.closed {
132            let last = self.last()?;
133            vertices.push(last.vertices(model)?[1]);
134        }
135        Ok(vertices)
136    }
137
138    /// Its first link.
139    pub fn first(&self) -> GeopResult<&Link> {
140        (self.links.first()).ok_or_else(|| GeopError::new("a chain has no links"))
141    }
142
143    /// Its last link.
144    pub fn last(&self) -> GeopResult<&Link> {
145        (self.links.last()).ok_or_else(|| GeopError::new("a chain has no links"))
146    }
147
148    pub fn edges(&self) -> Vec<EdgeId> {
149        self.links.iter().map(|l| l.edge).collect()
150    }
151
152    /// An open chain's start and end: at each its vertex, the faces either
153    /// side of the link there, and the unit direction out of the chain.
154    #[allow(clippy::type_complexity)]
155    pub fn ends(&self, model: &Model) -> GeopResult<[(VertexId, [FaceId; 2], Vector3); 2]> {
156        let vertices = self.vertices(model)?;
157        let first = self.first()?;
158        let last = self.last()?;
159        let (_, t0) = first.at(model, Scalar::ZERO)?;
160        let (_, t1) = last.at(model, Scalar::ONE)?;
161        Ok([
162            (vertices[0], first.faces, t0.neg()),
163            (
164                *vertices
165                    .last()
166                    .ok_or_else(|| GeopError::new("a chain has no vertices"))?,
167                last.faces,
168                t1,
169            ),
170        ])
171    }
172}
173
174/// Whether `faces` could meet tangentially at `vertex`, their normals
175/// there parallel: where the edge between them vanishes, the corner
176/// between the faces opening out flat — two equal pipes joined, where
177/// they touch. Its chain ends there, and a blend of it narrows to nothing.
178pub fn vanishes_at(model: &Model, vertex: VertexId, faces: [FaceId; 2]) -> GeopResult<bool> {
179    let (a, b) = (
180        model.normal_at_vertex(faces[0], vertex)?,
181        model.normal_at_vertex(faces[1], vertex)?,
182    );
183    Ok(a.could_be_parallel(&b) && !a.prod_dot(&b).definitely_less(Scalar::ZERO))
184}
185
186/// Whether the unit vectors `a` and `b` could point the same way.
187fn same_direction(a: &Vector3, b: &Vector3) -> bool {
188    a.could_be_parallel(b) && a.prod_dot(b).definitely_greater(Scalar::ZERO)
189}
190
191/// The link the chain goes on along from `link`, past the vertex it leaves
192/// at: the one other edge there it runs on into tangentially, between faces
193/// tangent to its own — or none. Where more than one is, the edge forks: an
194/// error naming them. Going the other way, from one of them, it does not
195/// fork, so the one picked says which way the chain goes.
196///
197/// Tangency is asked of the faces, whose normals at the vertex the
198/// surfaces state exactly: where both faces either side go on tangentially,
199/// so does the edge between them, which runs along both tangent planes.
200/// The edges' own tangents only tell which way the next one runs on — an
201/// edge traced as an intersection only encloses its points, not its
202/// direction.
203///
204/// That holds only where the faces cross. Where the link's own faces could
205/// be tangent to each other at the vertex — two equal pipes joined, where
206/// they touch — their normals state no direction at all, every edge there
207/// runs along both tangent planes, and the chain ends: an edge between
208/// faces turning tangent vanishes there (see [`vanishes_at`]), and one
209/// between faces folding back onto each other is no chain's to go on.
210fn next_link(named: &NamedModel, link: &Link) -> GeopResult<Option<Link>> {
211    let model = named.topology();
212    let vertex = link.vertices(model)?[1];
213    let (_, arriving) = link.at(model, Scalar::ONE)?;
214    let normals_at = |faces: [FaceId; 2]| -> GeopResult<[Vector3; 2]> {
215        let normal = |face| {
216            let ctx = with_context!("the normal of face {face} at vertex {vertex}");
217            model.normal_at_vertex(face, vertex).with_context(ctx)
218        };
219        Ok([normal(faces[0])?, normal(faces[1])?])
220    };
221    let normals = normals_at(link.faces)?;
222    if normals[0].could_be_parallel(&normals[1]) {
223        return Ok(None);
224    }
225    let mut found = Vec::new();
226    for (&id, e) in &model.edges {
227        if id == link.edge {
228            continue;
229        }
230        for forward in [true, false] {
231            let leaves = if forward {
232                e.start_vertex
233            } else {
234                e.end_vertex
235            };
236            if leaves != vertex {
237                continue;
238            }
239            let candidate = Link::new(model, id, forward)?;
240            let (_, leaving) = candidate.at(model, Scalar::ZERO)?;
241            if !arriving.prod_dot(&leaving).definitely_greater(Scalar::ZERO) {
242                continue;
243            }
244            let theirs = normals_at(candidate.faces)?;
245            if (0..2).all(|k| same_direction(&normals[k], &theirs[k])) {
246                found.push(candidate);
247            }
248        }
249    }
250    match found.len() {
251        0 => Ok(None),
252        1 => Ok(found.pop()),
253        _ => {
254            let name = |edge: EdgeId| named.name_of(edge).map_or(edge.to_string(), String::from);
255            let forks: Vec<String> = found.iter().map(|l| name(l.edge)).collect();
256            Err(GeopError::new(format!(
257                "edge {:?} forks at vertex {}: it runs on tangentially into each of the edges {forks:?}, between faces tangent to its own, so no one chain goes on from it. Pick the one of them the blend is to run on along, alone: a chain from it does not fork, and takes this edge along",
258                name(link.edge),
259                named
260                    .name_of(vertex)
261                    .map_or(vertex.to_string(), String::from),
262            )))
263        }
264    }
265}
266
267/// The tangent chain of `edge` (see the module docs), running the way the
268/// edge does.
269pub fn tangent_chain(named: &NamedModel, edge: EdgeId) -> GeopResult<TangentChain> {
270    let model = named.topology();
271    let walk = |first: Link| -> GeopResult<(Vec<Link>, bool)> {
272        let chain = walk_by(first, |l| l.edge, |last| next_link(named, last))?;
273        match chain.end {
274            Walked::Closed => Ok((chain.nodes, true)),
275            Walked::Open => Ok((chain.nodes, false)),
276            Walked::Revisited(next) => Err(GeopError::new(format!(
277                "the tangent chain of the edge runs round a loop not through it, at edge {}",
278                next.edge
279            ))),
280        }
281    };
282    let (ahead, closed) = walk(Link::new(model, edge, true)?)?;
283    if closed {
284        return Ok(TangentChain {
285            links: ahead,
286            closed,
287        });
288    }
289    let (behind, _) = walk(Link::new(model, edge, false)?)?;
290    let mut links: Vec<Link> = behind[1..].iter().rev().map(Link::reversed).collect();
291    links.extend(ahead);
292    Ok(TangentChain {
293        links,
294        closed: false,
295    })
296}