Skip to main content

brep_kernel/geometry/analytic_surface/
revolution.rs

1use super::*;
2use super::recognition::{full_circle_knots, nearly_equal_points, surface_scale};
3
4/// Frame, generatrix, and sweep of any surface built by `make_revolution`
5/// (full or partial), validated by exact reconstruction. Unlike
6/// `AnalyticSurface` this does not require the generatrix to be a line or
7/// classic quadric profile.
8pub struct RevolutionStructure {
9    pub frame: RevolutionFrame,
10    pub generatrix: NurbsCurve,
11    pub sweep: f64,
12}
13
14pub fn circumcenter(a: Vec3, b: Vec3, c: Vec3) -> Option<Vec3> {
15    let ab = b.sub(a);
16    let ac = c.sub(a);
17    let normal = ab.cross(ac);
18    let normal_squared = normal.dot(normal);
19    if !normal_squared.is_finite() || normal_squared <= 1e-28 * ab.dot(ab) * ac.dot(ac) {
20        return None;
21    }
22    let offset = normal
23        .cross(ab)
24        .scale(ac.dot(ac))
25        .add(ac.cross(normal).scale(ab.dot(ab)))
26        .scale(1.0 / (2.0 * normal_squared));
27    Some(a.add(offset))
28}
29
30pub fn revolution_structure(surface: &NurbsSurface) -> Option<RevolutionStructure> {
31    if surface.degree_u != 2 {
32        return None;
33    }
34    let rows = &surface.control_points;
35    if rows.len() < 3 || rows.len() % 2 == 0 {
36        return None;
37    }
38    let spans = (rows.len() - 1) / 2;
39    if spans > 4 {
40        return None;
41    }
42    let expected = full_circle_knots(spans);
43    if surface.knots_u.len() != expected.len()
44        || surface
45            .knots_u
46            .iter()
47            .zip(&expected)
48            .any(|(a, b)| (a - b).abs() > 1e-12)
49    {
50        return None;
51    }
52    let scale = surface_scale(surface);
53    // The mid-row weight ratio encodes the per-span arc angle.
54    let mut sweep = None;
55    for column in 0..rows[0].len() {
56        let corner = rows[0][column].w;
57        let middle = rows[1][column].w;
58        if corner.abs() <= 1e-12 {
59            continue;
60        }
61        let ratio = middle / corner;
62        if !(ratio > 0.0 && ratio <= 1.0 + 1e-12) {
63            return None;
64        }
65        let arc_angle = 2.0 * ratio.min(1.0).acos();
66        if arc_angle <= 1e-9 {
67            return None;
68        }
69        sweep = Some(arc_angle * spans as f64);
70        break;
71    }
72    let sweep = sweep?;
73    if sweep > std::f64::consts::TAU + 1e-9 {
74        return None;
75    }
76    // Frame from three points on a radiused angular arc (0.4/0.8 avoid the
77    // closed-circle wraparound where evaluate(1.0) == evaluate(0.0)).
78    let mut frame = None;
79    for column in 0..rows[0].len() {
80        let arc_points: Vec<crate::Vec4> = rows.iter().map(|row| row[column]).collect();
81        let Ok(arc) = NurbsCurve::new(2, surface.knots_u.clone(), arc_points) else {
82            return None;
83        };
84        let p0 = arc.evaluate(0.0).ok()?;
85        let p1 = arc.evaluate(0.4).ok()?;
86        let p2 = arc.evaluate(0.8).ok()?;
87        let Some(center) = circumcenter(p0, p1, p2) else {
88            continue;
89        };
90        let radial = p0.sub(center);
91        let radius = radial.length();
92        if radius <= RECOGNITION_TOLERANCE * scale.max(1.0) {
93            continue;
94        }
95        let axis = radial.cross(p1.sub(center)).normalized().ok()?;
96        let x_axis = radial.scale(1.0 / radius);
97        let y_axis = axis.cross(x_axis);
98        frame = Some(RevolutionFrame {
99            origin: center,
100            axis,
101            x_axis,
102            y_axis,
103        });
104        break;
105    }
106    let frame = frame?;
107    let generatrix =
108        NurbsCurve::new(surface.degree_v, surface.knots_v.clone(), rows[0].clone()).ok()?;
109    let rebuilt = make_revolution(frame.origin, frame.axis, &generatrix, sweep).ok()?;
110    if !nearly_equal_points(surface, &rebuilt, scale) {
111        return None;
112    }
113    Some(RevolutionStructure {
114        frame,
115        generatrix,
116        sweep,
117    })
118}
119
120pub(super) fn rotate_curve_about_axis(
121    curve: &NurbsCurve,
122    origin: Vec3,
123    axis: Vec3,
124    angle: f64,
125) -> Option<NurbsCurve> {
126    let (sin, cos) = angle.sin_cos();
127    let controls = curve
128        .control_points
129        .iter()
130        .map(|control| {
131            let point = control.point().ok()?;
132            let d = point.sub(origin);
133            let axial = axis.scale(d.dot(axis));
134            let radial = d.sub(axial);
135            let ortho = axis.cross(radial);
136            let rotated = origin
137                .add(axial)
138                .add(radial.scale(cos))
139                .add(ortho.scale(sin));
140            Some(crate::Vec4::from_point(rotated, control.w))
141        })
142        .collect::<Option<Vec<_>>>()?;
143    NurbsCurve::new(curve.degree, curve.knots.clone(), controls).ok()
144}
145
146/// Exact intersection of two surfaces of revolution about a COMMON axis:
147/// by rotational symmetry the intersection is the revolution of the 2D
148/// profile intersections, i.e. circular arcs over the angular overlap.
149/// One exact curve×surface hit-test on a generatrix placed mid-overlap
150/// finds the (radius, height) pairs; empty hits prove the surfaces
151/// disjoint (skipping the marcher, which is ill-conditioned exactly here
152/// when the profiles touch tangentially).
153pub(super) fn intersect_coaxial_revolutions(
154    first: &NurbsSurface,
155    second: &NurbsSurface,
156    tolerance: f64,
157) -> Option<Vec<NurbsCurve>> {
158    let a = revolution_structure(first)?;
159    let b = revolution_structure(second)?;
160    let scale = surface_scale(first).max(surface_scale(second)).max(1.0);
161    let alignment = a.frame.axis.dot(b.frame.axis);
162    if alignment.abs() < 1.0 - 1e-9 {
163        return None;
164    }
165    let offset = b.frame.origin.sub(a.frame.origin);
166    let perpendicular = offset.sub(a.frame.axis.scale(offset.dot(a.frame.axis)));
167    if perpendicular.length() > 1e-9 * scale {
168        return None;
169    }
170    // Angular interval of `second` expressed in `first`'s frame, its start
171    // brought into [0, τ) first. `atan2` lands in (-π, π], so an anti-aligned
172    // full-sweep `second` read as (start − τ, start) began as low as −3π and
173    // the ±τ shifts below reached only three quarters of it: the hosted
174    // 2026-11-02 "boolean fails" reports' revolve caps (axis down) against a
175    // bore cylinder (axis up) imprinted a 270° arc and the bore never
176    // pierced the cap.
177    let b_start = b
178        .frame
179        .x_axis
180        .dot(a.frame.y_axis)
181        .atan2(b.frame.x_axis.dot(a.frame.x_axis));
182    let tau = std::f64::consts::TAU;
183    let (b_lo, b_hi) = if alignment > 0.0 {
184        (b_start, b_start + b.sweep)
185    } else {
186        (b_start - b.sweep, b_start)
187    };
188    let b_wrapped = b_lo.rem_euclid(tau);
189    let (b_lo, b_hi) = (b_wrapped, b_hi + (b_wrapped - b_lo));
190    let mut intervals = Vec::new();
191    for shift in [-tau, 0.0, tau] {
192        let lo = (b_lo + shift).max(0.0);
193        let hi = (b_hi + shift).min(a.sweep);
194        if hi - lo > 1e-9 {
195            intervals.push((lo, hi));
196        }
197    }
198    // The pieces of one overlap that the wrap cut apart are one arc again,
199    // so a full turn comes back as the one closed circle it is.
200    intervals.sort_by(|x, y| x.0.total_cmp(&y.0));
201    let mut merged: Vec<(f64, f64)> = Vec::new();
202    for (lo, hi) in intervals {
203        if let Some(last) = merged.last_mut() {
204            if lo <= last.1 + 1e-9 {
205                last.1 = last.1.max(hi);
206                continue;
207            }
208        }
209        merged.push((lo, hi));
210    }
211    let intervals = merged;
212    if intervals.is_empty() {
213        return Some(Vec::new());
214    }
215    let middle = (intervals[0].0 + intervals[0].1) / 2.0;
216    let generatrix = rotate_curve_about_axis(&a.generatrix, a.frame.origin, a.frame.axis, middle)?;
217    let hits = crate::intersect_curve_surface(&generatrix, second, tolerance).ok()?;
218    if hits.len() > 8 {
219        // A hit spray means the profiles overlap (coincident surfaces);
220        // leave that to the cosurface machinery.
221        return None;
222    }
223    let radius_floor = (tolerance * 10.0).max(1e-9 * scale);
224    let mut seen: Vec<(f64, f64)> = Vec::new();
225    let mut curves = Vec::new();
226    for hit in hits {
227        // TANGENT-CONTACT SKIP: a meridian hit whose curve tangent lies IN
228        // the other surface (hit.tangential — computed exactly by the
229        // curve×surface intersector, no projector involved) sweeps a CONTACT
230        // circle where the coaxial surfaces touch without crossing. Minting
231        // it duplicates the true section ring (the cap-plane circle) and
232        // strands one-use edges at assembly. A transversal meridian crossing
233        // keeps tangential=false and is unaffected. Escape hatch:
234        // BREP_COAXIAL_TANGENT_SKIP=0.
235        if hit.tangential
236            && std::env::var("BREP_COAXIAL_TANGENT_SKIP").as_deref() != Ok("0")
237        {
238            continue;
239        }
240        let (_, radius, axial) = a.frame.cylindrical(hit.point);
241        if radius <= radius_floor {
242            continue;
243        }
244        if seen
245            .iter()
246            .any(|(r, z)| (r - radius).abs() + (z - axial).abs() <= radius_floor)
247        {
248            continue;
249        }
250        seen.push((radius, axial));
251        for (lo, hi) in &intervals {
252            let center = a.frame.origin.add(a.frame.axis.scale(axial));
253            curves.push(
254                make_arc(center, a.frame.x_axis, a.frame.y_axis, radius, *lo, *hi).ok()?,
255            );
256        }
257    }
258    Some(curves)
259}