1use crate::topology::{
2 BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
3};
4use crate::{make_line, make_plane, make_revolution, NurbsCurve, NurbsSurface, Vec3, Vec4};
5
6const TOLERANCE: f64 = 1e-5;
7
8fn next_id(counter: &mut u64) -> u64 {
9 let id = *counter;
10 *counter += 1;
11 id
12}
13
14fn parameter_line(u0: f64, v0: f64, u1: f64, v1: f64) -> Result<NurbsCurve, String> {
15 make_line(Vec3::new(u0, v0, 0.0), Vec3::new(u1, v1, 0.0))
16}
17
18fn curve_to_plane_parameters(
19 curve: &NurbsCurve,
20 origin: Vec3,
21 x_axis: Vec3,
22 y_axis: Vec3,
23) -> Result<NurbsCurve, String> {
24 NurbsCurve::new(
25 curve.degree,
26 curve.knots.clone(),
27 curve
28 .control_points
29 .iter()
30 .map(|point| {
31 let euclidean = Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w);
32 let delta = euclidean.sub(origin);
33 Vec4 {
34 x: delta.dot(x_axis) * point.w,
35 y: delta.dot(y_axis) * point.w,
36 z: 0.0,
37 w: point.w,
38 }
39 })
40 .collect(),
41 )
42}
43
44fn rotate_point(point: Vec3, origin: Vec3, axis: Vec3, angle: f64) -> Vec3 {
45 let vector = point.sub(origin);
46 let cosine = angle.cos();
47 let sine = angle.sin();
48 origin
49 .add(vector.scale(cosine))
50 .add(axis.cross(vector).scale(sine))
51 .add(axis.scale(axis.dot(vector) * (1.0 - cosine)))
52}
53
54fn rotate_curve(
55 curve: &NurbsCurve,
56 origin: Vec3,
57 axis: Vec3,
58 angle: f64,
59) -> Result<NurbsCurve, String> {
60 NurbsCurve::new(
61 curve.degree,
62 curve.knots.clone(),
63 curve
64 .control_points
65 .iter()
66 .map(|point| {
67 let euclidean = Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w);
68 Vec4::from_point(rotate_point(euclidean, origin, axis, angle), point.w)
69 })
70 .collect(),
71 )
72}
73
74fn signed_profile_area(
75 curves: &[NurbsCurve],
76 origin: Vec3,
77 radial: Vec3,
78 axis: Vec3,
79) -> Result<f64, String> {
80 let mut area = 0.0;
81 for curve in curves {
82 let [start, end] = curve.domain()?;
83 let mut previous = curve.evaluate(start)?;
84 for index in 1..=64 {
85 let point = curve.evaluate(start + (end - start) * index as f64 / 64.0)?;
86 let a = previous.sub(origin);
87 let b = point.sub(origin);
88 area += 0.5 * (a.dot(radial) * b.dot(axis) - b.dot(radial) * a.dot(axis));
89 previous = point;
90 }
91 }
92 Ok(area)
93}
94
95struct RevolveProfile {
96 curves: Vec<NurbsCurve>,
97 points: Vec<Vec3>,
98 axis_points: Vec<bool>,
99 axis_curves: Vec<bool>,
100 radial_curves: Vec<bool>,
108 radial: Vec3,
109 axis: Vec3,
110 input_indices: Vec<usize>,
116}
117
118fn prepare_profile(
119 input: &[NurbsCurve],
120 axis_point: Vec3,
121 axis_direction: Vec3,
122) -> Result<RevolveProfile, String> {
123 if input.len() < 2 {
124 return Err("profile needs at least 2 curves".into());
125 }
126 let axis = axis_direction.normalized()?;
127 let mut curves: Vec<NurbsCurve> = input
128 .iter()
129 .map(|curve| {
130 NurbsCurve::new(
131 curve.degree,
132 curve.knots.clone(),
133 curve.control_points.clone(),
134 )
135 })
136 .collect::<Result<_, _>>()?;
137 let closure_points = |curves: &[NurbsCurve]| -> Result<Vec<Vec3>, String> {
138 let mut points = Vec::with_capacity(curves.len());
139 for index in 0..curves.len() {
140 let [start, end] = curves[index].domain()?;
141 let next_start = curves[(index + 1) % curves.len()].domain()?[0];
142 let point = curves[index].evaluate(start)?;
143 if curves[index]
144 .evaluate(end)?
145 .sub(curves[(index + 1) % curves.len()].evaluate(next_start)?)
146 .length()
147 > TOLERANCE
148 {
149 return Err(format!("profile not closed at curve {index}"));
150 }
151 points.push(point);
152 }
153 Ok(points)
154 };
155 let mut points = closure_points(&curves)?;
156
157 let radial_distance = |point: Vec3| {
158 let delta = point.sub(axis_point);
159 delta.sub(axis.scale(delta.dot(axis))).length()
160 };
161 let mut radial = None;
162 'outer: for curve in &curves {
163 let [start, end] = curve.domain()?;
164 for index in 0..=24 {
165 let point = curve.evaluate(start + (end - start) * index as f64 / 24.0)?;
166 let delta = point.sub(axis_point);
167 let candidate = delta.sub(axis.scale(delta.dot(axis)));
168 if candidate.length() > TOLERANCE * 100.0 {
169 radial = Some(candidate.normalized()?);
170 break 'outer;
171 }
172 }
173 }
174 let radial = radial.ok_or_else(|| "revolveSolid: profile touches the axis".to_string())?;
175 let curve_on_axis = |curve: &NurbsCurve| -> Result<bool, String> {
176 let [start, end] = curve.domain()?;
177 for index in 0..=8 {
178 if radial_distance(curve.evaluate(start + (end - start) * index as f64 / 8.0)?)
179 > TOLERANCE * 100.0
180 {
181 return Ok(false);
182 }
183 }
184 Ok(true)
185 };
186 for curve in &curves {
187 let [start, end] = curve.domain()?;
188 let on_axis = curve_on_axis(curve)?;
189 for index in 0..=24 {
190 let point = curve.evaluate(start + (end - start) * index as f64 / 24.0)?;
191 let delta = point.sub(axis_point);
192 let in_plane_radial = delta.dot(radial);
193 let off_plane = delta
194 .sub(axis.scale(delta.dot(axis)))
195 .sub(radial.scale(in_plane_radial))
196 .length();
197 if off_plane > TOLERANCE * 100.0 {
198 return Err("revolveSolid: profile not in a half-plane containing the axis".into());
199 }
200 if in_plane_radial < -TOLERANCE * 100.0 {
201 return Err("revolveSolid: profile crosses the revolve axis".into());
202 }
203 if in_plane_radial < TOLERANCE * 100.0 && index != 0 && index != 24 && !on_axis {
204 return Err(
205 "revolveSolid: profile may only touch the axis at boundary vertices or axis edges"
206 .into(),
207 );
208 }
209 }
210 }
211 let mut input_indices: Vec<usize> = (0..curves.len()).collect();
212 if signed_profile_area(&curves, axis_point, radial, axis)? < 0.0 {
213 curves = curves
214 .iter()
215 .rev()
216 .map(NurbsCurve::reversed)
217 .collect::<Result<_, _>>()?;
218 points = closure_points(&curves)?;
219 input_indices.reverse();
220 }
221 let mut axis_points: Vec<bool> = points
222 .iter()
223 .map(|point| radial_distance(*point) <= TOLERANCE * 100.0)
224 .collect();
225 if axis_points.iter().any(|flag| *flag) {
226 let snapped: Vec<Vec3> = points
227 .iter()
228 .enumerate()
229 .map(|(index, point)| {
230 if axis_points[index] {
231 axis_point.add(axis.scale(point.sub(axis_point).dot(axis)))
232 } else {
233 *point
234 }
235 })
236 .collect();
237 for index in 0..curves.len() {
238 let next = (index + 1) % curves.len();
239 if !axis_points[index] && !axis_points[next] {
240 continue;
241 }
242 let mut control_points = curves[index].control_points.clone();
243 if axis_points[index] {
244 let weight = control_points[0].w;
245 control_points[0] = Vec4::from_point(snapped[index], weight);
246 }
247 if axis_points[next] {
248 let last = control_points.len() - 1;
249 let weight = control_points[last].w;
250 control_points[last] = Vec4::from_point(snapped[next], weight);
251 }
252 curves[index] = NurbsCurve::new(
253 curves[index].degree,
254 curves[index].knots.clone(),
255 control_points,
256 )?;
257 }
258 points = closure_points(&curves)?;
259 axis_points = points
260 .iter()
261 .map(|point| radial_distance(*point) <= TOLERANCE * 100.0)
262 .collect();
263 }
264 let axis_curves = curves
265 .iter()
266 .map(curve_on_axis)
267 .collect::<Result<Vec<_>, _>>()?;
268 let radial_curves = curves
273 .iter()
274 .enumerate()
275 .map(|(index, curve)| {
276 if axis_curves[index] {
277 return Ok(false);
278 }
279 Ok(is_straight_radial(curve, axis)?)
280 })
281 .collect::<Result<Vec<_>, String>>()?;
282 Ok(RevolveProfile {
283 curves,
284 points,
285 axis_points,
286 axis_curves,
287 radial_curves,
288 radial,
289 axis,
290 input_indices,
291 })
292}
293
294fn is_straight_radial(curve: &NurbsCurve, axis: Vec3) -> Result<bool, String> {
299 if curve.degree != 1 || curve.control_points.len() != 2 {
300 return Ok(false);
301 }
302 let [start, end] = curve.domain()?;
303 let p0 = curve.evaluate(start)?;
304 let p1 = curve.evaluate(end)?;
305 let direction = p1.sub(p0);
306 let length = direction.length();
307 if length <= TOLERANCE {
308 return Ok(false);
309 }
310 Ok(direction.dot(axis).abs() <= 1e-3 * length)
311}
312
313fn junction_curve(
314 index: usize,
315 surfaces: &[Option<NurbsSurface>],
316 points: &[Vec3],
317) -> Result<NurbsCurve, String> {
318 if let Some(surface) = &surfaces[index] {
319 return surface.iso_curve_v(surface.knots_v[surface.degree_v]);
320 }
321 let previous = (index + surfaces.len() - 1) % surfaces.len();
322 if let Some(surface) = &surfaces[previous] {
323 return surface.iso_curve_v(surface.knots_v[surface.knots_v.len() - 1 - surface.degree_v]);
324 }
325 make_line(points[index], points[index])
326}
327
328#[allow(clippy::too_many_arguments)]
336fn radial_plane_face(
337 profile: &RevolveProfile,
338 index: usize,
339 count: usize,
340 origin: Vec3,
341 revolution: &NurbsSurface,
342 arc_curves: &[NurbsCurve],
343 end_curves: &[NurbsCurve],
344 arc_ids: &[u64],
345 start_edge_ids: &[u64],
346 end_edge_ids: &[u64],
347 counter: &mut u64,
348 name: Option<String>,
349) -> Result<FaceRecord, String> {
350 let axis = profile.axis;
351 let a_start = profile.points[index].sub(origin).dot(axis);
352 let a_end = profile.points[(index + 1) % count].sub(origin).dot(axis);
353 let plane_center = origin.add(axis.scale(0.5 * (a_start + a_end)));
354 let e1 = profile.radial;
355 let e2 = axis.cross(e1);
356
357 let next = (index + 1) % count;
358 let boundary: [(NurbsCurve, u64, bool); 4] = [
359 (arc_curves[index].clone(), arc_ids[index], true),
360 (end_curves[index].clone(), end_edge_ids[index], true),
361 (arc_curves[next].clone(), arc_ids[next], false),
362 (profile.curves[index].clone(), start_edge_ids[index], false),
363 ];
364
365 let mut min1 = f64::INFINITY;
366 let mut max1 = f64::NEG_INFINITY;
367 let mut min2 = f64::INFINITY;
368 let mut max2 = f64::NEG_INFINITY;
369 for (curve, _, _) in &boundary {
370 let [s, e] = curve.domain()?;
371 for k in 0..=16 {
372 let point = curve.evaluate(s + (e - s) * k as f64 / 16.0)?;
373 let d = point.sub(plane_center);
374 let c1 = d.dot(e1);
375 let c2 = d.dot(e2);
376 min1 = min1.min(c1);
377 max1 = max1.max(c1);
378 min2 = min2.min(c2);
379 max2 = max2.max(c2);
380 }
381 }
382 let padding = (max1 - min1).max(max2 - min2) * 0.05 + 1e-6;
383 let width = max1 - min1 + 2.0 * padding;
384 let height = max2 - min2 + 2.0 * padding;
385 let corner = plane_center
386 .add(e1.scale(min1 - padding))
387 .add(e2.scale(min2 - padding));
388 let plane = make_plane(corner, e1, e2, width, height)?;
389
390 let [ru0, ru1] = revolution.domain_u()?;
394 let [rv0, rv1] = revolution.domain_v()?;
395 let rev_normal = revolution.normal((ru0 + ru1) * 0.5, (rv0 + rv1) * 0.5)?;
396 let same_sense = rev_normal.dot(e1.cross(e2)) >= 0.0;
397
398 let mut coedges = Vec::with_capacity(4);
399 for (curve, edge_id, forward) in boundary {
400 let mapped = curve_to_plane_parameters(&curve, corner, e1, e2)?;
401 let pcurve = if forward { mapped } else { mapped.reversed()? };
402 coedges.push(CoedgeRecord {
403 id: next_id(counter),
404 edge_id,
405 forward,
406 pcurve,
407 });
408 }
409 let loop_id = next_id(counter);
410 Ok(FaceRecord {
411 id: next_id(counter),
412 surface: plane,
413 same_sense,
414 loops: vec![LoopRecord {
415 id: loop_id,
416 coedges,
417 }],
418 name,
419 })
420}
421
422pub fn revolve_profile_brep(
423 input: &[NurbsCurve],
424 axis_point: Vec3,
425 axis_direction: Vec3,
426 angle: f64,
427) -> Result<BrepSolid, String> {
428 revolve_profile_brep_named(input, axis_point, axis_direction, angle, &[], &[])
429}
430
431pub fn revolve_profile_brep_named(
437 input: &[NurbsCurve],
438 axis_point: Vec3,
439 axis_direction: Vec3,
440 angle: f64,
441 side_names: &[Option<String>],
442 cap_names: &[Option<String>],
443) -> Result<BrepSolid, String> {
444 if angle <= 1e-9 || angle > std::f64::consts::TAU + 1e-9 {
445 return Err("revolveSolid: angle must be in (0, 2*PI]".into());
446 }
447 if !side_names.is_empty() && side_names.len() != input.len() {
448 return Err(format!(
449 "revolveSolid: {} side names for {} profile curves",
450 side_names.len(),
451 input.len()
452 ));
453 }
454 let full_turn = angle > std::f64::consts::TAU - 1e-9;
455 let profile = prepare_profile(input, axis_point, axis_direction)?;
456 if full_turn {
457 revolve_full(profile, axis_point, side_names)
458 } else {
459 revolve_partial(profile, axis_point, angle, side_names, cap_names)
460 }
461}
462
463fn side_name(
464 profile: &RevolveProfile,
465 curve_index: usize,
466 side_names: &[Option<String>],
467) -> Option<String> {
468 profile
469 .input_indices
470 .get(curve_index)
471 .and_then(|input_index| side_names.get(*input_index))
472 .cloned()
473 .flatten()
474}
475
476fn revolve_full(
477 profile: RevolveProfile,
478 origin: Vec3,
479 side_names: &[Option<String>],
480) -> Result<BrepSolid, String> {
481 let count = profile.curves.len();
482 let surfaces: Vec<Option<NurbsSurface>> = profile
483 .curves
484 .iter()
485 .enumerate()
486 .map(|(index, curve)| {
487 if profile.axis_curves[index] {
488 Ok(None)
489 } else {
490 make_revolution(origin, profile.axis, curve, std::f64::consts::TAU).map(Some)
491 }
492 })
493 .collect::<Result<_, String>>()?;
494 let vertices: Vec<VertexRecord> = profile
495 .points
496 .iter()
497 .enumerate()
498 .map(|(index, point)| VertexRecord {
499 id: index as u64 + 1,
500 point: *point,
501 })
502 .collect();
503 let mut counter = 100_u64;
504 let mut edges = Vec::new();
505 let mut circle_ids = Vec::with_capacity(count);
506 for index in 0..count {
507 let curve = junction_curve(index, &surfaces, &profile.points)?;
508 let [start, end] = curve.domain()?;
509 let id = next_id(&mut counter);
510 circle_ids.push(id);
511 edges.push(EdgeRecord {
512 id,
513 curve,
514 t0: start,
515 t1: end,
516 start_vertex_id: index as u64 + 1,
517 end_vertex_id: index as u64 + 1,
518 degenerate: profile.axis_points[index],
519 name: None,
520 });
521 }
522 let mut faces = Vec::new();
523 for index in 0..count {
524 if profile.axis_curves[index] {
525 continue;
526 }
527 let curve = profile.curves[index].clone();
528 let [start, end] = curve.domain()?;
529 let seam_id = next_id(&mut counter);
530 edges.push(EdgeRecord {
531 id: seam_id,
532 curve,
533 t0: start,
534 t1: end,
535 start_vertex_id: index as u64 + 1,
536 end_vertex_id: ((index + 1) % count) as u64 + 1,
537 degenerate: false,
538 name: None,
539 });
540 let coedges = vec![
541 CoedgeRecord {
542 id: next_id(&mut counter),
543 edge_id: circle_ids[index],
544 forward: true,
545 pcurve: parameter_line(0.0, start, 1.0, start)?,
546 },
547 CoedgeRecord {
548 id: next_id(&mut counter),
549 edge_id: seam_id,
550 forward: true,
551 pcurve: parameter_line(1.0, start, 1.0, end)?,
552 },
553 CoedgeRecord {
554 id: next_id(&mut counter),
555 edge_id: circle_ids[(index + 1) % count],
556 forward: false,
557 pcurve: parameter_line(1.0, end, 0.0, end)?,
558 },
559 CoedgeRecord {
560 id: next_id(&mut counter),
561 edge_id: seam_id,
562 forward: false,
563 pcurve: parameter_line(0.0, end, 0.0, start)?,
564 },
565 ];
566 let loop_id = next_id(&mut counter);
567 faces.push(FaceRecord {
568 id: next_id(&mut counter),
569 surface: surfaces[index].clone().unwrap(),
570 same_sense: true,
571 loops: vec![LoopRecord {
572 id: loop_id,
573 coedges,
574 }],
575 name: side_name(&profile, index, side_names),
576 });
577 }
578 let solid = BrepSolid {
579 id: next_id(&mut counter),
580 vertices,
581 edges,
582 shells: vec![ShellRecord {
583 id: next_id(&mut counter),
584 faces,
585 }],
586 genus: if profile.axis_points.iter().any(|flag| *flag) {
587 0
588 } else {
589 1
590 },
591 };
592 let issues = solid.validate();
593 if issues.is_empty() {
594 Ok(solid)
595 } else {
596 Err(format!("Rust full revolution is invalid: {issues:?}"))
597 }
598}
599
600fn revolve_partial(
601 profile: RevolveProfile,
602 origin: Vec3,
603 angle: f64,
604 side_names: &[Option<String>],
605 cap_names: &[Option<String>],
606) -> Result<BrepSolid, String> {
607 let count = profile.curves.len();
608 let end_curves: Vec<NurbsCurve> = profile
609 .curves
610 .iter()
611 .map(|curve| rotate_curve(curve, origin, profile.axis, angle))
612 .collect::<Result<_, _>>()?;
613 let surfaces: Vec<Option<NurbsSurface>> = profile
614 .curves
615 .iter()
616 .enumerate()
617 .map(|(index, curve)| {
618 if profile.axis_curves[index] {
619 Ok(None)
620 } else {
621 make_revolution(origin, profile.axis, curve, angle).map(Some)
622 }
623 })
624 .collect::<Result<_, String>>()?;
625 let mut vertices = Vec::new();
626 let mut start_vertex_ids = Vec::with_capacity(count);
627 let mut end_vertex_ids = Vec::with_capacity(count);
628 let mut counter = 100_u64;
629 for point in &profile.points {
630 let id = next_id(&mut counter);
631 start_vertex_ids.push(id);
632 vertices.push(VertexRecord { id, point: *point });
633 }
634 for index in 0..count {
635 if profile.axis_points[index] {
636 end_vertex_ids.push(start_vertex_ids[index]);
637 } else {
638 let id = next_id(&mut counter);
639 end_vertex_ids.push(id);
640 vertices.push(VertexRecord {
641 id,
642 point: rotate_point(profile.points[index], origin, profile.axis, angle),
643 });
644 }
645 }
646
647 let mut edges = Vec::new();
648 let mut arc_ids = Vec::with_capacity(count);
649 let mut arc_curves = Vec::with_capacity(count);
650 for index in 0..count {
651 let curve = junction_curve(index, &surfaces, &profile.points)?;
652 let [start, end] = curve.domain()?;
653 let id = next_id(&mut counter);
654 arc_ids.push(id);
655 arc_curves.push(curve.clone());
656 edges.push(EdgeRecord {
657 id,
658 curve,
659 t0: start,
660 t1: end,
661 start_vertex_id: start_vertex_ids[index],
662 end_vertex_id: end_vertex_ids[index],
663 degenerate: profile.axis_points[index],
664 name: None,
665 });
666 }
667 let mut start_edge_ids = Vec::with_capacity(count);
668 let mut end_edge_ids = Vec::with_capacity(count);
669 for index in 0..count {
670 let [start, end] = profile.curves[index].domain()?;
671 let start_id = next_id(&mut counter);
672 start_edge_ids.push(start_id);
673 edges.push(EdgeRecord {
674 id: start_id,
675 curve: profile.curves[index].clone(),
676 t0: start,
677 t1: end,
678 start_vertex_id: start_vertex_ids[index],
679 end_vertex_id: start_vertex_ids[(index + 1) % count],
680 degenerate: false,
681 name: None,
682 });
683 if profile.axis_curves[index] {
684 end_edge_ids.push(start_id);
685 } else {
686 let end_id = next_id(&mut counter);
687 end_edge_ids.push(end_id);
688 edges.push(EdgeRecord {
689 id: end_id,
690 curve: end_curves[index].clone(),
691 t0: start,
692 t1: end,
693 start_vertex_id: end_vertex_ids[index],
694 end_vertex_id: end_vertex_ids[(index + 1) % count],
695 degenerate: false,
696 name: None,
697 });
698 }
699 }
700
701 let mut faces = Vec::new();
702 for index in 0..count {
703 if profile.axis_curves[index] {
704 continue;
705 }
706 if profile.radial_curves[index] {
707 faces.push(radial_plane_face(
708 &profile,
709 index,
710 count,
711 origin,
712 surfaces[index].as_ref().unwrap(),
713 &arc_curves,
714 &end_curves,
715 &arc_ids,
716 &start_edge_ids,
717 &end_edge_ids,
718 &mut counter,
719 side_name(&profile, index, side_names),
720 )?);
721 continue;
722 }
723 let [start, end] = profile.curves[index].domain()?;
724 let coedges = vec![
725 CoedgeRecord {
726 id: next_id(&mut counter),
727 edge_id: arc_ids[index],
728 forward: true,
729 pcurve: parameter_line(0.0, start, 1.0, start)?,
730 },
731 CoedgeRecord {
732 id: next_id(&mut counter),
733 edge_id: end_edge_ids[index],
734 forward: true,
735 pcurve: parameter_line(1.0, start, 1.0, end)?,
736 },
737 CoedgeRecord {
738 id: next_id(&mut counter),
739 edge_id: arc_ids[(index + 1) % count],
740 forward: false,
741 pcurve: parameter_line(1.0, end, 0.0, end)?,
742 },
743 CoedgeRecord {
744 id: next_id(&mut counter),
745 edge_id: start_edge_ids[index],
746 forward: false,
747 pcurve: parameter_line(0.0, end, 0.0, start)?,
748 },
749 ];
750 let loop_id = next_id(&mut counter);
751 faces.push(FaceRecord {
752 id: next_id(&mut counter),
753 surface: surfaces[index].clone().unwrap(),
754 same_sense: true,
755 loops: vec![LoopRecord {
756 id: loop_id,
757 coedges,
758 }],
759 name: side_name(&profile, index, side_names),
760 });
761 }
762
763 let mut min_radial = f64::INFINITY;
764 let mut max_radial = f64::NEG_INFINITY;
765 let mut min_axis = f64::INFINITY;
766 let mut max_axis = f64::NEG_INFINITY;
767 for curve in &profile.curves {
768 let [start, end] = curve.domain()?;
769 for index in 0..=24 {
770 let delta = curve
771 .evaluate(start + (end - start) * index as f64 / 24.0)?
772 .sub(origin);
773 let radial = delta.dot(profile.radial);
774 let axial = delta.dot(profile.axis);
775 min_radial = min_radial.min(radial);
776 max_radial = max_radial.max(radial);
777 min_axis = min_axis.min(axial);
778 max_axis = max_axis.max(axial);
779 }
780 }
781 let padding = (max_radial - min_radial).max(max_axis - min_axis) * 0.05 + 1e-6;
782 let width = max_radial - min_radial + 2.0 * padding;
783 let height = max_axis - min_axis + 2.0 * padding;
784 let start_origin = origin
785 .add(profile.radial.scale(min_radial - padding))
786 .add(profile.axis.scale(min_axis - padding));
787 let mut start_coedges = Vec::with_capacity(count);
788 for index in 0..count {
789 start_coedges.push(CoedgeRecord {
790 id: next_id(&mut counter),
791 edge_id: start_edge_ids[index],
792 forward: true,
793 pcurve: curve_to_plane_parameters(
794 &profile.curves[index],
795 start_origin,
796 profile.radial,
797 profile.axis,
798 )?,
799 });
800 }
801 let start_loop_id = next_id(&mut counter);
802 faces.push(FaceRecord {
803 id: next_id(&mut counter),
804 surface: make_plane(start_origin, profile.radial, profile.axis, width, height)?,
805 same_sense: true,
806 loops: vec![LoopRecord {
807 id: start_loop_id,
808 coedges: start_coedges,
809 }],
810 name: cap_names.first().cloned().flatten(),
811 });
812
813 let end_radial = rotate_point(origin.add(profile.radial), origin, profile.axis, angle)
814 .sub(origin)
815 .normalized()?;
816 let end_origin = origin
817 .add(end_radial.scale(min_radial - padding))
818 .add(profile.axis.scale(min_axis - padding));
819 let mut end_coedges = Vec::with_capacity(count);
820 for index in (0..count).rev() {
821 end_coedges.push(CoedgeRecord {
822 id: next_id(&mut counter),
823 edge_id: end_edge_ids[index],
824 forward: false,
825 pcurve: curve_to_plane_parameters(
826 &end_curves[index],
827 end_origin,
828 profile.axis,
829 end_radial,
830 )?
831 .reversed()?,
832 });
833 }
834 let end_loop_id = next_id(&mut counter);
835 faces.push(FaceRecord {
836 id: next_id(&mut counter),
837 surface: make_plane(end_origin, profile.axis, end_radial, height, width)?,
838 same_sense: true,
839 loops: vec![LoopRecord {
840 id: end_loop_id,
841 coedges: end_coedges,
842 }],
843 name: cap_names.get(1).cloned().flatten(),
844 });
845
846 let solid = BrepSolid {
847 id: next_id(&mut counter),
848 vertices,
849 edges,
850 shells: vec![ShellRecord {
851 id: next_id(&mut counter),
852 faces,
853 }],
854 genus: 0,
855 };
856 let issues = solid.validate();
857 if issues.is_empty() {
858 Ok(solid)
859 } else {
860 Err(format!("Rust partial revolution is invalid: {issues:?}"))
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867
868 fn radial_rectangle(inner_radius: f64) -> Vec<NurbsCurve> {
869 let points = [
870 Vec3::new(inner_radius, 0.0, 0.0),
871 Vec3::new(4.0, 0.0, 0.0),
872 Vec3::new(4.0, 3.0, 0.0),
873 Vec3::new(inner_radius, 3.0, 0.0),
874 ];
875 (0..4)
876 .map(|index| make_line(points[index], points[(index + 1) % 4]).unwrap())
877 .collect()
878 }
879
880 #[test]
881 fn full_revolution_handles_axis_edge_and_hollow_profile() {
882 let axis = Vec3::new(0.0, 1.0, 0.0);
883 let solid = revolve_profile_brep(
884 &radial_rectangle(0.0),
885 Vec3::default(),
886 axis,
887 std::f64::consts::TAU,
888 )
889 .unwrap();
890 assert!(solid.validate().is_empty());
891 assert_eq!(solid.genus, 0);
892 let hollow = revolve_profile_brep(
893 &radial_rectangle(2.0),
894 Vec3::default(),
895 axis,
896 std::f64::consts::TAU,
897 )
898 .unwrap();
899 assert!(hollow.validate().is_empty());
900 assert_eq!(hollow.genus, 1);
901 }
902
903 #[test]
904 fn partial_revolution_has_radial_caps() {
905 let solid = revolve_profile_brep(
906 &radial_rectangle(0.0),
907 Vec3::default(),
908 Vec3::new(0.0, 1.0, 0.0),
909 std::f64::consts::PI * 1.25,
910 )
911 .unwrap();
912 assert!(solid.validate().is_empty());
913 assert_eq!(solid.genus, 0);
914 }
915
916 #[test]
922 fn named_revolve_keeps_names_on_their_input_curves() {
923 let points = [
927 Vec3::new(0.0, 0.0, 0.0),
928 Vec3::new(0.0, 3.0, 0.0),
929 Vec3::new(4.0, 3.0, 0.0),
930 Vec3::new(4.0, 0.0, 0.0),
931 ];
932 let curves: Vec<NurbsCurve> = (0..4)
933 .map(|index| make_line(points[index], points[(index + 1) % 4]).unwrap())
934 .collect();
935 let side_names: Vec<Option<String>> = ["AXIS", "TOP", "SIDE", "BOTTOM"]
936 .into_iter()
937 .map(|name| Some(name.to_string()))
938 .collect();
939 let cap_names = vec![Some("CAP_START".to_string()), Some("CAP_END".to_string())];
940 let solid = revolve_profile_brep_named(
941 &curves,
942 Vec3::default(),
943 Vec3::new(0.0, 1.0, 0.0),
944 std::f64::consts::PI * 1.25,
945 &side_names,
946 &cap_names,
947 )
948 .unwrap();
949 assert!(solid.validate().is_empty());
950 let faces: Vec<_> = solid.shells[0].faces.iter().collect();
951 assert!(faces
953 .iter()
954 .all(|face| face.name.as_deref() != Some("AXIS")));
955 for (name, probe) in [
956 ("TOP", Vec3::new(2.0, 3.0, 0.0)),
958 ("SIDE", Vec3::new(4.0, 1.5, 0.0)),
959 ("BOTTOM", Vec3::new(2.0, 0.0, 0.0)),
960 ] {
961 let face = faces
962 .iter()
963 .find(|face| face.name.as_deref() == Some(name))
964 .unwrap_or_else(|| panic!("face {name} missing"));
965 let projection = crate::project_point_to_surface(&face.surface, probe).unwrap();
966 assert!(
967 projection.distance < 1e-9,
968 "{name}: named face does not contain its input curve \
969 (distance {})",
970 projection.distance
971 );
972 }
973 assert!(faces
974 .iter()
975 .any(|face| face.name.as_deref() == Some("CAP_START")));
976 assert!(faces
977 .iter()
978 .any(|face| face.name.as_deref() == Some("CAP_END")));
979 }
980}