geop_ops_extrude_revolve/revolve.rs
1//! Revolve a planar profile, given as `(r, z)` points in the half-plane `r
2//! >= 0` (whose first and last points must be poles, `r = 0`), 360 degrees
3//! around a vertical axis, entirely from euler operations — built one
4//! angular quadrant *column* at a time (all `P = profile.len() - 1` row
5//! faces of a given 90-degree wedge, before moving to the next wedge),
6//! exactly the same strategy [`sphere_octants`](super::sphere::sphere_octants)
7//! uses for its own dedicated (exact, doubly-curved) construction, just
8//! generalized to an arbitrary profile instead of a fixed 2-segment
9//! pole-equator-pole one.
10//!
11//! One persistent face (`mvfs`'s own, playing the same role `extrude`'s
12//! placeholder does) always holds whatever's still left to close off. The
13//! very first column (meridian 0, the profile's own `P`-edge chain, straight
14//! in the profile direction) is bootstrapped directly from `mvfs`'s vertex.
15//! Each of the next 2 columns grows a fresh meridian one angular "beam" arc
16//! at a time (one per non-pole row) and immediately closes every row's own
17//! degree-(2, 1) quadrant face (`mve` for the beam, `mef` — minting that
18//! row's new meridian edge as it goes — for the face) against the *previous*
19//! meridian. The 4th and last column closes back onto the very first
20//! meridian's own reversed edges instead of growing a new one — only the
21//! interior beams (angle 3 `->` 0) are still new, one per non-last row's own
22//! closing `mef` — and its very last row needs no `mef` at all: by the time
23//! every other row of that column has closed, its own boundary is already
24//! sitting complete on the placeholder face, so `replace_face` alone turns
25//! it real. No separate degenerate cap is ever needed, unlike a naive
26//! row-by-row sweep (which always leaves one extra zero-area face behind at
27//! the final pole).
28//!
29//! A profile row at `r = 0` (only ever the first/last, both required poles)
30//! collapses to a single shared vertex — no angular beam is grown for it at
31//! all, and its neighboring rows' own meridian edges connect straight to
32//! that one shared vertex instead, both euler ops here (`mve`, `mef`) taking
33//! existing vertices as-is rather than minting fresh ones.
34
35use crate::common::{
36 Profile, arc3, embed_curve, embed_point, end_point, line2, sqrt2_over_2, start_point,
37};
38use geop_core_geometry::{
39 nurb_curve::{NurbCurve2D, NurbCurve3D},
40 nurb_surface::NurbSurface,
41};
42use geop_core_math::{
43 geop_error::{GeopError, GeopResult, WithContext},
44 primitives::CoordinateSystem,
45 scalars::Scalar,
46 vector::{Vector2, Vector3, Vector4},
47 with_context,
48};
49use geop_core_part::{Namer, Part};
50use geop_core_topology::{CoedgeId, SolidId};
51
52/// Bridges a quadrant face's own `(u, v)` boundary-loop gap at a pole row
53/// it touches on its `v = 0` (top) side — the row's two meridian edges meet
54/// directly at the shared pole vertex (a real, correctly-shared 3-D vertex,
55/// nothing wrong there), but nothing was ever built to represent that
56/// row's own zero-length angular span in *parameter* space, so the loop
57/// jumps straight from `u = 0` to `u = 1` without a pcurve covering the
58/// gap between them — invisible to per-edge/per-coedge validation (every
59/// individual coedge's own pcurve is perfectly valid), but fatal to
60/// `contains::face::face_contains`'s ray-casting, which relies on the
61/// coedges' pcurves alone tracing a fully closed 2-D polygon.
62///
63/// `at` is the coedge immediately *after* the gap in its current loop (the
64/// row's own pre-existing meridian coedge, reused directly in place of a
65/// real angular beam since a pole has no angular extent) — the bridge is
66/// inserted right before it, via [`Part::add_vertex_coedge`] onto
67/// `at.prev` (whatever's on the gap's other side), sitting at the same
68/// shared pole vertex the whole way rather than minting any new edge or
69/// vertex of its own.
70pub(crate) fn close_top_pole_gap<S: Scalar>(part: &mut Part<S>, at: CoedgeId) -> GeopResult<()> {
71 let before = part.topology().get_coedge(at)?.prev;
72 let pole = part.topology().coedge_end_vertex_id(before)?;
73 part.add_vertex_coedge(before, pole, beam_pcurve_top()?)
74 .with_context("revolve_at: closing top-pole pcurve gap failed")?;
75 Ok(())
76}
77/// Like [`close_top_pole_gap`], but for a pole row touched on a quadrant's
78/// `v = 1` (bottom) side: the gap sits right *after* `at` (the row's own
79/// meridian coedge) instead of right before it, so the bridge grows from
80/// `at` itself.
81pub(crate) fn close_bottom_pole_gap<S: Scalar>(part: &mut Part<S>, at: CoedgeId) -> GeopResult<()> {
82 let pole = part.topology().coedge_end_vertex_id(at)?;
83 part.add_vertex_coedge(at, pole, beam_pcurve_bottom()?)
84 .with_context("revolve_at: closing bottom-pole pcurve gap failed")?;
85 Ok(())
86}
87
88const N: usize = 4;
89
90/// The pcurve an "old" (already-built, at whichever angle is currently
91/// leading) meridian coedge carries, on *whichever* quadrant it ends up
92/// on: `u = 1` (the angularly-later side of that quadrant's own arc), `v: 0
93/// -> 1` (top row to bottom row). Every meridian coedge here plays this
94/// exact role, so one constant works for all of them.
95fn meridian_pcurve<S: Scalar>() -> GeopResult<geop_core_geometry::nurb_curve::NurbCurve2D<S>> {
96 line2(
97 Vector2::from_array([S::ONE, S::ZERO]),
98 Vector2::from_array([S::ONE, S::ONE]),
99 )
100}
101/// The pcurve a `mef`'s own new meridian edge carries, on the quadrant it
102/// closes off: `u = 0` (the angularly-earlier side), `v: 1 -> 0` (bottom
103/// row back to top row — the boundary loop runs the opposite way around
104/// this side, same reason a plain rectangle's own right edge runs
105/// top-to-bottom even though its top edge ran left-to-right).
106fn meridian_closing_pcurve<S: Scalar>() -> GeopResult<geop_core_geometry::nurb_curve::NurbCurve2D<S>>
107{
108 line2(
109 Vector2::from_array([S::ZERO, S::ONE]),
110 Vector2::from_array([S::ZERO, S::ZERO]),
111 )
112}
113/// The pcurve a row's angular beam carries as the *upper* quadrant's own
114/// `v = 1` (bottom) edge: `u: 1 -> 0` (old angle to new).
115fn beam_pcurve_bottom<S: Scalar>() -> GeopResult<geop_core_geometry::nurb_curve::NurbCurve2D<S>> {
116 line2(
117 Vector2::from_array([S::ONE, S::ONE]),
118 Vector2::from_array([S::ZERO, S::ONE]),
119 )
120}
121/// The pcurve the same beam carries (reversed) as the *lower* quadrant's
122/// own `v = 0` (top) edge: `u: 0 -> 1` (new angle back to old).
123fn beam_pcurve_top<S: Scalar>() -> GeopResult<geop_core_geometry::nurb_curve::NurbCurve2D<S>> {
124 line2(
125 Vector2::from_array([S::ZERO, S::ZERO]),
126 Vector2::from_array([S::ONE, S::ZERO]),
127 )
128}
129
130/// An exact 90-degree arc, around `center`, from `p0` to `p1` — one row's
131/// own angular "beam" edge between two adjacent quadrant columns.
132fn beam_curve<S: Scalar>(
133 p0: Vector3<S>,
134 p1: Vector3<S>,
135 center: Vector3<S>,
136 w: S,
137) -> GeopResult<NurbCurve3D<S>> {
138 let mid = p0.add(&p1).sub(¢er);
139 arc3(p0, mid, p1, w)
140}
141
142/// The quadrant of the surface of revolution swept by the profile curve
143/// `curve` (in `(r, z)`) from angle `angle_new` back to `angle_old`, where
144/// `dirs[k]` is the in-plane direction of angle `k`.
145///
146/// The tensor product of `curve` with an exact 90-degree arc: `u` runs along
147/// the arc (degree 2, from `angle_new` to `angle_old`), `v` along `curve`
148/// (its own degree and knots). A profile control point `(w r, w z, w)` sweeps
149/// the arc `P(angle_new), P(angle_new) + P(angle_old) - center, P(angle_old)`
150/// with weights `w, w √2/2, w`; at `r = 0` all three collapse onto the axis,
151/// so poles need no special case.
152fn quadrant_patch<S: Scalar>(
153 curve: &NurbCurve2D<S>,
154 coordinate_system: &CoordinateSystem<S>,
155 dirs: &[Vector3<S>; N],
156 angle_new: usize,
157 angle_old: usize,
158) -> GeopResult<NurbSurface<S, 4>> {
159 let (o, axis) = (coordinate_system.origin(), coordinate_system.w());
160 let (d_new, d_old) = (dirs[angle_new], dirs[angle_old]);
161 let w = sqrt2_over_2::<S>();
162 let mid = d_new.add(&d_old);
163 let rows = [(d_new, S::ONE), (mid, w), (d_old, S::ONE)];
164 let control_points = rows
165 .iter()
166 .flat_map(|(d, weight)| {
167 curve.control_points.iter().map(move |cp| {
168 let p = embed_point(cp, o, d, axis);
169 Vector4::from_array([
170 p[0].mul(*weight),
171 p[1].mul(*weight),
172 p[2].mul(*weight),
173 p[3].mul(*weight),
174 ])
175 })
176 })
177 .collect();
178 NurbSurface::try_new(
179 2,
180 curve.degree,
181 control_points,
182 vec![S::ZERO, S::ZERO, S::ZERO, S::ONE, S::ONE, S::ONE],
183 curve.knot_vector.clone(),
184 )
185}
186
187/// Revolve `profile` — an open chain of curves in the `(r, z)` half-plane
188/// `r >= 0`, starting and ending on the axis (`r = 0`), each clamped and on
189/// the domain `[0, 1]` — 360 degrees around a vertical axis through `origin`
190/// (parallel to the z-axis), producing a closed, manifold solid of exactly
191/// `4 * profile.len()` faces (no leftover degenerate one) — `origin` is added
192/// to every generated point, so the profile's own `z` values are relative to
193/// `origin`'s `z`. See the module doc for the overall column-by-column
194/// strategy, and [`revolve_at_oriented`] for how the result is named.
195///
196/// The profile runs "top-down": walked from its first point to its last,
197/// the region it bounds together with the axis lies on its right (e.g.
198/// `(0, h) -> (r, h) -> (r, 0) -> (0, 0)` for a cylinder). Walked the other
199/// way, the solid comes out inside-out.
200///
201/// A thin wrapper around [`revolve_at_oriented`] with the identity
202/// (z-axis) coordinate system — see that function to revolve around an
203/// arbitrary axis (e.g. [`super::cylinder::revolved_cylinder_along_axis`]).
204pub fn revolve_at<S: Scalar>(
205 part: &mut Part<S>,
206 namer: &Namer,
207 profile: &Profile<S>,
208 origin: Vector3<S>,
209) -> GeopResult<SolidId> {
210 let identity = CoordinateSystem::try_new(
211 origin,
212 Vector3::from_array([S::ONE, S::ZERO, S::ZERO]),
213 Vector3::from_array([S::ZERO, S::ONE, S::ZERO]),
214 Vector3::from_array([S::ZERO, S::ZERO, S::ONE]),
215 )
216 .expect("axis-aligned basis is never degenerate");
217 revolve_at_oriented(part, namer, &namer.root(), profile, &identity)
218}
219
220/// Like [`revolve_at`], but revolves around `coordinate_system`'s own `w`
221/// axis instead of always the ambient z-axis: a profile point `(r, z)` maps
222/// to `coordinate_system.to_xyz([r cos, r sin, z])` — `u`/`v` span the
223/// equatorial plane (the angle-0 direction and its 90-degree-around-`w`
224/// follower, respectively) and `w` is the revolution axis, exactly the
225/// same role `extrude`'s own coordinate system's `w` plays as its sweep
226/// direction. Every position this builds still ultimately comes from this
227/// one `pos`/`centers` pair — the rest of the function (euler operations,
228/// pcurves) has no notion of x/y/z at all, so generalizing the axis needed
229/// no changes anywhere else.
230///
231/// Everything built is named after the profile's curves `X` and joints `P`
232/// (see [`Profile`]), the angles `a0..a3` at which the meridians lie
233/// (`a0` along `u`, `a1` along `v`, ...), and the quadrants `q0..q3` between
234/// them (`q0` from `a0` to `a1`, ...), following `geop_core_part`'s scheme:
235///
236/// | entity | name |
237/// |---|---|
238/// | the solid | `solid` (usually `N` itself) |
239/// | face swept by `X` through quadrant `q` | `N(X,q)` |
240/// | meridian edge: `X` at angle `a` | `N(X,a)` |
241/// | circular edge swept by `P` through `q` | `N(P,q)` |
242/// | vertex of `P` at angle `a` | `N(P,a)` |
243/// | vertex of `P` on the axis (a pole) | `N(P)` |
244pub fn revolve_at_oriented<S: Scalar>(
245 part: &mut Part<S>,
246 namer: &Namer,
247 solid: &str,
248 profile: &Profile<S>,
249 coordinate_system: &CoordinateSystem<S>,
250) -> GeopResult<SolidId> {
251 profile.check_names()?;
252 if profile.is_closed() {
253 return Err(GeopError::new(
254 "revolve: the profile must be an open chain with a name for its end joint",
255 ));
256 }
257 let curves = &profile.curves;
258 if curves.is_empty() {
259 return Err(GeopError::new(
260 "revolve: profile must have at least 1 curve",
261 ));
262 }
263 let p_segments = curves.len();
264 // Row `i`: the profile's `i`-th vertex, where curve `i` starts.
265 let mut rows = curves
266 .iter()
267 .map(start_point)
268 .collect::<GeopResult<Vec<_>>>()?;
269 rows.push(end_point(&curves[p_segments - 1])?);
270 for i in 0..p_segments - 1 {
271 if !end_point(&curves[i])?.could_be_equal(&rows[i + 1]) {
272 return Err(GeopError::new(format!(
273 "revolve: profile curve {i} does not end where curve {} starts",
274 i + 1
275 )));
276 }
277 }
278 let m = rows.len();
279
280 let zero = S::ZERO;
281 let one = S::ONE;
282 let (u, v) = (coordinate_system.u(), coordinate_system.v());
283 let dirs = [
284 *u,
285 *v,
286 u.prod_scalar(zero.sub(one)),
287 v.prod_scalar(zero.sub(one)),
288 ];
289
290 let degenerate: Vec<bool> = rows.iter().map(|p| p[0].could_be_equal(zero)).collect();
291 if !degenerate[0] {
292 return Err(GeopError::new(
293 "revolve: profile must start at r = 0 (a pole)",
294 ));
295 }
296 if !degenerate[m - 1] {
297 return Err(GeopError::new(
298 "revolve: profile must end at r = 0 (a pole)",
299 ));
300 }
301
302 // Names, see the table above.
303 let face_name = |i: usize, k: usize| namer.name(&[&profile.curve_names[i], &format!("q{k}")]);
304 let meridian_name =
305 |i: usize, k: usize| namer.name(&[&profile.curve_names[i], &format!("a{k}")]);
306 let beam_name =
307 |row: usize, k: usize| namer.name(&[&profile.joint_names[row], &format!("q{k}")]);
308 let vertex_name = |row: usize, k: usize| {
309 if degenerate[row] {
310 namer.name(&[&profile.joint_names[row]])
311 } else {
312 namer.name(&[&profile.joint_names[row], &format!("a{k}")])
313 }
314 };
315
316 let centers: Vec<Vector3<S>> = rows
317 .iter()
318 .map(|p| coordinate_system.to_xyz(&Vector3::from_array([zero, zero, p[1]])))
319 .collect();
320 let pos = |i: usize, k: usize| -> Vector3<S> {
321 if degenerate[i] {
322 centers[i]
323 } else {
324 centers[i].add(&dirs[k].prod_scalar(rows[i][0]))
325 }
326 };
327 // Curve `i` of the profile at angle `k`, from row `i` to row `i + 1`.
328 let meridian = |i: usize, k: usize| {
329 embed_curve(
330 &curves[i],
331 coordinate_system.origin(),
332 &dirs[k],
333 coordinate_system.w(),
334 )
335 };
336 let w = sqrt2_over_2::<S>();
337
338 // The placeholder face becomes the last segment's last quadrant, via
339 // `replace_face` at the very end.
340 let (v0, face_id, solid_id) = part.mvfs(
341 centers[0],
342 vertex_name(0, 0),
343 face_name(p_segments - 1, N - 1),
344 solid,
345 )?;
346
347 // Bootstrap meridian 0 (angle index 0): the `p_segments`-edge profile
348 // chain from `v0` through every other row, straight in 3-D (the user's
349 // own profile segments). `anchors[i]` (forward) gets rebuilt as
350 // segment `i`'s own "old" meridian every time a new angle is grown;
351 // `mirrors[i]` (reversed) stays untouched, needed only once more, to
352 // close the very last angle back onto this first one.
353 let mut anchors = Vec::with_capacity(p_segments);
354 let mut mirrors = Vec::with_capacity(p_segments);
355 let (_, a0, r0, _) = part
356 .mve_from_vertex(
357 face_id,
358 v0,
359 meridian(0, 0)?,
360 meridian_pcurve()?,
361 meridian_closing_pcurve()?,
362 pos(1, 0),
363 vertex_name(1, 0),
364 meridian_name(0, 0),
365 )
366 .with_context("revolve_at: bootstrap segment 0 failed")?;
367 anchors.push(a0);
368 mirrors.push(r0);
369 for i in 1..p_segments {
370 let (_, a, r, _) = part
371 .mve(
372 anchors[i - 1],
373 meridian(i, 0)?,
374 meridian_pcurve()?,
375 meridian_closing_pcurve()?,
376 pos(i + 1, 0),
377 vertex_name(i + 1, 0),
378 meridian_name(i, 0),
379 )
380 .with_context(with_context!("revolve_at: bootstrap segment {i} failed"))?;
381 anchors.push(a);
382 mirrors.push(r);
383 }
384
385 // Grow meridians 1..N-1 (angle indices 1, 2, 3), closing all
386 // `p_segments` quadrant faces of each new column as it's grown: for
387 // every non-pole interior row, one new "beam" (angular arc) edge grows
388 // that row across to the new angle (`mve`); each segment's own closing
389 // `mef` then mints its own new meridian edge (straight, at the new
390 // angle) using whichever of its two rows' beams exist, or the row's
391 // shared vertex directly (via the *old* meridian coedge) if a row is a
392 // pole.
393 for k in 0..N - 1 {
394 let k1 = k + 1;
395 let mut beam_fwd: Vec<Option<CoedgeId>> = vec![None; p_segments + 1];
396 let mut beam_rev: Vec<Option<CoedgeId>> = vec![None; p_segments + 1];
397 for row in 1..p_segments {
398 if !degenerate[row] {
399 let (_, bf, br, _) = part
400 .mve(
401 anchors[row - 1],
402 beam_curve(pos(row, k), pos(row, k1), centers[row], w)?,
403 beam_pcurve_bottom()?,
404 beam_pcurve_top()?,
405 pos(row, k1),
406 vertex_name(row, k1),
407 beam_name(row, k),
408 )
409 .with_context(with_context!(
410 "revolve_at: beam at row {row}, angle {k} -> {k1} failed"
411 ))?;
412 beam_fwd[row] = Some(bf);
413 beam_rev[row] = Some(br);
414 }
415 }
416
417 let mut new_anchors = Vec::with_capacity(p_segments);
418 for i in 0..p_segments {
419 let coedge1 = if degenerate[i + 1] {
420 anchors[i]
421 } else {
422 beam_fwd[i + 1].unwrap()
423 };
424 let coedge2 = if degenerate[i] {
425 anchors[i]
426 } else {
427 beam_rev[i].unwrap()
428 };
429 let curve = meridian(i, k1)?.reverse();
430 let surface = quadrant_patch(&curves[i], coordinate_system, &dirs, k1, k)?;
431 let (_, _, _, coedge_backward) = part
432 .mef(
433 coedge1,
434 coedge2,
435 curve,
436 meridian_closing_pcurve()?,
437 meridian_pcurve()?,
438 surface,
439 meridian_name(i, k1),
440 face_name(i, k),
441 )
442 .with_context(with_context!(
443 "revolve_at: closing segment {i}, angle {k} -> {k1} failed"
444 ))?;
445 if degenerate[i] {
446 close_top_pole_gap(part, coedge2)?;
447 }
448 if degenerate[i + 1] {
449 close_bottom_pole_gap(part, coedge1)?;
450 }
451 new_anchors.push(coedge_backward);
452 }
453 anchors = new_anchors;
454 }
455
456 // Close the last column (angle 3 -> 0) back onto the very first
457 // meridian's own mirrors, reusing `anchors`/`mirrors` directly instead
458 // of growing anything new on the meridian side — only the interior
459 // beams (angle 3 -> 0) are actually new, one per non-last row's own
460 // closing `mef`. The very last segment needs no `mef` at all: after
461 // all the others close, its own boundary is already exactly what's
462 // left on the placeholder face, so it only needs `replace_face` to
463 // become real — the same trick `sphere_octants` uses, no separate
464 // degenerate cap required.
465 let (k, k1) = (N - 1, 0);
466 for i in 0..p_segments - 1 {
467 let row = i + 1;
468 let curve = beam_curve(pos(row, k), pos(row, k1), centers[row], w)?;
469 let surface = quadrant_patch(&curves[i], coordinate_system, &dirs, k1, k)?;
470 part.mef(
471 anchors[i],
472 mirrors[i],
473 curve,
474 beam_pcurve_bottom()?,
475 beam_pcurve_top()?,
476 surface,
477 beam_name(row, k),
478 face_name(i, k),
479 )
480 .with_context(with_context!(
481 "revolve_at: closing final beam at row {row} failed"
482 ))?;
483 if degenerate[i] {
484 close_top_pole_gap(part, anchors[i])?;
485 }
486 }
487 let last = p_segments - 1;
488 let surface = quadrant_patch(&curves[last], coordinate_system, &dirs, k1, k)?;
489 if degenerate[last] {
490 close_top_pole_gap(part, anchors[last])?;
491 }
492 if degenerate[last + 1] {
493 close_bottom_pole_gap(part, anchors[last])?;
494 }
495 part.replace_face(face_id, surface)
496 .with_context("revolve_at: final replace_face failed")?;
497
498 Ok(solid_id)
499}
500
501/// `revolve_at` around the z-axis itself (`origin = (0, 0, 0)`).
502pub fn revolve<S: Scalar>(
503 part: &mut Part<S>,
504 namer: &Namer,
505 profile: &Profile<S>,
506) -> GeopResult<SolidId> {
507 revolve_at(part, namer, profile, Vector3::from_array([S::ZERO; 3]))
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::common::{arc2, polyline};
514 use geop_core_math::for_all_scalars;
515 use geop_core_topology::{
516 Model,
517 validation::{ValidationParameters, validate, validate_manifold},
518 };
519
520 /// Revolve `curves` around the z-axis into a fresh part, as operation `r`.
521 fn revolved<S: Scalar>(curves: Vec<NurbCurve2D<S>>) -> Part<S> {
522 let mut part = Part::<S>::new();
523 let namer = Namer::new("revolve", "r").unwrap();
524 revolve(&mut part, &namer, &Profile::open(curves)).unwrap();
525 part.check_names().unwrap();
526 part
527 }
528
529 fn v2<S: Scalar>(x: f64, y: f64) -> Vector2<S> {
530 Vector2::from_array([S::from_f64(x), S::from_f64(y)])
531 }
532
533 fn assert_valid<S: Scalar>(model: &Model<S>) {
534 let params = ValidationParameters::default();
535 if let Err(e) = validate(¶ms, model) {
536 panic!("{e:?}");
537 }
538 if let Err(e) = validate_manifold(¶ms, model) {
539 panic!("{e:?}");
540 }
541 }
542
543 /// An exact sphere from two quarter arcs: curved profile edges become
544 /// doubly curved quadrant patches.
545 fn check_sphere_from_arcs_is_valid<S: Scalar>() {
546 let w = sqrt2_over_2::<S>();
547 let profile = vec![
548 arc2(v2(0.0, 1.0), v2(1.0, 1.0), v2(1.0, 0.0), w).unwrap(),
549 arc2(v2(1.0, 0.0), v2(1.0, -1.0), v2(0.0, -1.0), w).unwrap(),
550 ];
551 let part = revolved(profile);
552 let model = part.topology();
553 assert_valid(model);
554 assert_eq!(model.faces.len(), 8);
555 // Two poles, and the equator's ring of four vertices named after the
556 // joint between the arcs.
557 for name in [
558 "revolve(r,p0)",
559 "revolve(r,p2)",
560 "revolve(r,p1,a0)",
561 "revolve(r,p1,a3)",
562 ] {
563 part.vertex_id(name).unwrap();
564 }
565 for name in ["revolve(r,c0,a0)", "revolve(r,c1,a2)", "revolve(r,p1,q3)"] {
566 part.edge_id(name).unwrap();
567 }
568 part.face_id("revolve(r,c1,q3)").unwrap();
569 }
570 #[test]
571 fn sphere_from_arcs_is_valid() {
572 for_all_scalars!(check_sphere_from_arcs_is_valid);
573 }
574
575 /// A vase: a line up the side and a cubic spline bulging out, capped by
576 /// lines back to the axis.
577 fn check_vase_with_spline_is_valid<S: Scalar>() {
578 let hom = |x: f64, y: f64| {
579 geop_core_math::vector::Vector3::from_array([S::from_f64(x), S::from_f64(y), S::ONE])
580 };
581 let spline = geop_core_geometry::nurb_curve::NurbCurve::try_new(
582 3,
583 vec![hom(0.5, 2.0), hom(1.5, 1.5), hom(0.2, 0.7), hom(1.0, 0.0)],
584 vec![
585 S::ZERO,
586 S::ZERO,
587 S::ZERO,
588 S::ZERO,
589 S::ONE,
590 S::ONE,
591 S::ONE,
592 S::ONE,
593 ],
594 )
595 .unwrap();
596 let mut profile = polyline(&[v2(0.0, 2.0), v2(0.5, 2.0)]).unwrap();
597 profile.push(spline);
598 profile.extend(polyline(&[v2(1.0, 0.0), v2(0.0, 0.0)]).unwrap());
599 let part = revolved(profile);
600 let model = part.topology();
601 assert_valid(model);
602 assert_eq!(model.faces.len(), 12);
603 }
604 #[test]
605 fn vase_with_spline_is_valid() {
606 for_all_scalars!(check_vase_with_spline_is_valid);
607 }
608
609 fn check_cone_is_valid<S: Scalar>() {
610 let profile = polyline(&[v2::<S>(0.0, 1.0), v2(1.0, 0.0), v2(0.0, 0.0)]).unwrap();
611 assert_valid(revolved(profile).topology());
612 }
613 #[test]
614 fn cone_is_valid() {
615 for_all_scalars!(check_cone_is_valid);
616 }
617
618 fn check_sphere_is_valid<S: Scalar>() {
619 let n = 6;
620 let points: Vec<Vector2<S>> = (0..=n)
621 .map(|k| {
622 if k == 0 || k == n {
623 return v2(0.0, if k == 0 { 1.0 } else { -1.0 });
624 }
625 let t = std::f64::consts::PI * (k as f64) / (n as f64);
626 v2(t.sin(), t.cos())
627 })
628 .collect();
629 assert_valid(revolved(polyline(&points).unwrap()).topology());
630 }
631 #[test]
632 fn sphere_is_valid() {
633 for_all_scalars!(check_sphere_is_valid);
634 }
635}