1use 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
51pub trait StripGeometry {
54 fn joint(&self, s: usize, i: usize) -> GeopResult<Vector3>;
56 fn curve(&self, s: usize, i: usize) -> GeopResult<NurbCurve3D>;
58 fn lateral(&self, j: usize, i: usize) -> GeopResult<NurbCurve3D>;
61 fn wall(&self, j: usize, i: usize) -> GeopResult<NurbSurface3D>;
64}
65
66#[derive(Clone, Debug)]
68pub enum Glue {
69 Vertex {
71 station: usize,
72 joint: usize,
73 vertex: usize,
74 },
75 Section {
79 station: usize,
80 strip: usize,
81 at: usize,
82 joints: Vec<usize>,
83 },
84}
85
86pub 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 pub poles: &'a [bool],
111 pub on_axis: &'a [bool],
113 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 fn next(&self, i: usize) -> usize {
127 (i + 1) % self.joints()
128 }
129
130 fn station_after(&self, j: usize) -> usize {
132 (j + 1) % self.station_names.len()
133 }
134
135 fn is_apex(&self, s: usize) -> bool {
137 self.apex.get(s).copied().unwrap_or(false)
138 }
139
140 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
187pub 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
200pub struct Skinned {
204 pub vertices: Vec<Vec<Vec<usize>>>,
205 pub edges: Vec<Vec<Vec<CurveEdge>>>,
206}
207
208pub type CurveEdge = Option<(usize, bool)>;
211
212fn 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
223fn 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
232pub 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 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 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 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 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 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}