1use std::collections::BTreeMap;
40
41use geop_core_geometry::{
42 nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D, embed_point, loop_signed_area},
43 nurb_surface::{FlatPatch, NurbSurface, NurbSurface3D},
44};
45use geop_core_math::{
46 geop_error::{GeopError, GeopResult, WithContext},
47 scalars::Scalar,
48 union_find::UnionFind,
49 vector::{Vector2, Vector3, Vector4},
50 with_context,
51};
52use geop_core_topology::build::{BodySpec, BuiltBody, CoedgeOn};
53use geop_ops_model::{BodyNames, NamedModel, Namer, Profile};
54
55use crate::skin::{self, Strip, StripCap, StripGeometry};
56
57#[derive(Clone, Debug)]
60pub struct Frame {
61 pub origin: Vector3,
62 pub e1: Vector3,
63 pub e2: Vector3,
64}
65
66impl Frame {
67 pub fn point(&self, p: &Vector2) -> Vector3 {
69 self.origin
70 .add(&self.e1.prod_scalar(p[0]))
71 .add(&self.e2.prod_scalar(p[1]))
72 }
73
74 pub fn curve(&self, curve: &NurbCurve2D) -> GeopResult<NurbCurve3D> {
77 curve.embed(&self.origin, &self.e1, &self.e2)
78 }
79}
80
81#[derive(Clone, Debug)]
83pub enum Span {
84 Line,
86 Curve {
93 degree: usize,
94 knots: Vec<Scalar>,
95 middle: Vec<(Frame, Option<Scalar>)>,
96 },
97 Blend {
103 degree: usize,
104 knots: Vec<Scalar>,
105 middle: Vec<[(Frame, Scalar); 2]>,
106 },
107 Through {
113 degree: usize,
114 knots: Vec<Scalar>,
115 middle: Vec<Vec<(usize, Scalar)>>,
116 },
117}
118
119impl Span {
120 pub fn arc(middle: Frame, weight: Scalar) -> Self {
123 Span::Curve {
124 degree: 2,
125 knots: vec![
126 Scalar::ZERO,
127 Scalar::ZERO,
128 Scalar::ZERO,
129 Scalar::ONE,
130 Scalar::ONE,
131 Scalar::ONE,
132 ],
133 middle: vec![(middle, Some(weight))],
134 }
135 }
136}
137
138enum Row<'a> {
140 First(&'a Frame, Option<Scalar>),
143 Last(&'a Frame),
145 Both(&'a [(Frame, Scalar); 2]),
148 Stations(&'a [(usize, Scalar)]),
151}
152
153impl Row<'_> {
154 fn point(&self, stations: &[Frame], (a, b): (usize, usize), at: &[Vector3]) -> Vector4 {
158 let place =
159 |p: &Vector3, frame: &Frame| embed_point(p, &frame.origin, &frame.e1, &frame.e2);
160 match self {
161 Row::First(frame, weight) => weighted(place(&at[a], frame), *weight),
162 Row::Last(frame) => place(&at[b], frame),
163 Row::Both([(fa, wa), (fb, wb)]) => {
164 let (pa, pb) = (place(&at[a], fa), place(&at[b], fb));
165 Vector4::from_array(std::array::from_fn(|k| pa[k].mul(*wa).add(pb[k].mul(*wb))))
166 }
167 Row::Stations(terms) => {
168 let term = |&(s, coefficient): &(usize, Scalar)| -> Vector4 {
172 let p = place(&at[s], &stations[s]);
173 if coefficient.is_exactly(Scalar::ONE) {
174 p
175 } else {
176 Vector4::from_array(std::array::from_fn(|k| p[k].mul(coefficient)))
177 }
178 };
179 let mut terms = terms.iter().map(term);
180 let first = terms.next().expect("a row of at least one section");
181 terms.fold(first, |sum, t| {
182 Vector4::from_array(std::array::from_fn(|k| sum[k].add(t[k])))
183 })
184 }
185 }
186 }
187}
188
189#[derive(Clone, Debug)]
193pub struct Path {
194 pub stations: Vec<Frame>,
195 pub spans: Vec<Span>,
196 pub closed: bool,
197 pub along_normal: bool,
200 pub station_names: Vec<String>,
203 pub span_names: Vec<Option<String>>,
206}
207
208impl Path {
209 fn station(&self, j: usize) -> usize {
211 if self.closed {
212 j % self.stations.len()
213 } else {
214 j
215 }
216 }
217
218 fn rows(&self, j: usize) -> Vec<Row<'_>> {
222 let (a, b) = (&self.stations[j], &self.stations[self.station(j + 1)]);
223 let middle: Vec<Row<'_>> = match &self.spans[j] {
224 Span::Line => Vec::new(),
225 Span::Curve { middle, .. } => middle.iter().map(|(m, w)| Row::First(m, *w)).collect(),
226 Span::Blend { middle, .. } => middle.iter().map(Row::Both).collect(),
227 Span::Through { middle, .. } => middle.iter().map(|m| Row::Stations(m)).collect(),
228 };
229 std::iter::once(Row::First(a, None))
230 .chain(middle)
231 .chain(std::iter::once(Row::Last(b)))
232 .collect()
233 }
234
235 fn degree(&self, j: usize) -> usize {
236 match &self.spans[j] {
237 Span::Line => 1,
238 Span::Curve { degree, .. }
239 | Span::Blend { degree, .. }
240 | Span::Through { degree, .. } => *degree,
241 }
242 }
243
244 fn knots(&self, j: usize) -> Vec<Scalar> {
245 match &self.spans[j] {
246 Span::Line => vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
247 Span::Curve { knots, .. } | Span::Blend { knots, .. } | Span::Through { knots, .. } => {
248 knots.clone()
249 }
250 }
251 }
252
253 fn ends(&self, j: usize) -> (usize, usize) {
255 (j, self.station(j + 1))
256 }
257
258 fn lateral(&self, j: usize, points: &[Vector2]) -> GeopResult<NurbCurve3D> {
261 let at: Vec<Vector3> = points
262 .iter()
263 .map(|p| Vector3::from_array([p[0], p[1], Scalar::ONE]))
264 .collect();
265 let control_points = self
266 .rows(j)
267 .iter()
268 .map(|row| row.point(&self.stations, self.ends(j), &at))
269 .collect();
270 NurbCurve::try_new(self.degree(j), control_points, self.knots(j))
271 }
272
273 fn wall(&self, j: usize, curves: &[&NurbCurve2D]) -> GeopResult<NurbSurface3D> {
277 let a = curves[j];
278 let rows = self.rows(j);
279 let mut control_points = Vec::with_capacity(rows.len() * a.control_points.len());
280 for row in &rows {
281 for k in 0..a.control_points.len() {
282 let at: Vec<Vector3> = curves.iter().map(|c| c.control_points[k]).collect();
283 control_points.push(row.point(&self.stations, self.ends(j), &at));
284 }
285 }
286 NurbSurface::try_new(
287 self.degree(j),
288 a.degree,
289 control_points,
290 self.knots(j),
291 a.knot_vector.clone(),
292 )
293 }
294
295 fn check(&self) -> GeopResult<()> {
296 let stations = self.stations.len();
297 let spans = self.spans.len();
298 let expected = if self.closed { stations } else { stations - 1 };
299 if spans == 0 || spans != expected {
300 return Err(GeopError::new(format!(
301 "sweep: a {} path of {stations} stations cannot have {spans} spans",
302 if self.closed { "closed" } else { "open" }
303 )));
304 }
305 if self.station_names.len() != stations || self.span_names.len() != spans {
306 return Err(GeopError::new("sweep: a name for every station and span"));
307 }
308 Ok(())
309 }
310}
311
312fn weighted(p: Vector4, weight: Option<Scalar>) -> Vector4 {
314 match weight {
315 None => p,
316 Some(w) => Vector4::from_array([p[0].mul(w), p[1].mul(w), p[2].mul(w), p[3].mul(w)]),
317 }
318}
319
320#[derive(Clone, Debug)]
324pub struct SweepLoop {
325 pub profile: Profile,
326 pub poles: Vec<bool>,
328 pub on_axis: Vec<bool>,
330 pub apex: bool,
333}
334
335impl SweepLoop {
336 pub fn plain(profile: Profile) -> Self {
338 Self {
339 poles: vec![false; profile.joint_names.len()],
340 on_axis: vec![false; profile.curves.len()],
341 profile,
342 apex: false,
343 }
344 }
345
346 pub fn reversed(&self) -> Self {
349 let n = self.profile.curves.len();
350 let poles = if self.profile.is_closed() {
351 (0..n).map(|m| self.poles[(n - m) % n]).collect()
352 } else {
353 self.poles.iter().rev().copied().collect()
354 };
355 Self {
356 profile: self.profile.reversed(),
357 poles,
358 on_axis: self.on_axis.iter().rev().copied().collect(),
359 apex: self.apex,
360 }
361 }
362
363 pub fn at_corners(&self) -> GeopResult<Self> {
374 let mut out = self.clone();
375 for i in (0..self.profile.curves.len()).rev() {
377 let curve = &self.profile.curves[i];
378 let mut corners = Vec::new();
379 let mut knots = curve.knot_vector
380 [curve.degree + 1..curve.knot_vector.len() - curve.degree - 1]
381 .iter()
382 .peekable();
383 while let Some(&k) = knots.next() {
384 let mut multiplicity = 1;
385 while knots.next_if(|&&n| n.is_exactly(k)).is_some() {
386 multiplicity += 1;
387 }
388 if multiplicity < curve.degree {
389 continue;
390 }
391 let (before, after) = curve.split(k)?;
392 let (incoming, outgoing) = (
393 before.tangent(before.domain().1)?,
394 after.tangent(after.domain().0)?,
395 );
396 let turns = !incoming.prod_cross(&outgoing).could_be_equal(Scalar::ZERO)
397 || incoming.prod_dot(&outgoing).definitely_less(Scalar::ZERO);
398 if turns {
399 corners.push(k);
400 }
401 }
402 if corners.is_empty() {
403 continue;
404 }
405 let (t0, t1) = curve.domain();
406 let bounds: Vec<Scalar> = std::iter::once(t0).chain(corners).chain([t1]).collect();
407 let pieces = bounds
408 .windows(2)
409 .map(|w| {
410 curve
411 .sub_curve(w[0], w[1])?
412 .with_unit_domain()?
413 .with_unit_end_weights()
414 })
415 .collect::<GeopResult<Vec<_>>>()?;
416 let name = self.profile.curve_names[i].clone();
417 let cuts = pieces.len() - 1;
418 out.profile.curves.splice(i..=i, pieces);
419 out.profile
420 .curve_names
421 .splice(i + 1..i + 1, (1..=cuts).map(|k| format!("{name}#k{k}")));
422 out.profile
423 .joint_names
424 .splice(i + 1..i + 1, (1..=cuts).map(|k| format!("{name}@k{k}")));
425 let on_axis = self.on_axis[i];
426 out.on_axis.splice(i..=i, vec![on_axis; cuts + 1]);
427 out.poles.splice(i + 1..i + 1, vec![on_axis; cuts]);
428 }
429 Ok(out)
430 }
431
432 fn joints(&self) -> usize {
433 self.profile.joint_names.len()
434 }
435
436 fn next(&self, i: usize) -> usize {
438 (i + 1) % self.joints()
439 }
440
441 fn check(&self) -> GeopResult<()> {
446 let profile = &self.profile;
447 profile.check_names()?;
448 let curves = &profile.curves;
449 let n = curves.len();
450 if n == 0 || (profile.is_closed() && n < 2) {
451 return Err(GeopError::new(format!(
452 "sweep: a profile of {n} curve(s) is too short"
453 )));
454 }
455 if self.poles.len() != self.joints() || self.on_axis.len() != n {
456 return Err(GeopError::new("sweep: a flag for every joint and curve"));
457 }
458 for (i, curve) in curves.iter().enumerate() {
459 let (t0, t1) = curve.domain();
460 if !(t0.could_be_equal(Scalar::ZERO) && t1.could_be_equal(Scalar::ONE)) {
461 return Err(GeopError::new(format!(
462 "sweep: curve {} has domain ({t0:?}, {t1:?}), not [0, 1]",
463 profile.curve_names[i]
464 )));
465 }
466 if i + 1 < n || profile.is_closed() {
467 let next = &curves[(i + 1) % n];
468 let (end, start) = (curve.end()?, next.start()?);
469 if !end.could_be_equal(&start) {
470 return Err(GeopError::new(format!(
471 "sweep: curve {} ends at {end:?}, but the next one starts at {start:?}",
472 profile.curve_names[i]
473 )));
474 }
475 }
476 if self.on_axis[i] && !(self.poles[i] && self.poles[self.next(i)]) {
477 return Err(GeopError::new(format!(
478 "sweep: curve {} lies on the axis, but its ends are not poles",
479 profile.curve_names[i]
480 )));
481 }
482 }
483 Ok(())
484 }
485}
486
487pub fn sweep(
507 model: &mut NamedModel,
508 namer: &Namer,
509 path: &Path,
510 loops: &[SweepLoop],
511 solid: Option<&str>,
512) -> GeopResult<BuiltBody> {
513 let loops = (loops.iter())
514 .map(SweepLoop::at_corners)
515 .collect::<GeopResult<Vec<_>>>()?;
516 let sections = vec![loops; path.stations.len()];
517 skin(model, namer, path, §ions, solid)
518}
519
520pub fn sweep_drawn(
525 model: &mut NamedModel,
526 namer: &Namer,
527 path: &Path,
528 loops: &[SweepLoop],
529 solid: Option<&str>,
530) -> GeopResult<BuiltBody> {
531 if path.along_normal {
532 let reversed: Vec<SweepLoop> = loops.iter().map(SweepLoop::reversed).collect();
533 sweep(model, namer, path, &reversed, solid)
534 } else {
535 sweep(model, namer, path, loops, solid)
536 }
537}
538
539pub fn skin(
551 model: &mut NamedModel,
552 namer: &Namer,
553 path: &Path,
554 sections: &[Vec<SweepLoop>],
555 solid: Option<&str>,
556) -> GeopResult<BuiltBody> {
557 let (spec, names) = skinned(namer, path, sections, solid)?;
558 model.build_body(spec, names).with_context(with_context!(
559 "skin({}, solid={solid:?}): building it",
560 namer.root()
561 ))
562}
563
564pub(crate) fn skinned(
566 namer: &Namer,
567 path: &Path,
568 sections: &[Vec<SweepLoop>],
569 solid: Option<&str>,
570) -> GeopResult<(BodySpec, BodyNames)> {
571 let ctx = with_context!(
572 "skin({}, {} section(s) of {} loop(s), solid={solid:?})",
573 namer.root(),
574 sections.len(),
575 sections.first().map_or(0, Vec::len)
576 );
577 path.check().with_context(ctx)?;
578 check_sections(path, sections, solid.is_some()).with_context(ctx)?;
579 let loops = §ions[0];
580 let geometry: Vec<Placed<'_>> = (0..loops.len())
581 .map(|l| Placed { path, sections, l })
582 .collect();
583 let apexes: Vec<Vec<bool>> = (0..loops.len())
584 .map(|l| sections.iter().map(|section| section[l].apex).collect())
585 .collect();
586 let strips: Vec<Strip<'_>> = loops
587 .iter()
588 .zip(&geometry)
589 .zip(&apexes)
590 .map(|((lp, geometry), apex)| Strip {
591 namer,
592 joint_names: &lp.profile.joint_names,
593 curve_names: &lp.profile.curve_names,
594 station_names: &path.station_names,
595 span_names: &path.span_names,
596 closed: path.closed,
597 poles: &lp.poles,
598 on_axis: &lp.on_axis,
599 apex,
600 glued: Vec::new(),
601 geometry,
602 })
603 .collect();
604 let mut caps = Vec::new();
605 if solid.is_some() && !path.closed {
606 for (s, forward, name) in [(0, true, "start"), (path.stations.len() - 1, false, "end")] {
608 let (frame, section) = (&path.stations[s], §ions[s]);
609 if section.iter().any(|lp| lp.apex) {
611 if section.len() > 1 {
612 return Err(GeopError::new(format!(
613 "skin: the section at {} comes to a point, and has holes",
614 path.station_names[s]
615 )))
616 .with_context(ctx);
617 }
618 continue;
619 }
620 for (a, area) in areas(section).with_context(ctx)?.into_iter().enumerate() {
624 let patch = FlatPatch::around(
625 frame.origin,
626 frame.e1,
627 frame.e2,
628 area.iter().flat_map(|&l| §ion[l].profile.curves),
629 path.along_normal == forward,
630 )?;
631 let loops = (area.iter())
632 .map(|&l| {
633 let pcurves = (section[l].profile.curves.iter())
634 .map(|c| patch.pcurve(c))
635 .collect::<GeopResult<_>>()?;
636 Ok((l, pcurves))
637 })
638 .collect::<GeopResult<_>>()?;
639 let name = match a {
640 0 => namer.name(&[name]),
641 _ => {
642 let outline = §ion[area[0]].profile.curve_names;
643 let least = outline.iter().min().map_or("", String::as_str);
644 namer.name(&[name, least])
645 }
646 };
647 caps.push(StripCap {
648 name,
649 station: s,
650 forward,
651 surface: patch.surface()?,
652 loops,
653 });
654 }
655 }
656 }
657
658 let mut spec = BodySpec::new(solid.is_some());
659 let mut names = BodyNames {
660 solid: solid.map(str::to_string),
661 ..BodyNames::default()
662 };
663 skin::skin(&mut spec, &mut names, &strips, &caps).with_context(ctx)?;
664 spec.shells = connected_faces(&spec);
665 Ok((spec, names))
666}
667
668pub(crate) fn areas(loops: &[SweepLoop]) -> GeopResult<Vec<Vec<usize>>> {
673 if loops.len() < 2 {
674 return Ok(vec![(0..loops.len()).collect()]);
675 }
676 let winds = |l: usize| -> GeopResult<bool> {
677 let area = loop_signed_area(&loops[l].profile.curves)?;
678 if area.definitely_greater(Scalar::ZERO) {
679 Ok(true)
680 } else if area.definitely_less(Scalar::ZERO) {
681 Ok(false)
682 } else {
683 Err(GeopError::new(format!(
684 "loop {l} of the profile encloses an area {area:?} that could be zero: which way it winds, and so whether it is a hole, is unknown"
685 )))
686 }
687 };
688 let outer = winds(0)?;
689 let mut areas: Vec<Vec<usize>> = Vec::new();
690 for l in 0..loops.len() {
691 match areas.last_mut() {
692 Some(area) if winds(l)? != outer => area.push(l),
693 _ => areas.push(vec![l]),
694 }
695 }
696 Ok(areas)
697}
698
699pub(crate) fn wall(
702 path: &Path,
703 loops: &[SweepLoop],
704 (l, j, i): (usize, usize, usize),
705) -> GeopResult<NurbSurface3D> {
706 let sections = vec![loops.to_vec(); path.stations.len()];
707 Placed {
708 path,
709 sections: §ions,
710 l,
711 }
712 .wall(j, i)
713}
714
715struct Placed<'a> {
718 path: &'a Path,
719 sections: &'a [Vec<SweepLoop>],
720 l: usize,
721}
722
723impl Placed<'_> {
724 fn profile(&self, s: usize) -> &Profile {
725 &self.sections[s][self.l].profile
726 }
727}
728
729impl StripGeometry for Placed<'_> {
730 fn joint(&self, s: usize, i: usize) -> GeopResult<Vector3> {
731 Ok(self.path.stations[s].point(&self.profile(s).joint(i)?))
732 }
733
734 fn curve(&self, s: usize, i: usize) -> GeopResult<NurbCurve3D> {
735 self.path.stations[s].curve(&self.profile(s).curves[i])
736 }
737
738 fn lateral(&self, j: usize, i: usize) -> GeopResult<NurbCurve3D> {
739 let points = (0..self.sections.len())
740 .map(|s| self.profile(s).joint(i))
741 .collect::<GeopResult<Vec<_>>>()?;
742 self.path.lateral(j, &points)
743 }
744
745 fn wall(&self, j: usize, i: usize) -> GeopResult<NurbSurface3D> {
746 let curves: Vec<&NurbCurve2D> = (0..self.sections.len())
747 .map(|s| &self.profile(s).curves[i])
748 .collect();
749 self.path.wall(j, &curves)
750 }
751}
752
753fn check_sections(path: &Path, sections: &[Vec<SweepLoop>], solid: bool) -> GeopResult<()> {
758 if sections.len() != path.stations.len() {
759 return Err(GeopError::new(format!(
760 "skin: {} sections for {} stations",
761 sections.len(),
762 path.stations.len()
763 )));
764 }
765 let first = §ions[0];
766 if first.is_empty() {
767 return Err(GeopError::new("sweep: nothing to sweep"));
768 }
769 let same = |a: &Scalar, b: &Scalar| a.is_subset_of(*b) && b.is_subset_of(*a);
770 for (s, section) in sections.iter().enumerate() {
771 if section.len() != first.len() {
772 return Err(GeopError::new(format!(
773 "skin: section {s} has {} loops, the first {}",
774 section.len(),
775 first.len()
776 )));
777 }
778 for (l, (lp, lp0)) in section.iter().zip(first).enumerate() {
779 lp.check()?;
780 if solid && !lp.profile.is_closed() {
781 return Err(GeopError::new(
782 "sweep: a solid is swept from closed loops only",
783 ));
784 }
785 if lp.profile.curves.len() != lp0.profile.curves.len()
786 || lp.profile.is_closed() != lp0.profile.is_closed()
787 || lp.poles != lp0.poles
788 || lp.on_axis != lp0.on_axis
789 {
790 return Err(GeopError::new(format!(
791 "skin: loop {l} of section {s} does not match the first section's"
792 )));
793 }
794 for (i, (c, c0)) in lp
795 .profile
796 .curves
797 .iter()
798 .zip(&lp0.profile.curves)
799 .enumerate()
800 {
801 if c.degree != c0.degree
802 || c.knot_vector.len() != c0.knot_vector.len()
803 || !c
804 .knot_vector
805 .iter()
806 .zip(&c0.knot_vector)
807 .all(|(a, b)| same(a, b))
808 {
809 return Err(GeopError::new(format!(
810 "skin: curve {i} of loop {l} of section {s} (degree {}, knots {:?}) is not compatible with the first section's (degree {}, knots {:?})",
811 c.degree, c.knot_vector, c0.degree, c0.knot_vector
812 )));
813 }
814 }
815 }
816 }
817 Ok(())
818}
819
820fn connected_faces(spec: &BodySpec) -> Vec<Vec<usize>> {
824 let mut sets = UnionFind::new(spec.faces.len());
825 let mut user_of_edge: BTreeMap<usize, usize> = BTreeMap::new();
826 for (f, face) in spec.faces.iter().enumerate() {
827 for coedge in std::iter::once(&face.outer).chain(&face.holes).flatten() {
828 if let CoedgeOn::Edge(e, _) = coedge.on {
829 match user_of_edge.get(&e) {
830 Some(&other) => sets.union(f, other),
831 None => {
832 user_of_edge.insert(e, f);
833 }
834 }
835 }
836 }
837 }
838 sets.groups()
839}