1use crate::common::{
42 Profile, bilinear, embed_curve, embed_point, end_point, line2, line3, start_point,
43};
44use geop_core_geometry::{
45 nurb_curve::{NurbCurve, NurbCurve2D},
46 nurb_surface::{NurbSurface, NurbSurface3D},
47};
48use geop_core_math::{
49 geop_error::{GeopError, GeopResult, WithContext},
50 primitives::CoordinateSystem,
51 scalars::Scalar,
52 vector::{Vector2, Vector3},
53 with_context,
54};
55use geop_core_part::{Namer, Part};
56use geop_core_topology::{CoedgeId, FaceId, SolidId, VertexId};
57
58struct Caps<'a, S: Scalar> {
62 coordinate_system: &'a CoordinateSystem<S>,
63 lo: Vector2<S>,
64 size: Vector2<S>,
65}
66
67impl<'a, S: Scalar> Caps<'a, S> {
68 fn new(
69 coordinate_system: &'a CoordinateSystem<S>,
70 loops: impl Iterator<Item = &'a NurbCurve2D<S>>,
71 ) -> GeopResult<Self> {
72 let mut lo = [f64::INFINITY; 2];
73 let mut hi = [f64::NEG_INFINITY; 2];
74 for curve in loops {
75 for cp in &curve.control_points {
76 for k in 0..2 {
77 let x = cp[k].div(cp[2])?;
78 lo[k] = lo[k].min(x.lower().to_f64());
79 hi[k] = hi[k].max(x.upper().to_f64());
80 }
81 }
82 }
83 let size = Vector2::from_array([S::from_f64(hi[0] - lo[0]), S::from_f64(hi[1] - lo[1])]);
87 let lo = Vector2::from_array([S::from_f64(lo[0]), S::from_f64(lo[1])]);
88 Ok(Caps {
89 coordinate_system,
90 lo,
91 size,
92 })
93 }
94
95 fn bottom_pcurve(&self, curve: &NurbCurve2D<S>) -> GeopResult<NurbCurve2D<S>> {
97 let control_points = curve
98 .control_points
99 .iter()
100 .map(|cp| {
101 Ok(Vector3::from_array([
102 cp[0].sub(cp[2].mul(self.lo[0])).div(self.size[0])?,
103 cp[1].sub(cp[2].mul(self.lo[1])).div(self.size[1])?,
104 cp[2],
105 ]))
106 })
107 .collect::<GeopResult<Vec<_>>>()?;
108 NurbCurve::try_new(curve.degree, control_points, curve.knot_vector.clone())
109 }
110
111 fn top_pcurve(&self, curve: &NurbCurve2D<S>) -> GeopResult<NurbCurve2D<S>> {
113 Ok(self.bottom_pcurve(curve)?.swap_xy())
114 }
115
116 fn corner(&self, i: usize, j: usize, height: S) -> Vector3<S> {
117 let pick = |k: usize, far: usize| {
118 if far == 1 {
119 self.lo[k].add(self.size[k])
120 } else {
121 self.lo[k]
122 }
123 };
124 self.coordinate_system
125 .to_xyz(&Vector3::from_array([pick(0, i), pick(1, j), height]))
126 }
127
128 fn bottom_surface(&self) -> GeopResult<NurbSurface3D<S>> {
129 let h = S::ZERO;
130 bilinear(
131 self.corner(0, 0, h),
132 self.corner(1, 0, h),
133 self.corner(1, 1, h),
134 self.corner(0, 1, h),
135 )
136 }
137
138 fn top_surface(&self) -> GeopResult<NurbSurface3D<S>> {
139 let h = S::ONE;
140 bilinear(
141 self.corner(0, 0, h),
142 self.corner(0, 1, h),
143 self.corner(1, 1, h),
144 self.corner(1, 0, h),
145 )
146 }
147
148 fn point(&self, p: &Vector2<S>, height: S) -> Vector3<S> {
150 self.coordinate_system
151 .to_xyz(&Vector3::from_array([p[0], p[1], height]))
152 }
153
154 fn curve(
156 &self,
157 curve: &NurbCurve2D<S>,
158 height: S,
159 ) -> GeopResult<geop_core_geometry::nurb_curve::NurbCurve3D<S>> {
160 let cs = self.coordinate_system;
161 embed_curve(
162 curve,
163 &cs.origin().add(&cs.w().prod_scalar(height)),
164 cs.u(),
165 cs.v(),
166 )
167 }
168
169 fn wall(&self, curve: &NurbCurve2D<S>) -> GeopResult<NurbSurface3D<S>> {
172 let cs = self.coordinate_system;
173 let top = cs.origin().add(cs.w());
174 let control_points = [cs.origin(), &top]
175 .into_iter()
176 .flat_map(|origin| {
177 curve
178 .control_points
179 .iter()
180 .map(move |cp| embed_point(cp, origin, cs.u(), cs.v()))
181 })
182 .collect();
183 NurbSurface::try_new(
184 1,
185 curve.degree,
186 control_points,
187 vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
188 curve.knot_vector.clone(),
189 )
190 }
191}
192
193pub struct ExtrudeNames<'a> {
209 pub namer: &'a Namer,
210 pub region: Option<&'a str>,
211 pub solid: String,
213}
214
215impl<'a> ExtrudeNames<'a> {
216 pub fn single(namer: &'a Namer) -> Self {
219 Self {
220 namer,
221 region: None,
222 solid: namer.root(),
223 }
224 }
225
226 fn curve(&self, profile: &Profile<impl Scalar>, i: usize, role: &str) -> String {
227 self.namer.name(&[&profile.curve_names[i], role])
228 }
229 fn joint(&self, profile: &Profile<impl Scalar>, i: usize, role: &str) -> String {
230 self.namer.name(&[&profile.joint_names[i], role])
231 }
232 fn side_face(&self, profile: &Profile<impl Scalar>, i: usize) -> String {
233 self.namer.name(&[&profile.curve_names[i]])
234 }
235 fn lateral_edge(&self, profile: &Profile<impl Scalar>, i: usize) -> String {
236 self.namer.name(&[&profile.joint_names[i]])
237 }
238 fn cap(&self, role: &str) -> String {
239 match self.region {
240 None => self.namer.name(&[role]),
241 Some(region) => self.namer.name(&[role, region]),
242 }
243 }
244}
245
246fn grow_ring<S: Scalar>(
254 part: &mut Part<S>,
255 names: &ExtrudeNames,
256 face_id: FaceId,
257 v0: VertexId,
258 caps: &Caps<S>,
259 profile: &Profile<S>,
260) -> GeopResult<(CoedgeId, CoedgeId, CoedgeId, CoedgeId)> {
261 let curves = &profile.curves;
262 let n = curves.len();
263 let bottom = |i: usize| -> GeopResult<_> {
264 let pcurve = caps.bottom_pcurve(&curves[i])?;
265 Ok((
266 caps.curve(&curves[i], S::ZERO)?,
267 pcurve.clone(),
268 pcurve.reverse(),
269 caps.point(&end_point(&curves[i])?, S::ZERO),
270 ))
271 };
272
273 let (curve, pcurve, pcurve_reversed, end) = bottom(0)?;
274 let (_, first_forward, first_mirror, _) = part.mve_from_vertex(
275 face_id,
276 v0,
277 curve,
278 pcurve,
279 pcurve_reversed,
280 end,
281 names.joint(profile, 1, "start"),
282 names.curve(profile, 0, "start"),
283 )?;
284
285 let mut cursor = first_forward;
286 let mut last_mirror = first_mirror;
287 for i in 1..n - 1 {
288 let (curve, pcurve, pcurve_reversed, end) = bottom(i)?;
289 let (_, next_forward, next_mirror, _) = part.mve(
290 cursor,
291 curve,
292 pcurve,
293 pcurve_reversed,
294 end,
295 names.joint(profile, i + 1, "start"),
296 names.curve(profile, i, "start"),
297 )?;
298 cursor = next_forward;
299 last_mirror = next_mirror;
300 }
301
302 Ok((first_forward, first_mirror, cursor, last_mirror))
303}
304
305fn base_level_pcurve<S: Scalar>() -> GeopResult<NurbCurve2D<S>> {
311 line2(
312 Vector2::from_array([S::ZERO, S::ONE]),
313 Vector2::from_array([S::ZERO, S::ZERO]),
314 )
315}
316
317fn build_side_walls<S: Scalar>(
328 part: &mut Part<S>,
329 names: &ExtrudeNames,
330 caps: &Caps<S>,
331 profile: &Profile<S>,
332 mut down_face_coedge: CoedgeId,
333) -> GeopResult<()> {
334 let curves = &profile.curves;
335 let n = curves.len();
336 let points = curves
337 .iter()
338 .map(start_point)
339 .collect::<GeopResult<Vec<_>>>()?;
340
341 for coedge_id in part
342 .topology()
343 .iterate_loop_coedges(down_face_coedge)
344 .collect::<Vec<_>>()
345 {
346 part.replace_pcurve(coedge_id, base_level_pcurve()?)
347 .with_context(with_context!(
348 "build_side_walls: replace_pcurve for coedge {coedge_id} failed"
349 ))?;
350 }
351
352 let upwards = |part: &mut Part<S>, at: CoedgeId, i: usize| {
355 let top = caps.point(&points[i], S::ONE);
356 part.mve(
357 at,
358 line3(caps.point(&points[i], S::ZERO), top)?,
359 line2(
360 Vector2::from_array([S::ZERO, S::ZERO]),
361 Vector2::from_array([S::ONE, S::ZERO]),
362 )?,
363 line2(
364 Vector2::from_array([S::ONE, S::ONE]),
365 Vector2::from_array([S::ZERO, S::ONE]),
366 )?,
367 top,
368 names.joint(profile, i, "end"),
369 names.lateral_edge(profile, i),
370 )
371 };
372 let close_wall = |part: &mut Part<S>, down: CoedgeId, up: CoedgeId, i: usize| {
374 part.mef(
375 down,
376 up,
377 caps.curve(&curves[i], S::ONE)?,
378 line2(
379 Vector2::from_array([S::ONE, S::ZERO]),
380 Vector2::from_array([S::ONE, S::ONE]),
381 )?,
382 caps.top_pcurve(&curves[i])?.reverse(),
383 caps.wall(&curves[i])?,
384 names.curve(profile, i, "end"),
385 names.side_face(profile, i),
386 )
387 };
388
389 let (_, mut coedge_down, mut coedge_up, _) = upwards(part, down_face_coedge, 0)
390 .with_context("build_side_walls: first upwards edge mve failed")?;
391
392 let mut prev_coedge_down = coedge_down;
393 let final_coedge_up = coedge_up;
394
395 for i in 1..n {
396 down_face_coedge = part.topology().get_coedge(down_face_coedge)?.prev;
397 (_, coedge_down, coedge_up, _) = upwards(part, down_face_coedge, i).with_context(
398 with_context!("build_side_walls: upwards edge mve failed (i={i})"),
399 )?;
400 close_wall(part, prev_coedge_down, coedge_up, i - 1).with_context(with_context!(
401 "build_side_walls: closing side face mef failed (i={i})"
402 ))?;
403 prev_coedge_down = coedge_down;
404 }
405
406 close_wall(part, prev_coedge_down, final_coedge_up, n - 1)
408 .with_context("build_side_walls: closing mef for the last side face failed")?;
409
410 Ok(())
411}
412
413fn validate_loop<S: Scalar>(profile: &Profile<S>) -> GeopResult<()> {
416 profile.check_names()?;
417 if !profile.is_closed() {
418 return Err(GeopError::new(
419 "extrude: every loop must be closed, but has a name for an end joint",
420 ));
421 }
422 let curves = &profile.curves;
423 if curves.len() < 2 {
424 return Err(GeopError::new(
425 "extrude: every loop needs at least 2 curves",
426 ));
427 }
428 for (i, curve) in curves.iter().enumerate() {
429 let (t0, t1) = curve.domain();
430 if !(t0.could_be_equal(S::ZERO) && t1.could_be_equal(S::ONE)) {
431 return Err(GeopError::new(format!(
432 "extrude: curve {i} has domain ({t0:?}, {t1:?}), not [0, 1]"
433 )));
434 }
435 let next = &curves[(i + 1) % curves.len()];
436 let (end, start) = (end_point(curve)?, start_point(next)?);
437 if !end.could_be_equal(&start) {
438 return Err(GeopError::new(format!(
439 "extrude: curve {i} ends at {end:?}, but the next one starts at {start:?}"
440 )));
441 }
442 }
443 Ok(())
444}
445
446pub fn extrude<S: Scalar>(
451 part: &mut Part<S>,
452 names: &ExtrudeNames,
453 coordinate_system: &CoordinateSystem<S>,
454 outer: &Profile<S>,
455 holes: &[Profile<S>],
456) -> GeopResult<SolidId> {
457 fn ctx<S: Scalar>(
463 coordinate_system: &CoordinateSystem<S>,
464 outer: &Profile<S>,
465 holes: &[Profile<S>],
466 part: &Part<S>,
467 e: GeopError,
468 ) -> GeopError {
469 e.with_context(format!(
470 "extrude(
471 coordinate_system={coordinate_system}
472 outer={outer:?}
473 holes={holes:?}
474 model={}
475)",
476 part.topology()
477 ))
478 }
479
480 validate_loop(outer)?;
481 for hole in holes {
482 validate_loop(hole)?;
483 }
484 let caps = Caps::new(
485 coordinate_system,
486 outer
487 .curves
488 .iter()
489 .chain(holes.iter().flat_map(|h| h.curves.iter())),
490 )?;
491 let n = outer.curves.len();
492
493 let (start_vertex, any_face, solid_id) = part.mvfs(
496 caps.point(&start_point(&outer.curves[0])?, S::ZERO),
497 names.joint(outer, 0, "start"),
498 names.cap("end"),
499 names.solid.clone(),
500 )?;
501
502 let (first_base_face_coedge, down_face_coedge, cursor_coedge_out, _) =
505 grow_ring(part, names, any_face, start_vertex, &caps, outer)
506 .with_context("extrude: growing the outer ring failed")
507 .with_context(&|e| ctx(coordinate_system, outer, holes, part, e))?;
508
509 let (_, bottom_face_id, _, _) = part
511 .mef(
512 cursor_coedge_out,
513 first_base_face_coedge,
514 caps.curve(&outer.curves[n - 1], S::ZERO)?,
515 caps.bottom_pcurve(&outer.curves[n - 1])?,
516 base_level_pcurve()?,
517 caps.bottom_surface()?,
518 names.curve(outer, n - 1, "start"),
519 names.cap("start"),
520 )
521 .with_context("extrude: closing mef for the bottom cap failed")
522 .with_context(&|e| ctx(coordinate_system, outer, holes, part, e))?;
523
524 let mut hole_scaffolds: Vec<(&Profile<S>, CoedgeId)> = Vec::new();
530 for hole in holes {
531 let last = hole.curves.len() - 1;
532 let hv0 = part
533 .mvr(
534 bottom_face_id,
535 caps.point(&start_point(&hole.curves[0])?, S::ZERO),
536 names.joint(hole, 0, "start"),
537 )
538 .with_context("extrude: mvr for a hole's starting vertex failed")
539 .with_context(&|e| ctx(coordinate_system, outer, holes, part, e))?;
540 let (_, first_mirror, _, last_mirror) =
541 grow_ring(part, names, bottom_face_id, hv0, &caps, hole)
542 .with_context("extrude: growing a hole's ring failed")
543 .with_context(&|e| ctx(coordinate_system, outer, holes, part, e))?;
544
545 part.mer(
549 first_mirror,
550 last_mirror,
551 caps.curve(&hole.curves[last], S::ZERO)?.reverse(),
552 base_level_pcurve()?,
553 caps.bottom_pcurve(&hole.curves[last])?,
554 any_face,
555 names.curve(hole, last, "start"),
556 )
557 .with_context("extrude: mer for a hole's ring failed")
558 .with_context(&|e| ctx(coordinate_system, outer, holes, part, e))?;
559
560 hole_scaffolds.push((hole, first_mirror));
561 }
562
563 build_side_walls(part, names, &caps, outer, down_face_coedge)
565 .with_context("extrude: building the outer ring's side walls failed")
566 .with_context(&|e| ctx(coordinate_system, outer, holes, part, e))?;
567 for (hole, hole_down_face_coedge) in hole_scaffolds {
568 build_side_walls(part, names, &caps, hole, hole_down_face_coedge)
569 .with_context("extrude: building a hole's side walls failed")
570 .with_context(&|e| ctx(coordinate_system, outer, holes, part, e))?;
571 }
572
573 part.replace_face(any_face, caps.top_surface()?)
575 .with_context("extrude: replace_face for the top cap failed")
576 .with_context(&|e| ctx(coordinate_system, outer, holes, part, e))?;
577
578 Ok(solid_id)
579}
580
581pub fn extrude_from_plane<S: Scalar>(
592 part: &mut Part<S>,
593 names: &ExtrudeNames,
594 plane: &CoordinateSystem<S>,
595 outer: &Profile<S>,
596 holes: &[Profile<S>],
597 distance: S,
598) -> GeopResult<SolidId> {
599 let w = plane.w().prod_scalar(distance);
600 if distance.definitely_less(S::ZERO) {
601 let cs = CoordinateSystem::try_new(*plane.origin(), *plane.u(), *plane.v(), w)?;
602 extrude(part, names, &cs, outer, holes)
603 } else if distance.definitely_greater(S::ZERO) {
604 let cs = CoordinateSystem::try_new(*plane.origin(), *plane.v(), *plane.u(), w)?;
605 let mirror = |profile: &Profile<S>| profile.map_curves(|c| c.swap_xy()).reversed();
606 let holes: Vec<_> = holes.iter().map(mirror).collect();
607 extrude(part, names, &cs, &mirror(outer), &holes)
608 } else {
609 Err(GeopError::new(format!(
610 "extrude_from_plane: distance {distance:?} could be zero"
611 )))
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use crate::common::{arc2, polygon, sqrt2_over_2};
619 use geop_core_math::for_all_scalars;
620 use geop_core_topology::{
621 Model,
622 validation::{ValidationParameters, validate, validate_manifold},
623 };
624
625 fn v2<S: Scalar>(x: f64, y: f64) -> Vector2<S> {
626 Vector2::from_array([S::from_f64(x), S::from_f64(y)])
627 }
628
629 fn unit_square<S: Scalar>() -> Vec<NurbCurve2D<S>> {
630 polygon(&[v2(0.0, 0.0), v2(1.0, 0.0), v2(1.0, 1.0), v2(0.0, 1.0)]).unwrap()
631 }
632
633 fn square_hole<S: Scalar>(x0: f64, y0: f64, size: f64) -> Vec<NurbCurve2D<S>> {
638 polygon(&[
639 v2(x0, y0),
640 v2(x0, y0 + size),
641 v2(x0 + size, y0 + size),
642 v2(x0 + size, y0),
643 ])
644 .unwrap()
645 }
646
647 fn circle<S: Scalar>(cx: f64, cy: f64, r: f64, clockwise: bool) -> Vec<NurbCurve2D<S>> {
650 let q = [(r, 0.0), (0.0, r), (-r, 0.0), (0.0, -r)];
651 let mut arcs: Vec<NurbCurve2D<S>> = (0..4)
652 .map(|i| {
653 let (a, b) = (q[i], q[(i + 1) % 4]);
654 arc2(
655 v2(cx + a.0, cy + a.1),
656 v2(cx + a.0 + b.0, cy + a.1 + b.1),
657 v2(cx + b.0, cy + b.1),
658 sqrt2_over_2(),
659 )
660 .unwrap()
661 })
662 .collect();
663 if clockwise {
664 arcs = arcs.iter().rev().map(|c| c.reverse()).collect();
665 }
666 arcs
667 }
668
669 fn axis_aligned_coordinate_system<S: Scalar>(origin: Vector3<S>) -> CoordinateSystem<S> {
676 let u = Vector3::from_array([S::ONE, S::ZERO, S::ZERO]);
677 let v = Vector3::from_array([S::ZERO, S::ONE, S::ZERO]);
678 let w = Vector3::from_array([S::ZERO, S::ZERO, S::from_f64(-1.0)]);
679 CoordinateSystem::try_new(origin, u, v, w).unwrap()
680 }
681
682 fn extruded<S: Scalar>(
684 cs: &CoordinateSystem<S>,
685 outer: Vec<NurbCurve2D<S>>,
686 holes: Vec<Vec<NurbCurve2D<S>>>,
687 ) -> Part<S> {
688 let mut part = Part::<S>::new();
689 let namer = Namer::new("extrude", "e").unwrap();
690 let holes: Vec<_> = holes
691 .into_iter()
692 .enumerate()
693 .map(|(k, h)| Profile::closed(h).with_prefix(&format!("h{k}")))
694 .collect();
695 extrude(
696 &mut part,
697 &ExtrudeNames::single(&namer),
698 cs,
699 &Profile::closed(outer),
700 &holes,
701 )
702 .unwrap();
703 part.check_names().unwrap();
704 part
705 }
706
707 fn assert_valid<S: Scalar>(model: &Model<S>) {
708 let params = ValidationParameters::default();
709 if let Err(e) = validate(¶ms, model) {
710 panic!("{e:?}");
711 }
712 if let Err(e) = validate_manifold(¶ms, model) {
713 panic!("{e:?}");
714 }
715 for edge_id in model.edges.keys() {
716 assert_eq!(model.coedges_of_edge(*edge_id).len(), 2);
717 }
718 }
719
720 fn check_extruded_square_is_valid<S: Scalar>() {
721 let cs = axis_aligned_coordinate_system(Vector3::from_array([S::ZERO; 3]));
722 let part = extruded(&cs, unit_square::<S>(), vec![]);
723 let model = part.topology();
724 assert_valid(model);
725 assert_eq!(model.faces.len(), 6);
726 assert_eq!(model.vertices.len(), 8);
727 assert_eq!(model.edges.len(), 12);
728 }
729 #[test]
730 fn extruded_square_is_valid() {
731 for_all_scalars!(check_extruded_square_is_valid);
732 }
733
734 fn check_extruded_square_with_hole_is_valid<S: Scalar>() {
735 let hole = square_hole::<S>(0.25, 0.25, 0.5);
736 let cs = axis_aligned_coordinate_system(Vector3::from_array([S::ZERO; 3]));
737 let part = extruded(&cs, unit_square::<S>(), vec![hole]);
738 let model = part.topology();
739 assert_valid(model);
740 assert_eq!(model.faces.len(), 6 + 4);
743 assert_eq!(model.vertices.len(), 8 + 8);
744 }
745 #[test]
746 fn extruded_square_with_hole_is_valid() {
747 for_all_scalars!(check_extruded_square_with_hole_is_valid);
748 }
749
750 fn check_extrude_offsets_footprint<S: Scalar>() {
751 let origin = Vector3::from_array([S::from_f64(2.0), S::from_f64(-1.0), S::from_f64(0.5)]);
752 let cs = axis_aligned_coordinate_system(origin);
753 let part = extruded(&cs, unit_square::<S>(), vec![]);
754 let model = part.topology();
755 assert_valid(model);
756 for vertex in model.vertices.values() {
760 assert!(
761 vertex.point[2].could_be_equal(S::from_f64(0.5))
762 || vertex.point[2].could_be_equal(S::from_f64(-0.5)),
763 "vertex at unexpected height: {:?}",
764 vertex.point
765 );
766 }
767 }
768 #[test]
769 fn extrude_offsets_footprint() {
770 for_all_scalars!(check_extrude_offsets_footprint);
771 }
772
773 fn check_extrude_profile_away_from_origin<S: Scalar>() {
776 let cs = axis_aligned_coordinate_system(Vector3::from_array([S::ZERO; 3]));
777 let outer = polygon(&[v2(-3.0, 2.0), v2(-1.0, 2.0), v2(-2.0, 5.0)]).unwrap();
778 let part = extruded(&cs, outer, vec![]);
779 let model = part.topology();
780 assert_valid(model);
781 assert_eq!(model.faces.len(), 5);
782 }
783 #[test]
784 fn extrude_profile_away_from_origin() {
785 for_all_scalars!(check_extrude_profile_away_from_origin);
786 }
787
788 fn check_extruded_ring_is_valid<S: Scalar>() {
790 let cs = axis_aligned_coordinate_system(Vector3::from_array([S::ZERO; 3]));
791 let outer = circle::<S>(0.0, 0.0, 2.0, false);
792 let hole = circle::<S>(0.3, 0.0, 1.0, true);
793 let part = extruded(&cs, outer, vec![hole]);
794 let model = part.topology();
795 assert_valid(model);
796 assert_eq!(model.faces.len(), 2 + 4 + 4);
797 }
798 #[test]
799 fn extruded_ring_is_valid() {
800 for_all_scalars!(check_extruded_ring_is_valid);
801 }
802
803 fn check_extrude_from_plane_both_directions<S: Scalar>() {
806 let plane = CoordinateSystem::try_new(
807 Vector3::from_array([S::ZERO, S::ZERO, S::ONE]),
808 Vector3::from_array([S::ONE, S::ZERO, S::ZERO]),
809 Vector3::from_array([S::ZERO, S::ONE, S::ZERO]),
810 Vector3::from_array([S::ZERO, S::ZERO, S::ONE]),
811 )
812 .unwrap();
813 let mut outer = polygon(&[v2(0.0, 0.0), v2(1.0, 0.0), v2(1.0, 1.0)]).unwrap();
814 outer.push(arc2(v2(1.0, 1.0), v2(0.0, 1.0), v2(0.0, 0.0), sqrt2_over_2()).unwrap());
815 outer.remove(2);
816 let namer = Namer::new("extrude", "e").unwrap();
817 let outer = Profile::closed(outer);
818 for (distance, far) in [(0.5, 1.5), (-0.5, 0.5)] {
819 let mut part = Part::<S>::new();
820 let names = ExtrudeNames::single(&namer);
821 extrude_from_plane(
822 &mut part,
823 &names,
824 &plane,
825 &outer,
826 &[],
827 S::from_f64(distance),
828 )
829 .unwrap();
830 part.check_names().unwrap();
831 let model = part.topology();
832 assert_valid(model);
833 assert!(
834 model
835 .vertices
836 .values()
837 .any(|v| v.point[2].could_be_equal(S::from_f64(far)))
838 );
839 let p0_start = part.vertex_id("extrude(e,p0,start)").unwrap();
843 let p0_end = part.vertex_id("extrude(e,p0,end)").unwrap();
844 let point = |v| model.get_vertex(v).unwrap().point;
845 assert!(point(p0_start).could_be_equal(&Vector3::from_array([
846 S::ZERO,
847 S::ZERO,
848 S::ONE
849 ])));
850 assert!(point(p0_end)[2].could_be_equal(S::from_f64(far)));
851 let end_cap = part.face_id("extrude(e,end)").unwrap();
852 assert!(model.iterate_face_coedges(end_cap).all(|c| {
853 model.coedge_start_vertex(c).unwrap().point[2].could_be_equal(S::from_f64(far))
854 }));
855 }
856 }
857 #[test]
858 fn extrude_from_plane_both_directions() {
859 for_all_scalars!(check_extrude_from_plane_both_directions);
860 }
861}
862
863#[cfg(test)]
864mod naming_tests {
865 use super::*;
866 use crate::common::polygon;
867 use geop_core_math::scalars::ScalInF64 as S;
868
869 #[test]
874 fn extruded_square_names_follow_the_profile() {
875 let square = polygon(&[
876 Vector2::from_array([S::ZERO, S::ZERO]),
877 Vector2::from_array([S::ONE, S::ZERO]),
878 Vector2::from_array([S::ONE, S::ONE]),
879 Vector2::from_array([S::ZERO, S::ONE]),
880 ])
881 .unwrap();
882 let plane = CoordinateSystem::try_new(
883 Vector3::zero(),
884 Vector3::from_array([S::ONE, S::ZERO, S::ZERO]),
885 Vector3::from_array([S::ZERO, S::ONE, S::ZERO]),
886 Vector3::from_array([S::ZERO, S::ZERO, S::ONE]),
887 )
888 .unwrap();
889 let mut part = Part::<S>::new();
890 let namer = Namer::new("extrude", "box").unwrap();
891 extrude_from_plane(
892 &mut part,
893 &ExtrudeNames::single(&namer),
894 &plane,
895 &Profile::closed(square),
896 &[],
897 S::ONE,
898 )
899 .unwrap();
900 part.check_names().unwrap();
901
902 let description = geop_core_part::PartDescription::of(&part).unwrap();
903 assert_eq!(description.solids.len(), 1);
904 assert_eq!(description.solids["extrude(box)"][0].len(), 6);
905 let mut side = description.faces["extrude(box,c0)"].outer.clone();
906 side.sort();
907 let mut expected: Vec<String> = [
908 "extrude(box,c0,end)",
909 "extrude(box,c0,start)",
910 "extrude(box,p0)",
911 "extrude(box,p1)",
912 ]
913 .iter()
914 .map(|e| e.to_string())
915 .collect();
916 expected.sort();
917 let unsigned: Vec<String> = side.iter().map(|e| e[1..].to_string()).collect();
918 let mut unsigned = unsigned;
919 unsigned.sort();
920 assert_eq!(unsigned, expected);
921 assert_eq!(
922 description.edges["extrude(box,p1)"].start,
923 "extrude(box,p1,start)"
924 );
925 assert_eq!(
926 description.edges["extrude(box,p1)"].end,
927 "extrude(box,p1,end)"
928 );
929 let corner = part.vertex_id("extrude(box,p2,end)").unwrap();
930 assert!(
931 part.topology()
932 .get_vertex(corner)
933 .unwrap()
934 .point
935 .could_be_equal(&Vector3::from_array([S::ONE; 3]))
936 );
937 }
938}