1use 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
18fn 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#[derive(Clone, Debug)]
32pub struct Link {
33 pub edge: EdgeId,
34 pub forward: bool,
35 pub faces: [FaceId; 2],
36}
37
38impl Link {
39 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 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 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#[derive(Clone, Debug)]
118pub struct TangentChain {
119 pub links: Vec<Link>,
120 pub closed: bool,
121}
122
123impl TangentChain {
124 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 pub fn first(&self) -> GeopResult<&Link> {
140 (self.links.first()).ok_or_else(|| GeopError::new("a chain has no links"))
141 }
142
143 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 #[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
174pub 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
186fn same_direction(a: &Vector3, b: &Vector3) -> bool {
188 a.could_be_parallel(b) && a.prod_dot(b).definitely_greater(Scalar::ZERO)
189}
190
191fn 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
267pub 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}