1use crate::sweep::{Frame, Path, Span, SweepLoop, sweep, sweep_drawn};
15use geop_core_geometry::nurb_curve::{
16 Handedness, Helix, HelixRow, NurbCurve2D, arc_span, cos_sin, helix_rows,
17};
18use geop_core_math::{
19 geop_error::{GeopError, GeopResult},
20 primitives::CoordinateSystem,
21 scalars::Scalar,
22 vector::{Vector2, Vector3},
23};
24use geop_core_topology::{SolidId, build::BuiltBody};
25use geop_ops_model::{NamedModel, Namer, Profile};
26
27pub fn revolution(axes: &CoordinateSystem, from: f64, to: f64) -> GeopResult<Path> {
38 let sweep = to - from;
39 if !(sweep != 0.0 && sweep.abs() <= 360.0) {
40 return Err(GeopError::new(format!(
41 "revolution: cannot turn from {from} to {to} degrees: it must be more than none and at most a full turn"
42 )));
43 }
44 let full = sweep.abs() == 360.0;
45 let (u, v, w) = (axes.u(), axes.v(), axes.w());
46 let direction = |degrees: f64| {
47 let (c, s) = cos_sin(degrees);
48 axes.uv_direction(&Vector2::from_array([
49 Scalar::from_f64(c),
50 Scalar::from_f64(s),
51 ]))
52 };
53 let frame = |e1: Vector3| Frame {
54 origin: *axes.origin(),
55 e1,
56 e2: *w,
57 };
58 let (spans, step) = if full {
61 (4, -90.0)
62 } else {
63 let spans = (sweep.abs() / 90.0).ceil() as usize;
64 (spans, sweep / spans as f64)
65 };
66 let angles: Vec<f64> = (0..spans + usize::from(!full))
67 .map(|k| from + k as f64 * step)
68 .collect();
69 let stations: Vec<Frame> = angles.iter().map(|&a| frame(direction(a))).collect();
70 let (weight, outwards) = arc_span(step);
71 let spans_of = (0..spans)
72 .map(|j| {
73 let (a, b) = (&stations[j], &stations[(j + 1) % stations.len()]);
74 Span::arc(frame(a.e1.add(&b.e1).prod_scalar(outwards)), weight)
75 })
76 .collect();
77 let (c, s) = cos_sin(from);
80 let tangent = v
81 .prod_scalar(Scalar::from_f64(c))
82 .sub(&u.prod_scalar(Scalar::from_f64(s)));
83 let along = stations[0]
84 .e1
85 .prod_cross(w)
86 .prod_dot(&tangent)
87 .mul(Scalar::from_f64(step));
88 let along_normal = along
89 .definite_sign()
90 .ok_or_else(|| GeopError::new(format!("revolution: the axes {axes} are degenerate")))?;
91 let (station_names, span_names) = if full {
92 (
93 ["a0", "a3", "a2", "a1"].map(String::from).to_vec(),
94 ["q3", "q2", "q1", "q0"]
95 .map(|q| Some(q.to_string()))
96 .to_vec(),
97 )
98 } else {
99 (
100 (0..angles.len()).map(|k| format!("a{k}")).collect(),
101 (0..spans).map(|k| Some(format!("q{k}"))).collect(),
102 )
103 };
104 Ok(Path {
105 stations,
106 spans: spans_of,
107 closed: full,
108 along_normal,
109 station_names,
110 span_names,
111 })
112}
113
114pub fn revolve(
126 model: &mut NamedModel,
127 namer: &Namer,
128 solid: Option<&str>,
129 axes: &CoordinateSystem,
130 from: f64,
131 to: f64,
132 loops: &[SweepLoop],
133) -> GeopResult<BuiltBody> {
134 let path = revolution(axes, from, to)?;
135 sweep_drawn(model, namer, &path, loops, solid)
136}
137
138pub fn screw_path(axes: &CoordinateSystem, helix: &Helix) -> GeopResult<Path> {
153 let rows = helix_rows(axes, helix)?;
154 let (origin, w) = (axes.origin(), axes.w());
155 let frame = |row: &HelixRow| {
156 let raised = origin.add(&w.prod_scalar(row.rise));
157 Frame {
158 origin: row
159 .out
160 .map_or(raised, |out| raised.add(&row.radial.prod_scalar(out))),
161 e1: row.radial,
162 e2: *w,
163 }
164 };
165 let stations: Vec<Frame> = rows.iter().step_by(2).map(frame).collect();
166 let spans: Vec<Span> = rows
167 .iter()
168 .skip(1)
169 .step_by(2)
170 .map(|row| Span::arc(frame(row), row.weight.expect("a middle row is weighted")))
171 .collect();
172 let along_normal = helix.handedness == Handedness::Left;
175 Ok(Path {
176 station_names: (0..stations.len()).map(|k| format!("a{k}")).collect(),
177 span_names: (0..spans.len()).map(|k| Some(format!("q{k}"))).collect(),
178 stations,
179 spans,
180 closed: false,
181 along_normal,
182 })
183}
184
185pub fn screw(
197 model: &mut NamedModel,
198 namer: &Namer,
199 solid: &str,
200 axes: &CoordinateSystem,
201 helix: &Helix,
202 loops: &[SweepLoop],
203) -> GeopResult<SolidId> {
204 let path = screw_path(axes, helix)?;
205 sweep_drawn(model, namer, &path, loops, Some(solid))?
206 .solid
207 .ok_or_else(|| GeopError::new("screw: built no solid"))
208}
209
210pub fn revolve_at_oriented(
218 model: &mut NamedModel,
219 namer: &Namer,
220 solid: &str,
221 profile: &Profile,
222 axes: &CoordinateSystem,
223) -> GeopResult<SolidId> {
224 profile.check_names()?;
225 let lp = if profile.is_closed() {
226 for (i, curve) in profile.curves.iter().enumerate() {
227 if curve.start()?[0].could_be_equal(Scalar::ZERO) {
228 return Err(GeopError::new(format!(
229 "revolve: a closed profile must stay off the axis, but its point {i} could be on it"
230 )));
231 }
232 }
233 SweepLoop::plain(profile.clone())
234 } else {
235 let (first, last) = match (profile.curves.first(), profile.curves.last()) {
237 (Some(first), Some(last)) => (first, last),
238 _ => {
239 return Err(GeopError::new(
240 "revolve: profile must have at least 1 curve",
241 ));
242 }
243 };
244 let mut closed = profile.clone();
245 closed
246 .curves
247 .push(NurbCurve2D::line(last.end()?, first.start()?)?);
248 closed.curve_names.push("axis".into());
249 let poles = closed
250 .curves
251 .iter()
252 .map(|c| Ok(c.start()?[0].could_be_equal(Scalar::ZERO)))
253 .collect::<GeopResult<Vec<bool>>>()?;
254 if !(poles[0] && poles[profile.curves.len()]) {
255 return Err(GeopError::new(
256 "revolve: an open profile must start and end at r = 0 (a pole)",
257 ));
258 }
259 let mut on_axis = vec![false; closed.curves.len()];
260 on_axis[profile.curves.len()] = true;
261 SweepLoop {
262 profile: closed,
263 poles,
264 on_axis,
265 apex: false,
266 }
267 };
268 let built = sweep(
271 model,
272 namer,
273 &revolution(axes, 0.0, 360.0)?,
274 &[lp],
275 Some(solid),
276 )?;
277 built
278 .solid
279 .ok_or_else(|| GeopError::new("revolve: built no solid"))
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::common::polyline;
286 use crate::shapes::v2;
287 use geop_core_geometry::nurb_curve::sqrt2_over_2;
288 use geop_core_math::vector::Vector2;
289 use geop_ops_model::testing::{assert_model_valid, assert_model_valid_sheets};
290
291 fn revolved_about_z(
293 model: &mut NamedModel,
294 namer: &Namer,
295 profile: &Profile,
296 ) -> GeopResult<SolidId> {
297 let axes = CoordinateSystem::world_at(Vector3::zero());
298 revolve_at_oriented(model, namer, &namer.root(), profile, &axes)
299 }
300
301 fn revolved(curves: Vec<NurbCurve2D>) -> NamedModel {
303 let mut model = NamedModel::new();
304 let namer = Namer::new("revolve", "r").unwrap();
305 revolved_about_z(&mut model, &namer, &Profile::open(curves)).unwrap();
306 model.check_names().unwrap();
307 model
308 }
309
310 #[test]
313 fn sphere_from_arcs_is_valid() {
314 let w = sqrt2_over_2();
315 let profile = vec![
316 NurbCurve2D::arc(v2(0.0, 1.0), v2(1.0, 1.0), v2(1.0, 0.0), w).unwrap(),
317 NurbCurve2D::arc(v2(1.0, 0.0), v2(1.0, -1.0), v2(0.0, -1.0), w).unwrap(),
318 ];
319 let model = revolved(profile);
320 let topology = model.topology();
321 assert_model_valid(topology);
322 assert_eq!(topology.faces.len(), 8);
323 for name in [
326 "revolve(r,p0)",
327 "revolve(r,p2)",
328 "revolve(r,p1,a0)",
329 "revolve(r,p1,a3)",
330 ] {
331 model.vertex_id(name).unwrap();
332 }
333 for name in ["revolve(r,c0,a0)", "revolve(r,c1,a2)", "revolve(r,p1,q3)"] {
334 model.edge_id(name).unwrap();
335 }
336 model.face_id("revolve(r,c1,q3)").unwrap();
337 }
338
339 #[test]
342 fn vase_with_spline_is_valid() {
343 let hom = |x: f64, y: f64| {
344 geop_core_math::vector::Vector3::from_array([
345 Scalar::from_f64(x),
346 Scalar::from_f64(y),
347 Scalar::ONE,
348 ])
349 };
350 let spline = geop_core_geometry::nurb_curve::NurbCurve::try_new(
351 3,
352 vec![hom(0.5, 2.0), hom(1.5, 1.5), hom(0.2, 0.7), hom(1.0, 0.0)],
353 vec![
354 Scalar::ZERO,
355 Scalar::ZERO,
356 Scalar::ZERO,
357 Scalar::ZERO,
358 Scalar::ONE,
359 Scalar::ONE,
360 Scalar::ONE,
361 Scalar::ONE,
362 ],
363 )
364 .unwrap();
365 let mut profile = polyline(&[v2(0.0, 2.0), v2(0.5, 2.0)]).unwrap();
366 profile.push(spline);
367 profile.extend(polyline(&[v2(1.0, 0.0), v2(0.0, 0.0)]).unwrap());
368 let model = revolved(profile);
369 let topology = model.topology();
370 assert_model_valid(topology);
371 assert_eq!(topology.faces.len(), 12);
372 }
373
374 #[test]
375 fn cone_is_valid() {
376 let profile = polyline(&[v2(0.0, 1.0), v2(1.0, 0.0), v2(0.0, 0.0)]).unwrap();
377 assert_model_valid(revolved(profile).topology());
378 }
379
380 #[test]
381 fn sphere_is_valid() {
382 let n = 6;
383 let points: Vec<Vector2> = (0..=n)
384 .map(|k| {
385 if k == 0 || k == n {
386 return v2(0.0, if k == 0 { 1.0 } else { -1.0 });
387 }
388 let t = std::f64::consts::PI * (k as f64) / (n as f64);
389 v2(t.sin(), t.cos())
390 })
391 .collect();
392 assert_model_valid(revolved(polyline(&points).unwrap()).topology());
393 }
394
395 fn revolved_ring(curves: Vec<NurbCurve2D>) -> NamedModel {
397 let mut model = NamedModel::new();
398 let namer = Namer::new("revolve", "r").unwrap();
399 revolved_about_z(&mut model, &namer, &Profile::closed(curves)).unwrap();
400 model.check_names().unwrap();
401 model
402 }
403
404 #[test]
407 fn square_ring_is_valid() {
408 let profile = polyline(&[
410 v2(1.0, 1.0),
411 v2(2.0, 1.0),
412 v2(2.0, 0.0),
413 v2(1.0, 0.0),
414 v2(1.0, 1.0),
415 ])
416 .unwrap();
417 let model = revolved_ring(profile);
418 let topology = model.topology();
419 assert_model_valid(topology);
420 assert_eq!(topology.faces.len(), 16);
421 assert_eq!(topology.vertices.len(), 16);
422 assert_eq!(topology.edges.len(), 32);
423 for name in ["revolve(r,c0,q0)", "revolve(r,c3,q3)", "revolve(r,p0,a3)"] {
424 assert!(
425 model.face_id(name).is_ok() || model.vertex_id(name).is_ok(),
426 "{name}"
427 );
428 }
429 model.edge_id("revolve(r,p0,q3)").unwrap();
430 model.edge_id("revolve(r,c3,a0)").unwrap();
431 }
432
433 #[test]
436 fn torus_is_valid() {
437 let w = sqrt2_over_2();
438 let profile = vec![
439 NurbCurve2D::arc(v2(2.0, 1.0), v2(3.0, 1.0), v2(3.0, 0.0), w).unwrap(),
440 NurbCurve2D::arc(v2(3.0, 0.0), v2(3.0, -1.0), v2(2.0, -1.0), w).unwrap(),
441 NurbCurve2D::arc(v2(2.0, -1.0), v2(1.0, -1.0), v2(1.0, 0.0), w).unwrap(),
442 NurbCurve2D::arc(v2(1.0, 0.0), v2(1.0, 1.0), v2(2.0, 1.0), w).unwrap(),
443 ];
444 let model = revolved_ring(profile);
445 let topology = model.topology();
446 assert_model_valid(topology);
447 assert_eq!(topology.faces.len(), 16);
448 }
449
450 #[test]
452 fn rings_stay_off_the_axis() {
453 let profile = polyline(&[v2(0.0, 1.0), v2(1.0, 1.0), v2(1.0, 0.0), v2(0.0, 1.0)]).unwrap();
454 let mut model = NamedModel::new();
455 let namer = Namer::new("revolve", "r").unwrap();
456 assert!(revolved_about_z(&mut model, &namer, &Profile::closed(profile)).is_err());
457 }
458
459 fn square_loop(r0: f64) -> SweepLoop {
462 let profile = Profile::closed(
463 polyline(&[
464 v2(r0, 0.0),
465 v2(r0 + 1.0, 0.0),
466 v2(r0 + 1.0, 1.0),
467 v2(r0, 1.0),
468 v2(r0, 0.0),
469 ])
470 .unwrap(),
471 );
472 let on = r0 == 0.0;
473 SweepLoop {
474 profile,
475 poles: vec![on, false, false, on],
476 on_axis: vec![false, false, false, on],
477 apex: false,
478 }
479 }
480
481 fn z_axes() -> CoordinateSystem {
482 CoordinateSystem::world_at(Vector3::zero())
483 }
484
485 fn revolved_by(loops: &[SweepLoop], from: f64, to: f64) -> NamedModel {
488 let mut model = NamedModel::new();
489 let namer = Namer::new("revolve", "r").unwrap();
490 revolve(
491 &mut model,
492 &namer,
493 Some("revolve(r)"),
494 &z_axes(),
495 from,
496 to,
497 loops,
498 )
499 .unwrap();
500 model.check_names().unwrap();
501 model
502 }
503
504 #[test]
507 fn partial_turns_are_capped() {
508 for (from, to, spans) in [
509 (0.0, 90.0, 1),
510 (0.0, 270.0, 3),
511 (0.0, -120.0, 2),
512 (-30.0, 45.0, 1),
513 ] {
514 let model = revolved_by(&[square_loop(0.0)], from, to);
515 let topology = model.topology();
516 assert_model_valid(topology);
517 assert_eq!(topology.faces.len(), 3 * spans + 2, "{from} to {to}");
518 let axis = model.edge_id("revolve(r,c3)").unwrap();
519 let caps: Vec<_> = topology
520 .coedges_of_edge(axis)
521 .into_iter()
522 .map(|c| {
523 model
524 .name_of(topology.get_coedge(c).unwrap().face)
525 .unwrap()
526 .to_string()
527 })
528 .collect();
529 assert_eq!(caps.len(), 2);
530 assert!(caps.contains(&"revolve(r,start)".to_string()));
531 assert!(caps.contains(&"revolve(r,end)".to_string()));
532 let corner = topology
534 .get_vertex(model.vertex_id(&format!("revolve(r,p2,a{spans})")).unwrap())
535 .unwrap()
536 .point;
537 let (c, s) = cos_sin(to);
538 let expected =
539 Vector3::from_array([Scalar::from_f64(c), Scalar::from_f64(s), Scalar::ONE]);
540 assert!(corner.could_be_equal(&expected), "{corner:?}");
541 }
542 }
543
544 #[test]
548 #[ignore = "slow: 4 s — run with `cargo test -- --ignored`"]
549 fn holes_sweep_voids_and_tunnels() {
550 let outer = SweepLoop::plain(Profile::closed(
551 polyline(&[
552 v2(1.0, 0.0),
553 v2(4.0, 0.0),
554 v2(4.0, 3.0),
555 v2(1.0, 3.0),
556 v2(1.0, 0.0),
557 ])
558 .unwrap(),
559 ));
560 let hole = SweepLoop::plain(
561 Profile::closed(
562 polyline(&[
563 v2(2.0, 1.0),
564 v2(2.0, 2.0),
565 v2(3.0, 2.0),
566 v2(3.0, 1.0),
567 v2(2.0, 1.0),
568 ])
569 .unwrap(),
570 )
571 .with_prefix("h"),
572 );
573 let loops = [outer, hole];
574 let full = revolved_by(&loops, 0.0, 360.0);
575 assert_model_valid(full.topology());
576 let solid = full.solid_id("revolve(r)").unwrap();
577 assert_eq!(full.topology().get_solid(solid).unwrap().shells.len(), 2);
578 assert_eq!(full.topology().faces.len(), 32);
579
580 let quarter = revolved_by(&loops, 0.0, 90.0);
581 assert_model_valid(quarter.topology());
582 assert_eq!(quarter.topology().faces.len(), 4 + 4 + 2);
583 let cap = quarter.face_id("revolve(r,start)").unwrap();
584 assert_eq!(quarter.topology().get_face(cap).unwrap().holes.len(), 1);
585 }
586
587 #[test]
591 fn sheets_are_walls_alone() {
592 let namer = Namer::new("revolve", "r").unwrap();
593 for (chain, faces, free) in [
594 (polyline(&[v2(1.0, 0.0), v2(1.0, 1.0)]).unwrap(), 4, 8),
595 (polyline(&[v2(0.0, 0.0), v2(1.0, 0.0)]).unwrap(), 4, 4),
596 ] {
597 let mut model = NamedModel::new();
598 let profile = Profile::open(chain);
599 let poles = profile
600 .joint_names
601 .iter()
602 .enumerate()
603 .map(|(i, _)| {
604 i == 0 && profile.curves[0].start().unwrap()[0].could_be_equal(Scalar::ZERO)
605 })
606 .collect();
607 let lp = SweepLoop {
608 on_axis: vec![false],
609 poles,
610 profile,
611 apex: false,
612 };
613 let built = revolve(&mut model, &namer, None, &z_axes(), 0.0, 360.0, &[lp]).unwrap();
614 model.check_names().unwrap();
615 assert!(built.solid.is_none());
616 let topology = model.topology();
617 assert_model_valid_sheets(topology);
618 assert_eq!(topology.faces.len(), faces);
619 assert_eq!(model.sheet_face_names().len(), faces);
620 let one_sided = topology
621 .edges
622 .keys()
623 .filter(|&&e| topology.coedges_of_edge(e).len() == 1)
624 .count();
625 assert_eq!(one_sided, free);
626 }
627 }
628
629 #[test]
634 fn a_screw_sweeps_a_thread() {
635 a_screw_sweeps_a_thread_tapered(Scalar::ZERO);
636 }
637
638 #[test]
642 fn a_tapered_screw_sweeps_a_pipe_thread() {
643 a_screw_sweeps_a_thread_tapered(Scalar::from_f64(1.0 / 32.0));
644 }
645
646 fn a_screw_sweeps_a_thread_tapered(taper: Scalar) {
647 use geop_core_geometry::nurb_curve::NurbCurve3D;
648 for handedness in [Handedness::Right, Handedness::Left] {
649 let mut model = NamedModel::new();
650 let namer = Namer::new("screw", "t").unwrap();
651 let corners = [
653 (1.0, -0.2),
654 (1.3, -0.2),
655 (1.3, 0.2),
656 (1.0, 0.2),
657 (1.0, -0.2),
658 ];
659 let points: Vec<Vector2> = corners.iter().map(|&(r, z)| v2(r, z)).collect();
660 let profile = Profile::closed(crate::common::polyline(&points).unwrap());
661 let axes = CoordinateSystem::world_at(Vector3::zero());
662 let pitch = Scalar::ONE;
663 screw(
664 &mut model,
665 &namer,
666 "screw(t)",
667 &axes,
668 &Helix::new(pitch, 1.25, handedness).tapered(taper),
669 &[SweepLoop::plain(profile)],
670 )
671 .unwrap();
672 model.check_names().unwrap();
673 assert_model_valid(model.topology());
674 assert_eq!(model.topology().faces.len(), 4 * 5 + 2);
675 let edge = model.edge_id("screw(t,p0,q2)").unwrap();
678 let curve = &model.topology().get_edge(edge).unwrap().curve;
679 let lowered = CoordinateSystem::world_at(Vector3::from_array([
680 Scalar::ZERO,
681 Scalar::ZERO,
682 Scalar::from_f64(-0.2),
683 ]));
684 let helix = NurbCurve3D::helix(
685 &lowered,
686 Scalar::ONE,
687 &Helix::new(pitch, 1.25, handedness).tapered(taper),
688 )
689 .unwrap();
690 for k in 0..=4 {
691 let along = Scalar::from_ratio(k, 4).unwrap();
692 let (t0, t1) = curve.domain();
693 let p = curve.evaluate(t0.add(t1.sub(t0).mul(along))).unwrap();
694 let h = helix
695 .evaluate(
696 Scalar::from_ratio(2, 5)
697 .unwrap()
698 .add(along.div(Scalar::from_f64(5.0)).unwrap()),
699 )
700 .unwrap();
701 assert!(p.could_be_equal(&h), "{handedness:?} {k}: {p:?} vs {h:?}");
702 }
703 }
704 }
705}