1use crate::sweep_topology::{curve_to_plane_parameters, parameter_line};
2use crate::topology::{
3 BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
4};
5use crate::{make_line, make_plane, make_revolution, NurbsCurve, NurbsSurface, Vec3, Vec4};
6
7const TOLERANCE: f64 = 1e-5;
8
9fn next_id(counter: &mut u64) -> u64 {
10 let id = *counter;
11 *counter += 1;
12 id
13}
14
15fn rotate_point(point: Vec3, origin: Vec3, axis: Vec3, angle: f64) -> Vec3 {
16 let vector = point.sub(origin);
17 let cosine = angle.cos();
18 let sine = angle.sin();
19 origin
20 .add(vector.scale(cosine))
21 .add(axis.cross(vector).scale(sine))
22 .add(axis.scale(axis.dot(vector) * (1.0 - cosine)))
23}
24
25fn rotate_curve(
26 curve: &NurbsCurve,
27 origin: Vec3,
28 axis: Vec3,
29 angle: f64,
30) -> Result<NurbsCurve, String> {
31 NurbsCurve::new(
32 curve.degree,
33 curve.knots.clone(),
34 curve
35 .control_points
36 .iter()
37 .map(|point| {
38 let euclidean = Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w);
39 Vec4::from_point(rotate_point(euclidean, origin, axis, angle), point.w)
40 })
41 .collect(),
42 )
43}
44
45fn signed_profile_area(
46 curves: &[NurbsCurve],
47 origin: Vec3,
48 radial: Vec3,
49 axis: Vec3,
50) -> Result<f64, String> {
51 let mut area = 0.0;
52 for curve in curves {
53 let [start, end] = curve.domain()?;
54 let mut previous = curve.evaluate(start)?;
55 for index in 1..=64 {
56 let point = curve.evaluate(start + (end - start) * index as f64 / 64.0)?;
57 let a = previous.sub(origin);
58 let b = point.sub(origin);
59 area += 0.5 * (a.dot(radial) * b.dot(axis) - b.dot(radial) * a.dot(axis));
60 previous = point;
61 }
62 }
63 Ok(area)
64}
65
66struct RevolveProfile {
67 curves: Vec<NurbsCurve>,
68 points: Vec<Vec3>,
69 axis_points: Vec<bool>,
70 exact_axis_points: Vec<bool>,
75 axis_curves: Vec<bool>,
76 radial_curves: Vec<bool>,
84 radial: Vec3,
85 axis: Vec3,
86 input_indices: Vec<usize>,
92}
93
94fn is_segment(curve: &NurbsCurve) -> bool {
96 curve.degree == 1 && curve.control_points.len() == 2
97}
98
99fn clamped_at(curve: &NurbsCurve, at_start: bool) -> bool {
102 let order = curve.degree + 1;
103 let knots = &curve.knots;
104 if at_start {
105 knots[..order].iter().all(|knot| *knot == knots[0])
106 } else {
107 knots[knots.len() - order..].iter().all(|knot| *knot == knots[knots.len() - 1])
108 }
109}
110
111fn end_control_point(curve: &NurbsCurve, at_start: bool) -> Vec3 {
112 let point = if at_start {
113 curve.control_points[0]
114 } else {
115 curve.control_points[curve.control_points.len() - 1]
116 };
117 Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w)
118}
119
120fn move_end(curve: &mut NurbsCurve, at_start: bool, target: Vec3) -> Result<(), String> {
122 let mut control_points = curve.control_points.clone();
123 let index = if at_start { 0 } else { control_points.len() - 1 };
124 control_points[index] = Vec4::from_point(target, control_points[index].w);
125 *curve = NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)?;
126 Ok(())
127}
128
129fn weld_junctions(curves: &mut [NurbsCurve]) -> Result<bool, String> {
135 let count = curves.len();
136 let mut moved = false;
137 for index in 0..count {
138 let next = (index + 1) % count;
139 if !clamped_at(&curves[index], false) || !clamped_at(&curves[next], true) {
140 continue;
141 }
142 let end = end_control_point(&curves[index], false);
143 let start = end_control_point(&curves[next], true);
144 if end.x == start.x && end.y == start.y && end.z == start.z {
145 continue;
146 }
147 if is_segment(&curves[index]) && !is_segment(&curves[next]) {
148 move_end(&mut curves[index], false, start)?;
149 } else {
150 move_end(&mut curves[next], true, end)?;
151 }
152 moved = true;
153 }
154 Ok(moved)
155}
156
157fn flatten_radial_generatrices(
168 curves: &mut [NurbsCurve],
169 radial: &[bool],
170 axis_point: Vec3,
171 axis: Vec3,
172) -> Result<bool, String> {
173 let count = curves.len();
174 let Some(first) = (0..count).find(|index| !radial[*index]) else {
175 return Ok(false);
176 };
177 let axial = |point: Vec3| point.sub(axis_point).dot(axis);
178 let mut moved = false;
179 let mut offset = 1;
180 while offset <= count {
181 let begin = (first + offset) % count;
182 if !radial[begin] {
183 offset += 1;
184 continue;
185 }
186 let mut run = vec![begin];
187 while radial[(run[run.len() - 1] + 1) % count] {
188 run.push((run[run.len() - 1] + 1) % count);
189 }
190 offset += run.len();
191 let last = run[run.len() - 1];
193 if run.iter().any(|index| !clamped_at(&curves[*index], true) || !clamped_at(&curves[*index], false)) {
194 continue;
195 }
196 let mut vertices: Vec<Vec3> = run.iter().map(|index| end_control_point(&curves[*index], true)).collect();
197 vertices.push(end_control_point(&curves[last], false));
198 let before = (begin + count - 1) % count;
199 let after = (last + 1) % count;
200 let mut fixed = Vec::new();
201 if !is_segment(&curves[before]) {
202 fixed.push(axial(vertices[0]));
203 }
204 if !is_segment(&curves[after]) {
205 fixed.push(axial(vertices[vertices.len() - 1]));
206 }
207 let pool: Vec<f64> = if fixed.is_empty() { vertices.iter().map(|point| axial(*point)).collect() } else { fixed };
208 let target = pool.iter().sum::<f64>() / pool.len() as f64;
209 for (k, vertex) in vertices.iter().enumerate() {
210 let shift = target - axial(*vertex);
211 if shift == 0.0 {
212 continue;
213 }
214 let point = vertex.add(axis.scale(shift));
215 let (incoming, outgoing) = if k == 0 {
216 (before, run[0])
217 } else if k == run.len() {
218 (last, after)
219 } else {
220 (run[k - 1], run[k])
221 };
222 if clamped_at(&curves[incoming], false) {
223 move_end(&mut curves[incoming], false, point)?;
224 }
225 if clamped_at(&curves[outgoing], true) {
226 move_end(&mut curves[outgoing], true, point)?;
227 }
228 moved = true;
229 }
230 }
231 Ok(moved)
232}
233
234fn prepare_profile(
235 input: &[NurbsCurve],
236 axis_point: Vec3,
237 axis_direction: Vec3,
238 flatten_radial: bool,
239) -> Result<RevolveProfile, String> {
240 if input.len() < 2 {
241 return Err("profile needs at least 2 curves".into());
242 }
243 let axis = axis_direction.normalized()?;
244 let mut curves: Vec<NurbsCurve> = input
245 .iter()
246 .map(|curve| {
247 NurbsCurve::new(
248 curve.degree,
249 curve.knots.clone(),
250 curve.control_points.clone(),
251 )
252 })
253 .collect::<Result<_, _>>()?;
254 let closure_points = |curves: &[NurbsCurve]| -> Result<Vec<Vec3>, String> {
255 let mut points = Vec::with_capacity(curves.len());
256 for index in 0..curves.len() {
257 let [start, end] = curves[index].domain()?;
258 let next_start = curves[(index + 1) % curves.len()].domain()?[0];
259 let point = curves[index].evaluate(start)?;
260 if curves[index]
261 .evaluate(end)?
262 .sub(curves[(index + 1) % curves.len()].evaluate(next_start)?)
263 .length()
264 > TOLERANCE
265 {
266 return Err(format!("profile not closed at curve {index}"));
267 }
268 points.push(point);
269 }
270 Ok(points)
271 };
272 let mut points = closure_points(&curves)?;
273
274 let radial_distance = |point: Vec3| {
275 let delta = point.sub(axis_point);
276 delta.sub(axis.scale(delta.dot(axis))).length()
277 };
278 let mut radial = None;
279 'outer: for curve in &curves {
280 let [start, end] = curve.domain()?;
281 for index in 0..=24 {
282 let point = curve.evaluate(start + (end - start) * index as f64 / 24.0)?;
283 let delta = point.sub(axis_point);
284 let candidate = delta.sub(axis.scale(delta.dot(axis)));
285 if candidate.length() > TOLERANCE * 100.0 {
286 radial = Some(candidate.normalized()?);
287 break 'outer;
288 }
289 }
290 }
291 let radial = radial.ok_or_else(|| "revolveSolid: profile touches the axis".to_string())?;
292 let curve_on_axis = |curve: &NurbsCurve| -> Result<bool, String> {
293 let [start, end] = curve.domain()?;
294 for index in 0..=8 {
295 if radial_distance(curve.evaluate(start + (end - start) * index as f64 / 8.0)?)
296 > TOLERANCE * 100.0
297 {
298 return Ok(false);
299 }
300 }
301 Ok(true)
302 };
303 for curve in &curves {
304 let [start, end] = curve.domain()?;
305 let on_axis = curve_on_axis(curve)?;
306 for index in 0..=24 {
307 let point = curve.evaluate(start + (end - start) * index as f64 / 24.0)?;
308 let delta = point.sub(axis_point);
309 let in_plane_radial = delta.dot(radial);
310 let off_plane = delta
311 .sub(axis.scale(delta.dot(axis)))
312 .sub(radial.scale(in_plane_radial))
313 .length();
314 if off_plane > TOLERANCE * 100.0 {
315 return Err("revolveSolid: profile not in a half-plane containing the axis".into());
316 }
317 if in_plane_radial < -TOLERANCE * 100.0 {
318 return Err("revolveSolid: profile crosses the revolve axis".into());
319 }
320 if in_plane_radial < TOLERANCE * 100.0 && index != 0 && index != 24 && !on_axis {
321 return Err(
322 "revolveSolid: profile may only touch the axis at boundary vertices or axis edges"
323 .into(),
324 );
325 }
326 }
327 }
328 let mut input_indices: Vec<usize> = (0..curves.len()).collect();
329 if signed_profile_area(&curves, axis_point, radial, axis)? < 0.0 {
330 curves = curves
331 .iter()
332 .rev()
333 .map(NurbsCurve::reversed)
334 .collect::<Result<_, _>>()?;
335 points = closure_points(&curves)?;
336 input_indices.reverse();
337 }
338 let mut axis_points: Vec<bool> = points
339 .iter()
340 .map(|point| radial_distance(*point) <= TOLERANCE * 100.0)
341 .collect();
342 let profile_size = points
343 .iter()
344 .fold(1.0f64, |size, point| size.max(point.sub(axis_point).length()));
345 let exact_axis_points: Vec<bool> = points
346 .iter()
347 .map(|point| radial_distance(*point) <= 1e-12 * profile_size)
348 .collect();
349 if axis_points.iter().any(|flag| *flag) {
350 let snapped: Vec<Vec3> = points
351 .iter()
352 .enumerate()
353 .map(|(index, point)| {
354 if axis_points[index] {
355 axis_point.add(axis.scale(point.sub(axis_point).dot(axis)))
356 } else {
357 *point
358 }
359 })
360 .collect();
361 for index in 0..curves.len() {
362 let next = (index + 1) % curves.len();
363 if !axis_points[index] && !axis_points[next] {
364 continue;
365 }
366 let mut control_points = curves[index].control_points.clone();
367 if axis_points[index] {
368 let weight = control_points[0].w;
369 control_points[0] = Vec4::from_point(snapped[index], weight);
370 }
371 if axis_points[next] {
372 let last = control_points.len() - 1;
373 let weight = control_points[last].w;
374 control_points[last] = Vec4::from_point(snapped[next], weight);
375 }
376 curves[index] = NurbsCurve::new(
377 curves[index].degree,
378 curves[index].knots.clone(),
379 control_points,
380 )?;
381 }
382 points = closure_points(&curves)?;
383 axis_points = points
384 .iter()
385 .map(|point| radial_distance(*point) <= TOLERANCE * 100.0)
386 .collect();
387 }
388 if std::env::var("BREP_REVOLVE_WELD").as_deref() != Ok("0") && weld_junctions(&mut curves)? {
394 points = closure_points(&curves)?;
395 }
396 let axis_curves = curves
397 .iter()
398 .map(curve_on_axis)
399 .collect::<Result<Vec<_>, _>>()?;
400 let radial_of = |curves: &[NurbsCurve]| {
405 curves
406 .iter()
407 .enumerate()
408 .map(|(index, curve)| {
409 if axis_curves[index] {
410 return Ok(false);
411 }
412 Ok(is_straight_radial(curve, axis)?)
413 })
414 .collect::<Result<Vec<_>, String>>()
415 };
416 let mut radial_curves = radial_of(&curves)?;
417 let flatten_radial = flatten_radial && std::env::var("BREP_REVOLVE_FLAT_RADIAL").as_deref() != Ok("0");
419 if flatten_radial && flatten_radial_generatrices(&mut curves, &radial_curves, axis_point, axis)? {
420 points = closure_points(&curves)?;
421 radial_curves = radial_of(&curves)?;
422 }
423 Ok(RevolveProfile {
424 curves,
425 points,
426 axis_points,
427 exact_axis_points,
428 axis_curves,
429 radial_curves,
430 radial,
431 axis,
432 input_indices,
433 })
434}
435
436fn is_straight_radial(curve: &NurbsCurve, axis: Vec3) -> Result<bool, String> {
441 if curve.degree != 1 || curve.control_points.len() != 2 {
442 return Ok(false);
443 }
444 let [start, end] = curve.domain()?;
445 let p0 = curve.evaluate(start)?;
446 let p1 = curve.evaluate(end)?;
447 let direction = p1.sub(p0);
448 let length = direction.length();
449 if length <= TOLERANCE {
450 return Ok(false);
451 }
452 Ok(direction.dot(axis).abs() <= 1e-3 * length)
453}
454
455fn junction_curve(
456 index: usize,
457 surfaces: &[Option<NurbsSurface>],
458 points: &[Vec3],
459) -> Result<NurbsCurve, String> {
460 if let Some(surface) = &surfaces[index] {
461 return surface.iso_curve_v(surface.knots_v[surface.degree_v]);
462 }
463 let previous = (index + surfaces.len() - 1) % surfaces.len();
464 if let Some(surface) = &surfaces[previous] {
465 return surface.iso_curve_v(surface.knots_v[surface.knots_v.len() - 1 - surface.degree_v]);
466 }
467 make_line(points[index], points[index])
468}
469
470#[allow(clippy::too_many_arguments)]
478fn radial_plane_face(
479 profile: &RevolveProfile,
480 index: usize,
481 count: usize,
482 origin: Vec3,
483 revolution: &NurbsSurface,
484 arc_curves: &[NurbsCurve],
485 end_curves: &[NurbsCurve],
486 arc_ids: &[u64],
487 start_edge_ids: &[u64],
488 end_edge_ids: &[u64],
489 counter: &mut u64,
490 name: Option<String>,
491) -> Result<FaceRecord, String> {
492 let axis = profile.axis;
493 let a_start = profile.points[index].sub(origin).dot(axis);
494 let a_end = profile.points[(index + 1) % count].sub(origin).dot(axis);
495 let plane_center = origin.add(axis.scale(0.5 * (a_start + a_end)));
496 let e1 = profile.radial;
497 let e2 = axis.cross(e1);
498
499 let next = (index + 1) % count;
500 let boundary: [(NurbsCurve, u64, bool); 4] = [
501 (arc_curves[index].clone(), arc_ids[index], true),
502 (end_curves[index].clone(), end_edge_ids[index], true),
503 (arc_curves[next].clone(), arc_ids[next], false),
504 (profile.curves[index].clone(), start_edge_ids[index], false),
505 ];
506
507 let mut min1 = f64::INFINITY;
508 let mut max1 = f64::NEG_INFINITY;
509 let mut min2 = f64::INFINITY;
510 let mut max2 = f64::NEG_INFINITY;
511 for (curve, _, _) in &boundary {
512 let [s, e] = curve.domain()?;
513 for k in 0..=16 {
514 let point = curve.evaluate(s + (e - s) * k as f64 / 16.0)?;
515 let d = point.sub(plane_center);
516 let c1 = d.dot(e1);
517 let c2 = d.dot(e2);
518 min1 = min1.min(c1);
519 max1 = max1.max(c1);
520 min2 = min2.min(c2);
521 max2 = max2.max(c2);
522 }
523 }
524 let padding = (max1 - min1).max(max2 - min2) * 0.05 + 1e-6;
525 let width = max1 - min1 + 2.0 * padding;
526 let height = max2 - min2 + 2.0 * padding;
527 let corner = plane_center
528 .add(e1.scale(min1 - padding))
529 .add(e2.scale(min2 - padding));
530 let plane = make_plane(corner, e1, e2, width, height)?;
531
532 let [ru0, ru1] = revolution.domain_u()?;
536 let [rv0, rv1] = revolution.domain_v()?;
537 let rev_normal = revolution.normal((ru0 + ru1) * 0.5, (rv0 + rv1) * 0.5)?;
538 let same_sense = rev_normal.dot(e1.cross(e2)) >= 0.0;
539
540 let mut coedges = Vec::with_capacity(4);
541 for (curve, edge_id, forward) in boundary {
542 let mapped = curve_to_plane_parameters(&curve, corner, e1, e2)?;
543 let pcurve = if forward { mapped } else { mapped.reversed()? };
544 coedges.push(CoedgeRecord {
545 id: next_id(counter),
546 edge_id,
547 forward,
548 pcurve,
549 });
550 }
551 let loop_id = next_id(counter);
552 Ok(FaceRecord {
553 id: next_id(counter),
554 surface: plane,
555 same_sense,
556 loops: vec![LoopRecord {
557 id: loop_id,
558 coedges,
559 }],
560 name,
561 })
562}
563
564pub fn revolve_profile_brep(
565 input: &[NurbsCurve],
566 axis_point: Vec3,
567 axis_direction: Vec3,
568 angle: f64,
569) -> Result<BrepSolid, String> {
570 revolve_profile_brep_named(input, axis_point, axis_direction, angle, &[], &[])
571}
572
573pub fn revolve_profile_brep_named(
579 input: &[NurbsCurve],
580 axis_point: Vec3,
581 axis_direction: Vec3,
582 angle: f64,
583 side_names: &[Option<String>],
584 cap_names: &[Option<String>],
585) -> Result<BrepSolid, String> {
586 if angle <= 1e-9 || angle > std::f64::consts::TAU + 1e-9 {
587 return Err("revolveSolid: angle must be in (0, 2*PI]".into());
588 }
589 if !side_names.is_empty() && side_names.len() != input.len() {
590 return Err(format!(
591 "revolveSolid: {} side names for {} profile curves",
592 side_names.len(),
593 input.len()
594 ));
595 }
596 let full_turn = angle > std::f64::consts::TAU - 1e-9;
597 let mut timing = crate::loft_topology::ConstructionProfile::new("revolve");
598 let prepared = prepare_profile(input, axis_point, axis_direction, !full_turn);
599 timing.lap("profile");
600 let built = prepared.and_then(|profile| {
601 if full_turn {
602 revolve_full(profile, axis_point, side_names)
603 } else {
604 revolve_partial(profile, axis_point, angle, side_names, cap_names)
605 }
606 });
607 dump_revolve(input, axis_point, axis_direction, angle, &built);
608 built
609}
610
611fn dump_revolve(
616 input: &[NurbsCurve],
617 axis_point: Vec3,
618 axis_direction: Vec3,
619 angle: f64,
620 built: &Result<BrepSolid, String>,
621) {
622 static SERIAL: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
623 let Some(directory) = std::env::var("BREP_REVOLVE_DUMP").ok().filter(|value| !value.is_empty()) else {
624 return;
625 };
626 let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
627 let record = serde_json::json!({
628 "curves": input,
629 "axis_point": axis_point,
630 "axis_direction": axis_direction,
631 "angle": angle,
632 "solid": built.as_ref().ok(),
633 "error": built.as_ref().err(),
634 });
635 let path = std::path::Path::new(&directory).join(format!("{}-{serial:05}.json", std::process::id()));
636 if let Err(error) = std::fs::write(&path, record.to_string()) {
637 eprintln!("BREP_REVOLVE_DUMP {}: {error}", path.display());
638 }
639}
640
641fn side_name(
642 profile: &RevolveProfile,
643 curve_index: usize,
644 side_names: &[Option<String>],
645) -> Option<String> {
646 profile
647 .input_indices
648 .get(curve_index)
649 .and_then(|input_index| side_names.get(*input_index))
650 .cloned()
651 .flatten()
652}
653
654fn revolve_full(
655 profile: RevolveProfile,
656 origin: Vec3,
657 side_names: &[Option<String>],
658) -> Result<BrepSolid, String> {
659 let mut timing = crate::loft_topology::ConstructionProfile::new("revolve_full");
660 let count = profile.curves.len();
661 let surfaces: Vec<Option<NurbsSurface>> = profile
662 .curves
663 .iter()
664 .enumerate()
665 .map(|(index, curve)| {
666 if profile.axis_curves[index] {
667 Ok(None)
668 } else {
669 make_revolution(origin, profile.axis, curve, std::f64::consts::TAU).map(Some)
670 }
671 })
672 .collect::<Result<_, String>>()?;
673 timing.lap("surface");
674 let mut vertices: Vec<VertexRecord> = profile
675 .points
676 .iter()
677 .enumerate()
678 .map(|(index, point)| VertexRecord {
679 id: index as u64 + 1,
680 point: *point,
681 })
682 .collect();
683 let mut counter = 100_u64;
684 let mut edges = Vec::new();
685 let mut circle_ids = Vec::with_capacity(count);
686 let mut end_circle_ids = Vec::with_capacity(count);
694 let mut end_vertex_ids: Vec<u64> = (0..count).map(|index| index as u64 + 1).collect();
695 let mut next_vertex_id = count as u64 + 1;
696 for index in 0..count {
697 let curve = junction_curve(index, &surfaces, &profile.points)?;
698 let [start, end] = curve.domain()?;
699 let id = next_id(&mut counter);
700 circle_ids.push(id);
701 let previous = (index + count - 1) % count;
702 let pinch = profile.axis_points[index]
703 && profile.exact_axis_points[index]
704 && !profile.axis_curves[index]
705 && !profile.axis_curves[previous];
706 if pinch {
707 next_vertex_id += 1;
708 let vertex_id = next_vertex_id;
709 vertices.push(VertexRecord { id: vertex_id, point: profile.points[index] });
710 let end_id = next_id(&mut counter);
711 edges.push(EdgeRecord {
712 id: end_id,
713 curve: curve.clone(),
714 t0: start,
715 t1: end,
716 start_vertex_id: vertex_id,
717 end_vertex_id: vertex_id,
718 degenerate: true,
719 name: None,
720 });
721 end_circle_ids.push(end_id);
722 end_vertex_ids[index] = vertex_id;
723 } else {
724 end_circle_ids.push(id);
725 }
726 edges.push(EdgeRecord {
727 id,
728 curve,
729 t0: start,
730 t1: end,
731 start_vertex_id: index as u64 + 1,
732 end_vertex_id: index as u64 + 1,
733 degenerate: profile.axis_points[index],
734 name: None,
735 });
736 }
737 timing.lap("edges");
738 let mut faces = Vec::new();
739 for index in 0..count {
740 if profile.axis_curves[index] {
741 continue;
742 }
743 let curve = profile.curves[index].clone();
744 let [start, end] = curve.domain()?;
745 let seam_id = next_id(&mut counter);
746 edges.push(EdgeRecord {
747 id: seam_id,
748 curve,
749 t0: start,
750 t1: end,
751 start_vertex_id: index as u64 + 1,
752 end_vertex_id: end_vertex_ids[(index + 1) % count],
753 degenerate: false,
754 name: None,
755 });
756 let coedges = vec![
757 CoedgeRecord {
758 id: next_id(&mut counter),
759 edge_id: circle_ids[index],
760 forward: true,
761 pcurve: parameter_line(0.0, start, 1.0, start)?,
762 },
763 CoedgeRecord {
764 id: next_id(&mut counter),
765 edge_id: seam_id,
766 forward: true,
767 pcurve: parameter_line(1.0, start, 1.0, end)?,
768 },
769 CoedgeRecord {
770 id: next_id(&mut counter),
771 edge_id: end_circle_ids[(index + 1) % count],
772 forward: false,
773 pcurve: parameter_line(1.0, end, 0.0, end)?,
774 },
775 CoedgeRecord {
776 id: next_id(&mut counter),
777 edge_id: seam_id,
778 forward: false,
779 pcurve: parameter_line(0.0, end, 0.0, start)?,
780 },
781 ];
782 let loop_id = next_id(&mut counter);
783 faces.push(FaceRecord {
784 id: next_id(&mut counter),
785 surface: surfaces[index].clone().unwrap(),
786 same_sense: true,
787 loops: vec![LoopRecord {
788 id: loop_id,
789 coedges,
790 }],
791 name: side_name(&profile, index, side_names),
792 });
793 }
794 let solid = BrepSolid {
795 mass_properties_cache: Default::default(),
796 cells: Default::default(),
797 id: next_id(&mut counter),
798 vertices,
799 edges,
800 shells: vec![ShellRecord {
801 id: next_id(&mut counter),
802 faces,
803 }],
804 genus: if profile.axis_points.iter().any(|flag| *flag) {
805 0
806 } else {
807 1
808 },
809 kind: crate::BodyKind::Solid,
810 };
811 timing.lap("topology_pcurves");
812 let issues = solid.validate();
813 timing.lap("validation");
814 if issues.is_empty() {
815 Ok(solid)
816 } else {
817 Err(format!("Rust full revolution is invalid: {issues:?}"))
818 }
819}
820
821fn revolve_partial(
822 profile: RevolveProfile,
823 origin: Vec3,
824 angle: f64,
825 side_names: &[Option<String>],
826 cap_names: &[Option<String>],
827) -> Result<BrepSolid, String> {
828 let mut timing = crate::loft_topology::ConstructionProfile::new("revolve_partial");
829 let count = profile.curves.len();
830 let end_curves: Vec<NurbsCurve> = profile
831 .curves
832 .iter()
833 .map(|curve| rotate_curve(curve, origin, profile.axis, angle))
834 .collect::<Result<_, _>>()?;
835 let surfaces: Vec<Option<NurbsSurface>> = profile
836 .curves
837 .iter()
838 .enumerate()
839 .map(|(index, curve)| {
840 if profile.axis_curves[index] {
841 Ok(None)
842 } else {
843 make_revolution(origin, profile.axis, curve, angle).map(Some)
844 }
845 })
846 .collect::<Result<_, String>>()?;
847 timing.lap("surface");
848 let mut vertices = Vec::new();
849 let mut start_vertex_ids = Vec::with_capacity(count);
850 let mut end_vertex_ids = Vec::with_capacity(count);
851 let mut counter = 100_u64;
852 for point in &profile.points {
853 let id = next_id(&mut counter);
854 start_vertex_ids.push(id);
855 vertices.push(VertexRecord { id, point: *point });
856 }
857 for index in 0..count {
858 if profile.axis_points[index] {
859 end_vertex_ids.push(start_vertex_ids[index]);
860 } else {
861 let id = next_id(&mut counter);
862 end_vertex_ids.push(id);
863 vertices.push(VertexRecord {
864 id,
865 point: rotate_point(profile.points[index], origin, profile.axis, angle),
866 });
867 }
868 }
869
870 let mut edges = Vec::new();
871 let mut arc_ids = Vec::with_capacity(count);
872 let mut arc_curves = Vec::with_capacity(count);
873 for index in 0..count {
874 let curve = junction_curve(index, &surfaces, &profile.points)?;
875 let [start, end] = curve.domain()?;
876 let id = next_id(&mut counter);
877 arc_ids.push(id);
878 arc_curves.push(curve.clone());
879 edges.push(EdgeRecord {
880 id,
881 curve,
882 t0: start,
883 t1: end,
884 start_vertex_id: start_vertex_ids[index],
885 end_vertex_id: end_vertex_ids[index],
886 degenerate: profile.axis_points[index],
887 name: None,
888 });
889 }
890 let mut start_edge_ids = Vec::with_capacity(count);
891 let mut end_edge_ids = Vec::with_capacity(count);
892 for index in 0..count {
893 let [start, end] = profile.curves[index].domain()?;
894 let start_id = next_id(&mut counter);
895 start_edge_ids.push(start_id);
896 edges.push(EdgeRecord {
897 id: start_id,
898 curve: profile.curves[index].clone(),
899 t0: start,
900 t1: end,
901 start_vertex_id: start_vertex_ids[index],
902 end_vertex_id: start_vertex_ids[(index + 1) % count],
903 degenerate: false,
904 name: None,
905 });
906 if profile.axis_curves[index] {
907 end_edge_ids.push(start_id);
908 } else {
909 let end_id = next_id(&mut counter);
910 end_edge_ids.push(end_id);
911 edges.push(EdgeRecord {
912 id: end_id,
913 curve: end_curves[index].clone(),
914 t0: start,
915 t1: end,
916 start_vertex_id: end_vertex_ids[index],
917 end_vertex_id: end_vertex_ids[(index + 1) % count],
918 degenerate: false,
919 name: None,
920 });
921 }
922 }
923
924 timing.lap("edges");
925 let mut faces = Vec::new();
926 for index in 0..count {
927 if profile.axis_curves[index] {
928 continue;
929 }
930 if profile.radial_curves[index] {
931 faces.push(radial_plane_face(
932 &profile,
933 index,
934 count,
935 origin,
936 surfaces[index].as_ref().unwrap(),
937 &arc_curves,
938 &end_curves,
939 &arc_ids,
940 &start_edge_ids,
941 &end_edge_ids,
942 &mut counter,
943 side_name(&profile, index, side_names),
944 )?);
945 continue;
946 }
947 let [start, end] = profile.curves[index].domain()?;
948 let coedges = vec![
949 CoedgeRecord {
950 id: next_id(&mut counter),
951 edge_id: arc_ids[index],
952 forward: true,
953 pcurve: parameter_line(0.0, start, 1.0, start)?,
954 },
955 CoedgeRecord {
956 id: next_id(&mut counter),
957 edge_id: end_edge_ids[index],
958 forward: true,
959 pcurve: parameter_line(1.0, start, 1.0, end)?,
960 },
961 CoedgeRecord {
962 id: next_id(&mut counter),
963 edge_id: arc_ids[(index + 1) % count],
964 forward: false,
965 pcurve: parameter_line(1.0, end, 0.0, end)?,
966 },
967 CoedgeRecord {
968 id: next_id(&mut counter),
969 edge_id: start_edge_ids[index],
970 forward: false,
971 pcurve: parameter_line(0.0, end, 0.0, start)?,
972 },
973 ];
974 let loop_id = next_id(&mut counter);
975 faces.push(FaceRecord {
976 id: next_id(&mut counter),
977 surface: surfaces[index].clone().unwrap(),
978 same_sense: true,
979 loops: vec![LoopRecord {
980 id: loop_id,
981 coedges,
982 }],
983 name: side_name(&profile, index, side_names),
984 });
985 }
986
987 let mut min_radial = f64::INFINITY;
988 let mut max_radial = f64::NEG_INFINITY;
989 let mut min_axis = f64::INFINITY;
990 let mut max_axis = f64::NEG_INFINITY;
991 for curve in &profile.curves {
992 let [start, end] = curve.domain()?;
993 for index in 0..=24 {
994 let delta = curve
995 .evaluate(start + (end - start) * index as f64 / 24.0)?
996 .sub(origin);
997 let radial = delta.dot(profile.radial);
998 let axial = delta.dot(profile.axis);
999 min_radial = min_radial.min(radial);
1000 max_radial = max_radial.max(radial);
1001 min_axis = min_axis.min(axial);
1002 max_axis = max_axis.max(axial);
1003 }
1004 }
1005 let padding = (max_radial - min_radial).max(max_axis - min_axis) * 0.05 + 1e-6;
1006 let width = max_radial - min_radial + 2.0 * padding;
1007 let height = max_axis - min_axis + 2.0 * padding;
1008 let start_origin = origin
1009 .add(profile.radial.scale(min_radial - padding))
1010 .add(profile.axis.scale(min_axis - padding));
1011 let mut start_coedges = Vec::with_capacity(count);
1012 for index in 0..count {
1013 start_coedges.push(CoedgeRecord {
1014 id: next_id(&mut counter),
1015 edge_id: start_edge_ids[index],
1016 forward: true,
1017 pcurve: curve_to_plane_parameters(
1018 &profile.curves[index],
1019 start_origin,
1020 profile.radial,
1021 profile.axis,
1022 )?,
1023 });
1024 }
1025 let start_loop_id = next_id(&mut counter);
1026 faces.push(FaceRecord {
1027 id: next_id(&mut counter),
1028 surface: make_plane(start_origin, profile.radial, profile.axis, width, height)?,
1029 same_sense: true,
1030 loops: vec![LoopRecord {
1031 id: start_loop_id,
1032 coedges: start_coedges,
1033 }],
1034 name: cap_names.first().cloned().flatten(),
1035 });
1036
1037 let end_radial = rotate_point(origin.add(profile.radial), origin, profile.axis, angle)
1038 .sub(origin)
1039 .normalized()?;
1040 let end_origin = origin
1041 .add(end_radial.scale(min_radial - padding))
1042 .add(profile.axis.scale(min_axis - padding));
1043 let mut end_coedges = Vec::with_capacity(count);
1044 for index in (0..count).rev() {
1045 end_coedges.push(CoedgeRecord {
1046 id: next_id(&mut counter),
1047 edge_id: end_edge_ids[index],
1048 forward: false,
1049 pcurve: curve_to_plane_parameters(
1050 &end_curves[index],
1051 end_origin,
1052 profile.axis,
1053 end_radial,
1054 )?
1055 .reversed()?,
1056 });
1057 }
1058 let end_loop_id = next_id(&mut counter);
1059 faces.push(FaceRecord {
1060 id: next_id(&mut counter),
1061 surface: make_plane(end_origin, profile.axis, end_radial, height, width)?,
1062 same_sense: true,
1063 loops: vec![LoopRecord {
1064 id: end_loop_id,
1065 coedges: end_coedges,
1066 }],
1067 name: cap_names.get(1).cloned().flatten(),
1068 });
1069
1070 let solid = BrepSolid {
1071 mass_properties_cache: Default::default(),
1072 cells: Default::default(),
1073 id: next_id(&mut counter),
1074 vertices,
1075 edges,
1076 shells: vec![ShellRecord {
1077 id: next_id(&mut counter),
1078 faces,
1079 }],
1080 genus: 0,
1081 kind: crate::BodyKind::Solid,
1082 };
1083 timing.lap("topology_pcurves");
1084 let issues = solid.validate();
1085 timing.lap("validation");
1086 if issues.is_empty() {
1087 Ok(solid)
1088 } else {
1089 Err(format!("Rust partial revolution is invalid: {issues:?}"))
1090 }
1091}
1092