Skip to main content

brep_kernel/construction/
revolve_topology.rs

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    /// The axis points that lay ON the axis before the snap above (to 1e-12
71    /// of the profile's size): only these may be built as a pinch. A point
72    /// snapped from off the axis is a generatrix the revolve cannot hold that
73    /// close, and stays refused.
74    exact_axis_points: Vec<bool>,
75    axis_curves: Vec<bool>,
76    /// For each entry of `curves`, true when it is a straight segment lying
77    /// (within tolerance) in a plane PERPENDICULAR to the axis — a radial
78    /// generatrix.  Revolving one sweeps a flat disk/annulus sector, so the
79    /// partial-revolve side face is genuinely PLANAR and is built with a
80    /// `make_plane` carrier (not `make_revolution`); otherwise the shallow
81    /// "cone" carrier is misrecognized (conical, not planar) and its boolean
82    /// intersections fall to the marcher and shed slivers.
83    radial_curves: Vec<bool>,
84    radial: Vec3,
85    axis: Vec3,
86    /// For each entry of `curves`, the index of the INPUT curve it came
87    /// from.  Winding normalization reverses the loop and axis-lying
88    /// curves produce no side face, so callers cannot recover this mapping
89    /// from the emitted face order — side-face names must travel through
90    /// it (the extrude naming permutation bug, revolve edition).
91    input_indices: Vec<usize>,
92}
93
94/// A straight two-point segment: moving either end keeps it straight.
95fn is_segment(curve: &NurbsCurve) -> bool {
96    curve.degree == 1 && curve.control_points.len() == 2
97}
98
99/// True when the curve interpolates its first (`at_start`) or last control
100/// point, so that control point IS the curve's end.
101fn 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
120/// Move a clamped end to `target`, its weight kept.
121fn 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
129/// Make every junction of the closed chain exact. Where a straight segment
130/// meets a curve that is not one, the segment's end moves (it stays straight
131/// and the curve stays exact); otherwise the next curve's start moves onto
132/// the previous curve's end, the sketch chainer's convention. Junctions whose
133/// ends are not clamped are left as they are. Returns whether anything moved.
134fn 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
157/// A partial revolve builds a radial generatrix's side face on a plane
158/// perpendicular to the axis (`radial_plane_face`), while its edges are the
159/// profile's own curves. `is_straight_radial` admits a slope, so without this
160/// the plane sits between the segment's two axial coordinates and every edge
161/// of the face stands off it by up to half their difference. Each run of
162/// consecutive radial segments is given one axial coordinate: that of the
163/// run's end vertices whose outer neighbour is not a straight segment (moving
164/// such a vertex would bend the curve), their mean if there are two, and the
165/// mean over the run's vertices when every neighbour is straight. Returns
166/// whether anything moved.
167fn 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        // Vertex k of the run is the start of run[k]; the last is the end of the run.
192        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    // one closed curve (an ellipse, a periodic spline) is a loop: cut in two, it is a profile
241    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    // The closure test accepts a junction within TOLERANCE, and every builder
395    // below reads a junction from both sides: face i trims at curve i's end,
396    // face i+1 and the junction edge at curve i+1's start. A gap kept here is a
397    // trim gap along the whole junction rim, and the shell does not close.
398    // Hatch: `BREP_REVOLVE_WELD=0`.
399    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    // A radial generatrix is a straight segment whose direction is
407    // perpendicular to the axis: its two endpoints share an axial coordinate,
408    // so the revolved patch is a flat disk/annulus sector (a plane), not a
409    // cone.  Curves that lie ON the axis produce no side face and are excluded.
410    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    // Hatch: `BREP_REVOLVE_FLAT_RADIAL=0`.
424    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
442/// True when `curve` is a straight, unit-weight line segment whose direction is
443/// perpendicular to `axis` (a radial generatrix).  The threshold is a small
444/// relative slope (~0.057 deg from perpendicular): it captures sketch-solver
445/// noise on an intended flat cap without swallowing a genuine shallow cone.
446fn 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/// Build the PLANAR side face of a partial revolve whose generatrix is radial
477/// (perpendicular to the axis): a flat disk/annulus sector.  The face carrier
478/// is an affine `make_plane` — so it recognizes as a `Plane` (not a shallow
479/// "cone"), reads planar in the app, and its boolean intersections take the
480/// exact analytic path instead of the sliver-prone marcher.  The bounding
481/// loop reuses the same edges as the revolution branch; only the surface and
482/// its pcurves differ (the edges are projected into the plane's frame).
483#[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    // Preserve the revolution carrier's outward orientation: the plane's
539    // geometric normal is e1 x e2, so same_sense follows whether that agrees
540    // with the revolution's normal at the patch midpoint.
541    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
579/// Revolve with caller-supplied names.  `side_names` is aligned with the
580/// INPUT curves; the kernel carries it through winding normalization and
581/// axis-curve skips, which the caller cannot reconstruct from the emitted
582/// face order.  `cap_names` is `[start_cap, end_cap]` for partial
583/// revolutions.  Empty slices leave faces unnamed.
584pub 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
617/// `BREP_REVOLVE_DUMP` names a directory. Every revolve then writes its input
618/// profile, axis and angle, with the solid it built or its error, as
619/// `<pid>-<serial>.json`, so a probe can rebuild the same inputs on another
620/// kernel and read each shell's closure (`revolve_shell_probe dumped`).
621fn 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    // The pole a face ENDS on: its own junction's, except at a PINCH — an
693    // axis point with a revolved face on both sides (a horn torus's disc
694    // touching the axis) — where the two fans meet only at the point. One
695    // vertex and pole edge for both makes V-E+F-H odd and the body is
696    // refused; the face ending there gets its own vertex and pole edge at the
697    // same point, which is the manifold body (brep-bench geometry/horn-torus,
698    // chamfer/cylinder-rim-to-point). A profile without a pinch is unchanged.
699    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