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