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