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 let [closed] = input {
242 if let Some(halves) = crate::closed_curves::closed_curve_halves(closed)? {
243 return prepare_profile(&halves, axis_point, axis_direction, flatten_radial);
244 }
245 }
246 if input.len() < 2 {
247 return Err("profile needs at least 2 curves".into());
248 }
249 let axis = axis_direction.normalized()?;
250 let mut curves: Vec<NurbsCurve> = input
251 .iter()
252 .map(|curve| {
253 NurbsCurve::new(
254 curve.degree,
255 curve.knots.clone(),
256 curve.control_points.clone(),
257 )
258 })
259 .collect::<Result<_, _>>()?;
260 let closure_points = |curves: &[NurbsCurve]| -> Result<Vec<Vec3>, String> {
261 let mut points = Vec::with_capacity(curves.len());
262 for index in 0..curves.len() {
263 let [start, end] = curves[index].domain()?;
264 let next_start = curves[(index + 1) % curves.len()].domain()?[0];
265 let point = curves[index].evaluate(start)?;
266 if curves[index]
267 .evaluate(end)?
268 .sub(curves[(index + 1) % curves.len()].evaluate(next_start)?)
269 .length()
270 > TOLERANCE
271 {
272 return Err(format!("profile not closed at curve {index}"));
273 }
274 points.push(point);
275 }
276 Ok(points)
277 };
278 let mut points = closure_points(&curves)?;
279
280 let radial_distance = |point: Vec3| {
281 let delta = point.sub(axis_point);
282 delta.sub(axis.scale(delta.dot(axis))).length()
283 };
284 let mut radial = None;
285 'outer: for curve in &curves {
286 let [start, end] = curve.domain()?;
287 for index in 0..=24 {
288 let point = curve.evaluate(start + (end - start) * index as f64 / 24.0)?;
289 let delta = point.sub(axis_point);
290 let candidate = delta.sub(axis.scale(delta.dot(axis)));
291 if candidate.length() > TOLERANCE * 100.0 {
292 radial = Some(candidate.normalized()?);
293 break 'outer;
294 }
295 }
296 }
297 let radial = radial.ok_or_else(|| "revolveSolid: profile touches the axis".to_string())?;
298 let curve_on_axis = |curve: &NurbsCurve| -> Result<bool, String> {
299 let [start, end] = curve.domain()?;
300 for index in 0..=8 {
301 if radial_distance(curve.evaluate(start + (end - start) * index as f64 / 8.0)?)
302 > TOLERANCE * 100.0
303 {
304 return Ok(false);
305 }
306 }
307 Ok(true)
308 };
309 for curve in &curves {
310 let [start, end] = curve.domain()?;
311 let on_axis = curve_on_axis(curve)?;
312 for index in 0..=24 {
313 let point = curve.evaluate(start + (end - start) * index as f64 / 24.0)?;
314 let delta = point.sub(axis_point);
315 let in_plane_radial = delta.dot(radial);
316 let off_plane = delta
317 .sub(axis.scale(delta.dot(axis)))
318 .sub(radial.scale(in_plane_radial))
319 .length();
320 if off_plane > TOLERANCE * 100.0 {
321 return Err("revolveSolid: profile not in a half-plane containing the axis".into());
322 }
323 if in_plane_radial < -TOLERANCE * 100.0 {
324 return Err("revolveSolid: profile crosses the revolve axis".into());
325 }
326 if in_plane_radial < TOLERANCE * 100.0 && index != 0 && index != 24 && !on_axis {
327 return Err(
328 "revolveSolid: profile may only touch the axis at boundary vertices or axis edges"
329 .into(),
330 );
331 }
332 }
333 }
334 let mut input_indices: Vec<usize> = (0..curves.len()).collect();
335 if signed_profile_area(&curves, axis_point, radial, axis)? < 0.0 {
336 curves = curves
337 .iter()
338 .rev()
339 .map(NurbsCurve::reversed)
340 .collect::<Result<_, _>>()?;
341 points = closure_points(&curves)?;
342 input_indices.reverse();
343 }
344 let mut axis_points: Vec<bool> = points
345 .iter()
346 .map(|point| radial_distance(*point) <= TOLERANCE * 100.0)
347 .collect();
348 let profile_size = points
349 .iter()
350 .fold(1.0f64, |size, point| size.max(point.sub(axis_point).length()));
351 let exact_axis_points: Vec<bool> = points
352 .iter()
353 .map(|point| radial_distance(*point) <= 1e-12 * profile_size)
354 .collect();
355 if axis_points.iter().any(|flag| *flag) {
356 let snapped: Vec<Vec3> = points
357 .iter()
358 .enumerate()
359 .map(|(index, point)| {
360 if axis_points[index] {
361 axis_point.add(axis.scale(point.sub(axis_point).dot(axis)))
362 } else {
363 *point
364 }
365 })
366 .collect();
367 for index in 0..curves.len() {
368 let next = (index + 1) % curves.len();
369 if !axis_points[index] && !axis_points[next] {
370 continue;
371 }
372 let mut control_points = curves[index].control_points.clone();
373 if axis_points[index] {
374 let weight = control_points[0].w;
375 control_points[0] = Vec4::from_point(snapped[index], weight);
376 }
377 if axis_points[next] {
378 let last = control_points.len() - 1;
379 let weight = control_points[last].w;
380 control_points[last] = Vec4::from_point(snapped[next], weight);
381 }
382 curves[index] = NurbsCurve::new(
383 curves[index].degree,
384 curves[index].knots.clone(),
385 control_points,
386 )?;
387 }
388 points = closure_points(&curves)?;
389 axis_points = points
390 .iter()
391 .map(|point| radial_distance(*point) <= TOLERANCE * 100.0)
392 .collect();
393 }
394 if std::env::var("BREP_REVOLVE_WELD").as_deref() != Ok("0") && weld_junctions(&mut curves)? {
400 points = closure_points(&curves)?;
401 }
402 let axis_curves = curves
403 .iter()
404 .map(curve_on_axis)
405 .collect::<Result<Vec<_>, _>>()?;
406 let radial_of = |curves: &[NurbsCurve]| {
411 curves
412 .iter()
413 .enumerate()
414 .map(|(index, curve)| {
415 if axis_curves[index] {
416 return Ok(false);
417 }
418 Ok(is_straight_radial(curve, axis)?)
419 })
420 .collect::<Result<Vec<_>, String>>()
421 };
422 let mut radial_curves = radial_of(&curves)?;
423 let flatten_radial = flatten_radial && std::env::var("BREP_REVOLVE_FLAT_RADIAL").as_deref() != Ok("0");
425 if flatten_radial && flatten_radial_generatrices(&mut curves, &radial_curves, axis_point, axis)? {
426 points = closure_points(&curves)?;
427 radial_curves = radial_of(&curves)?;
428 }
429 Ok(RevolveProfile {
430 curves,
431 points,
432 axis_points,
433 exact_axis_points,
434 axis_curves,
435 radial_curves,
436 radial,
437 axis,
438 input_indices,
439 })
440}
441
442fn is_straight_radial(curve: &NurbsCurve, axis: Vec3) -> Result<bool, String> {
447 if curve.degree != 1 || curve.control_points.len() != 2 {
448 return Ok(false);
449 }
450 let [start, end] = curve.domain()?;
451 let p0 = curve.evaluate(start)?;
452 let p1 = curve.evaluate(end)?;
453 let direction = p1.sub(p0);
454 let length = direction.length();
455 if length <= TOLERANCE {
456 return Ok(false);
457 }
458 Ok(direction.dot(axis).abs() <= 1e-3 * length)
459}
460
461fn junction_curve(
462 index: usize,
463 surfaces: &[Option<NurbsSurface>],
464 points: &[Vec3],
465) -> Result<NurbsCurve, String> {
466 if let Some(surface) = &surfaces[index] {
467 return surface.iso_curve_v(surface.knots_v[surface.degree_v]);
468 }
469 let previous = (index + surfaces.len() - 1) % surfaces.len();
470 if let Some(surface) = &surfaces[previous] {
471 return surface.iso_curve_v(surface.knots_v[surface.knots_v.len() - 1 - surface.degree_v]);
472 }
473 make_line(points[index], points[index])
474}
475
476#[allow(clippy::too_many_arguments)]
484fn radial_plane_face(
485 profile: &RevolveProfile,
486 index: usize,
487 count: usize,
488 origin: Vec3,
489 revolution: &NurbsSurface,
490 arc_curves: &[NurbsCurve],
491 end_curves: &[NurbsCurve],
492 arc_ids: &[u64],
493 start_edge_ids: &[u64],
494 end_edge_ids: &[u64],
495 counter: &mut u64,
496 name: Option<String>,
497) -> Result<FaceRecord, String> {
498 let axis = profile.axis;
499 let a_start = profile.points[index].sub(origin).dot(axis);
500 let a_end = profile.points[(index + 1) % count].sub(origin).dot(axis);
501 let plane_center = origin.add(axis.scale(0.5 * (a_start + a_end)));
502 let e1 = profile.radial;
503 let e2 = axis.cross(e1);
504
505 let next = (index + 1) % count;
506 let boundary: [(NurbsCurve, u64, bool); 4] = [
507 (arc_curves[index].clone(), arc_ids[index], true),
508 (end_curves[index].clone(), end_edge_ids[index], true),
509 (arc_curves[next].clone(), arc_ids[next], false),
510 (profile.curves[index].clone(), start_edge_ids[index], false),
511 ];
512
513 let mut min1 = f64::INFINITY;
514 let mut max1 = f64::NEG_INFINITY;
515 let mut min2 = f64::INFINITY;
516 let mut max2 = f64::NEG_INFINITY;
517 for (curve, _, _) in &boundary {
518 let [s, e] = curve.domain()?;
519 for k in 0..=16 {
520 let point = curve.evaluate(s + (e - s) * k as f64 / 16.0)?;
521 let d = point.sub(plane_center);
522 let c1 = d.dot(e1);
523 let c2 = d.dot(e2);
524 min1 = min1.min(c1);
525 max1 = max1.max(c1);
526 min2 = min2.min(c2);
527 max2 = max2.max(c2);
528 }
529 }
530 let padding = (max1 - min1).max(max2 - min2) * 0.05 + 1e-6;
531 let width = max1 - min1 + 2.0 * padding;
532 let height = max2 - min2 + 2.0 * padding;
533 let corner = plane_center
534 .add(e1.scale(min1 - padding))
535 .add(e2.scale(min2 - padding));
536 let plane = make_plane(corner, e1, e2, width, height)?;
537
538 let [ru0, ru1] = revolution.domain_u()?;
542 let [rv0, rv1] = revolution.domain_v()?;
543 let rev_normal = revolution.normal((ru0 + ru1) * 0.5, (rv0 + rv1) * 0.5)?;
544 let same_sense = rev_normal.dot(e1.cross(e2)) >= 0.0;
545
546 let mut coedges = Vec::with_capacity(4);
547 for (curve, edge_id, forward) in boundary {
548 let mapped = curve_to_plane_parameters(&curve, corner, e1, e2)?;
549 let pcurve = if forward { mapped } else { mapped.reversed()? };
550 coedges.push(CoedgeRecord {
551 id: next_id(counter),
552 edge_id,
553 forward,
554 pcurve,
555 });
556 }
557 let loop_id = next_id(counter);
558 Ok(FaceRecord {
559 id: next_id(counter),
560 surface: plane,
561 same_sense,
562 loops: vec![LoopRecord {
563 id: loop_id,
564 coedges,
565 }],
566 name,
567 })
568}
569
570pub fn revolve_profile_brep(
571 input: &[NurbsCurve],
572 axis_point: Vec3,
573 axis_direction: Vec3,
574 angle: f64,
575) -> Result<BrepSolid, String> {
576 revolve_profile_brep_named(input, axis_point, axis_direction, angle, &[], &[])
577}
578
579pub fn revolve_profile_brep_named(
585 input: &[NurbsCurve],
586 axis_point: Vec3,
587 axis_direction: Vec3,
588 angle: f64,
589 side_names: &[Option<String>],
590 cap_names: &[Option<String>],
591) -> Result<BrepSolid, String> {
592 if angle <= 1e-9 || angle > std::f64::consts::TAU + 1e-9 {
593 return Err("revolveSolid: angle must be in (0, 2*PI]".into());
594 }
595 if !side_names.is_empty() && side_names.len() != input.len() {
596 return Err(format!(
597 "revolveSolid: {} side names for {} profile curves",
598 side_names.len(),
599 input.len()
600 ));
601 }
602 let full_turn = angle > std::f64::consts::TAU - 1e-9;
603 let mut timing = crate::loft_topology::ConstructionProfile::new("revolve");
604 let prepared = prepare_profile(input, axis_point, axis_direction, !full_turn);
605 timing.lap("profile");
606 let built = prepared.and_then(|profile| {
607 if full_turn {
608 revolve_full(profile, axis_point, side_names)
609 } else {
610 revolve_partial(profile, axis_point, angle, side_names, cap_names)
611 }
612 });
613 dump_revolve(input, axis_point, axis_direction, angle, &built);
614 built
615}
616
617fn dump_revolve(
622 input: &[NurbsCurve],
623 axis_point: Vec3,
624 axis_direction: Vec3,
625 angle: f64,
626 built: &Result<BrepSolid, String>,
627) {
628 static SERIAL: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
629 let Some(directory) = std::env::var("BREP_REVOLVE_DUMP").ok().filter(|value| !value.is_empty()) else {
630 return;
631 };
632 let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
633 let record = serde_json::json!({
634 "curves": input,
635 "axis_point": axis_point,
636 "axis_direction": axis_direction,
637 "angle": angle,
638 "solid": built.as_ref().ok(),
639 "error": built.as_ref().err(),
640 });
641 let path = std::path::Path::new(&directory).join(format!("{}-{serial:05}.json", std::process::id()));
642 if let Err(error) = std::fs::write(&path, record.to_string()) {
643 eprintln!("BREP_REVOLVE_DUMP {}: {error}", path.display());
644 }
645}
646
647fn side_name(
648 profile: &RevolveProfile,
649 curve_index: usize,
650 side_names: &[Option<String>],
651) -> Option<String> {
652 profile
653 .input_indices
654 .get(curve_index)
655 .and_then(|input_index| side_names.get(*input_index))
656 .cloned()
657 .flatten()
658}
659
660fn revolve_full(
661 profile: RevolveProfile,
662 origin: Vec3,
663 side_names: &[Option<String>],
664) -> Result<BrepSolid, String> {
665 let mut timing = crate::loft_topology::ConstructionProfile::new("revolve_full");
666 let count = profile.curves.len();
667 let surfaces: Vec<Option<NurbsSurface>> = profile
668 .curves
669 .iter()
670 .enumerate()
671 .map(|(index, curve)| {
672 if profile.axis_curves[index] {
673 Ok(None)
674 } else {
675 make_revolution(origin, profile.axis, curve, std::f64::consts::TAU).map(Some)
676 }
677 })
678 .collect::<Result<_, String>>()?;
679 timing.lap("surface");
680 let mut vertices: Vec<VertexRecord> = profile
681 .points
682 .iter()
683 .enumerate()
684 .map(|(index, point)| VertexRecord {
685 id: index as u64 + 1,
686 point: *point,
687 })
688 .collect();
689 let mut counter = 100_u64;
690 let mut edges = Vec::new();
691 let mut circle_ids = Vec::with_capacity(count);
692 let mut end_circle_ids = Vec::with_capacity(count);
700 let mut end_vertex_ids: Vec<u64> = (0..count).map(|index| index as u64 + 1).collect();
701 let mut next_vertex_id = count as u64 + 1;
702 for index in 0..count {
703 let curve = junction_curve(index, &surfaces, &profile.points)?;
704 let [start, end] = curve.domain()?;
705 let id = next_id(&mut counter);
706 circle_ids.push(id);
707 let previous = (index + count - 1) % count;
708 let pinch = profile.axis_points[index]
709 && profile.exact_axis_points[index]
710 && !profile.axis_curves[index]
711 && !profile.axis_curves[previous];
712 if pinch {
713 next_vertex_id += 1;
714 let vertex_id = next_vertex_id;
715 vertices.push(VertexRecord { id: vertex_id, point: profile.points[index] });
716 let end_id = next_id(&mut counter);
717 edges.push(EdgeRecord {
718 id: end_id,
719 curve: curve.clone(),
720 t0: start,
721 t1: end,
722 start_vertex_id: vertex_id,
723 end_vertex_id: vertex_id,
724 degenerate: true,
725 name: None,
726 });
727 end_circle_ids.push(end_id);
728 end_vertex_ids[index] = vertex_id;
729 } else {
730 end_circle_ids.push(id);
731 }
732 edges.push(EdgeRecord {
733 id,
734 curve,
735 t0: start,
736 t1: end,
737 start_vertex_id: index as u64 + 1,
738 end_vertex_id: index as u64 + 1,
739 degenerate: profile.axis_points[index],
740 name: None,
741 });
742 }
743 timing.lap("edges");
744 let mut faces = Vec::new();
745 for index in 0..count {
746 if profile.axis_curves[index] {
747 continue;
748 }
749 let curve = profile.curves[index].clone();
750 let [start, end] = curve.domain()?;
751 let seam_id = next_id(&mut counter);
752 edges.push(EdgeRecord {
753 id: seam_id,
754 curve,
755 t0: start,
756 t1: end,
757 start_vertex_id: index as u64 + 1,
758 end_vertex_id: end_vertex_ids[(index + 1) % count],
759 degenerate: false,
760 name: None,
761 });
762 let coedges = vec![
763 CoedgeRecord {
764 id: next_id(&mut counter),
765 edge_id: circle_ids[index],
766 forward: true,
767 pcurve: parameter_line(0.0, start, 1.0, start)?,
768 },
769 CoedgeRecord {
770 id: next_id(&mut counter),
771 edge_id: seam_id,
772 forward: true,
773 pcurve: parameter_line(1.0, start, 1.0, end)?,
774 },
775 CoedgeRecord {
776 id: next_id(&mut counter),
777 edge_id: end_circle_ids[(index + 1) % count],
778 forward: false,
779 pcurve: parameter_line(1.0, end, 0.0, end)?,
780 },
781 CoedgeRecord {
782 id: next_id(&mut counter),
783 edge_id: seam_id,
784 forward: false,
785 pcurve: parameter_line(0.0, end, 0.0, start)?,
786 },
787 ];
788 let loop_id = next_id(&mut counter);
789 faces.push(FaceRecord {
790 id: next_id(&mut counter),
791 surface: surfaces[index].clone().unwrap(),
792 same_sense: true,
793 loops: vec![LoopRecord {
794 id: loop_id,
795 coedges,
796 }],
797 name: side_name(&profile, index, side_names),
798 });
799 }
800 let solid = BrepSolid {
801 mass_properties_cache: Default::default(),
802 cells: Default::default(),
803 id: next_id(&mut counter),
804 vertices,
805 edges,
806 shells: vec![ShellRecord {
807 id: next_id(&mut counter),
808 faces,
809 }],
810 genus: if profile.axis_points.iter().any(|flag| *flag) {
811 0
812 } else {
813 1
814 },
815 kind: crate::BodyKind::Solid,
816 };
817 timing.lap("topology_pcurves");
818 let issues = solid.validate();
819 timing.lap("validation");
820 if issues.is_empty() {
821 Ok(solid)
822 } else {
823 Err(format!("Rust full revolution is invalid: {issues:?}"))
824 }
825}
826
827fn revolve_partial(
828 profile: RevolveProfile,
829 origin: Vec3,
830 angle: f64,
831 side_names: &[Option<String>],
832 cap_names: &[Option<String>],
833) -> Result<BrepSolid, String> {
834 let mut timing = crate::loft_topology::ConstructionProfile::new("revolve_partial");
835 let count = profile.curves.len();
836 let end_curves: Vec<NurbsCurve> = profile
837 .curves
838 .iter()
839 .map(|curve| rotate_curve(curve, origin, profile.axis, angle))
840 .collect::<Result<_, _>>()?;
841 let surfaces: Vec<Option<NurbsSurface>> = profile
842 .curves
843 .iter()
844 .enumerate()
845 .map(|(index, curve)| {
846 if profile.axis_curves[index] {
847 Ok(None)
848 } else {
849 make_revolution(origin, profile.axis, curve, angle).map(Some)
850 }
851 })
852 .collect::<Result<_, String>>()?;
853 timing.lap("surface");
854 let mut vertices = Vec::new();
855 let mut start_vertex_ids = Vec::with_capacity(count);
856 let mut end_vertex_ids = Vec::with_capacity(count);
857 let mut counter = 100_u64;
858 for point in &profile.points {
859 let id = next_id(&mut counter);
860 start_vertex_ids.push(id);
861 vertices.push(VertexRecord { id, point: *point });
862 }
863 for index in 0..count {
864 if profile.axis_points[index] {
865 end_vertex_ids.push(start_vertex_ids[index]);
866 } else {
867 let id = next_id(&mut counter);
868 end_vertex_ids.push(id);
869 vertices.push(VertexRecord {
870 id,
871 point: rotate_point(profile.points[index], origin, profile.axis, angle),
872 });
873 }
874 }
875
876 let mut edges = Vec::new();
877 let mut arc_ids = Vec::with_capacity(count);
878 let mut arc_curves = Vec::with_capacity(count);
879 for index in 0..count {
880 let curve = junction_curve(index, &surfaces, &profile.points)?;
881 let [start, end] = curve.domain()?;
882 let id = next_id(&mut counter);
883 arc_ids.push(id);
884 arc_curves.push(curve.clone());
885 edges.push(EdgeRecord {
886 id,
887 curve,
888 t0: start,
889 t1: end,
890 start_vertex_id: start_vertex_ids[index],
891 end_vertex_id: end_vertex_ids[index],
892 degenerate: profile.axis_points[index],
893 name: None,
894 });
895 }
896 let mut start_edge_ids = Vec::with_capacity(count);
897 let mut end_edge_ids = Vec::with_capacity(count);
898 for index in 0..count {
899 let [start, end] = profile.curves[index].domain()?;
900 let start_id = next_id(&mut counter);
901 start_edge_ids.push(start_id);
902 edges.push(EdgeRecord {
903 id: start_id,
904 curve: profile.curves[index].clone(),
905 t0: start,
906 t1: end,
907 start_vertex_id: start_vertex_ids[index],
908 end_vertex_id: start_vertex_ids[(index + 1) % count],
909 degenerate: false,
910 name: None,
911 });
912 if profile.axis_curves[index] {
913 end_edge_ids.push(start_id);
914 } else {
915 let end_id = next_id(&mut counter);
916 end_edge_ids.push(end_id);
917 edges.push(EdgeRecord {
918 id: end_id,
919 curve: end_curves[index].clone(),
920 t0: start,
921 t1: end,
922 start_vertex_id: end_vertex_ids[index],
923 end_vertex_id: end_vertex_ids[(index + 1) % count],
924 degenerate: false,
925 name: None,
926 });
927 }
928 }
929
930 timing.lap("edges");
931 let mut faces = Vec::new();
932 for index in 0..count {
933 if profile.axis_curves[index] {
934 continue;
935 }
936 if profile.radial_curves[index] {
937 faces.push(radial_plane_face(
938 &profile,
939 index,
940 count,
941 origin,
942 surfaces[index].as_ref().unwrap(),
943 &arc_curves,
944 &end_curves,
945 &arc_ids,
946 &start_edge_ids,
947 &end_edge_ids,
948 &mut counter,
949 side_name(&profile, index, side_names),
950 )?);
951 continue;
952 }
953 let [start, end] = profile.curves[index].domain()?;
954 let coedges = vec![
955 CoedgeRecord {
956 id: next_id(&mut counter),
957 edge_id: arc_ids[index],
958 forward: true,
959 pcurve: parameter_line(0.0, start, 1.0, start)?,
960 },
961 CoedgeRecord {
962 id: next_id(&mut counter),
963 edge_id: end_edge_ids[index],
964 forward: true,
965 pcurve: parameter_line(1.0, start, 1.0, end)?,
966 },
967 CoedgeRecord {
968 id: next_id(&mut counter),
969 edge_id: arc_ids[(index + 1) % count],
970 forward: false,
971 pcurve: parameter_line(1.0, end, 0.0, end)?,
972 },
973 CoedgeRecord {
974 id: next_id(&mut counter),
975 edge_id: start_edge_ids[index],
976 forward: false,
977 pcurve: parameter_line(0.0, end, 0.0, start)?,
978 },
979 ];
980 let loop_id = next_id(&mut counter);
981 faces.push(FaceRecord {
982 id: next_id(&mut counter),
983 surface: surfaces[index].clone().unwrap(),
984 same_sense: true,
985 loops: vec![LoopRecord {
986 id: loop_id,
987 coedges,
988 }],
989 name: side_name(&profile, index, side_names),
990 });
991 }
992
993 let mut min_radial = f64::INFINITY;
994 let mut max_radial = f64::NEG_INFINITY;
995 let mut min_axis = f64::INFINITY;
996 let mut max_axis = f64::NEG_INFINITY;
997 for curve in &profile.curves {
998 let [start, end] = curve.domain()?;
999 for index in 0..=24 {
1000 let delta = curve
1001 .evaluate(start + (end - start) * index as f64 / 24.0)?
1002 .sub(origin);
1003 let radial = delta.dot(profile.radial);
1004 let axial = delta.dot(profile.axis);
1005 min_radial = min_radial.min(radial);
1006 max_radial = max_radial.max(radial);
1007 min_axis = min_axis.min(axial);
1008 max_axis = max_axis.max(axial);
1009 }
1010 }
1011 let padding = (max_radial - min_radial).max(max_axis - min_axis) * 0.05 + 1e-6;
1012 let width = max_radial - min_radial + 2.0 * padding;
1013 let height = max_axis - min_axis + 2.0 * padding;
1014 let start_origin = origin
1015 .add(profile.radial.scale(min_radial - padding))
1016 .add(profile.axis.scale(min_axis - padding));
1017 let mut start_coedges = Vec::with_capacity(count);
1018 for index in 0..count {
1019 start_coedges.push(CoedgeRecord {
1020 id: next_id(&mut counter),
1021 edge_id: start_edge_ids[index],
1022 forward: true,
1023 pcurve: curve_to_plane_parameters(
1024 &profile.curves[index],
1025 start_origin,
1026 profile.radial,
1027 profile.axis,
1028 )?,
1029 });
1030 }
1031 let start_loop_id = next_id(&mut counter);
1032 faces.push(FaceRecord {
1033 id: next_id(&mut counter),
1034 surface: make_plane(start_origin, profile.radial, profile.axis, width, height)?,
1035 same_sense: true,
1036 loops: vec![LoopRecord {
1037 id: start_loop_id,
1038 coedges: start_coedges,
1039 }],
1040 name: cap_names.first().cloned().flatten(),
1041 });
1042
1043 let end_radial = rotate_point(origin.add(profile.radial), origin, profile.axis, angle)
1044 .sub(origin)
1045 .normalized()?;
1046 let end_origin = origin
1047 .add(end_radial.scale(min_radial - padding))
1048 .add(profile.axis.scale(min_axis - padding));
1049 let mut end_coedges = Vec::with_capacity(count);
1050 for index in (0..count).rev() {
1051 end_coedges.push(CoedgeRecord {
1052 id: next_id(&mut counter),
1053 edge_id: end_edge_ids[index],
1054 forward: false,
1055 pcurve: curve_to_plane_parameters(
1056 &end_curves[index],
1057 end_origin,
1058 profile.axis,
1059 end_radial,
1060 )?
1061 .reversed()?,
1062 });
1063 }
1064 let end_loop_id = next_id(&mut counter);
1065 faces.push(FaceRecord {
1066 id: next_id(&mut counter),
1067 surface: make_plane(end_origin, profile.axis, end_radial, height, width)?,
1068 same_sense: true,
1069 loops: vec![LoopRecord {
1070 id: end_loop_id,
1071 coedges: end_coedges,
1072 }],
1073 name: cap_names.get(1).cloned().flatten(),
1074 });
1075
1076 let solid = BrepSolid {
1077 mass_properties_cache: Default::default(),
1078 cells: Default::default(),
1079 id: next_id(&mut counter),
1080 vertices,
1081 edges,
1082 shells: vec![ShellRecord {
1083 id: next_id(&mut counter),
1084 faces,
1085 }],
1086 genus: 0,
1087 kind: crate::BodyKind::Solid,
1088 };
1089 timing.lap("topology_pcurves");
1090 let issues = solid.validate();
1091 timing.lap("validation");
1092 if issues.is_empty() {
1093 Ok(solid)
1094 } else {
1095 Err(format!("Rust partial revolution is invalid: {issues:?}"))
1096 }
1097}
1098