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