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(crate) 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 <= 1e-30 {
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.
171    let b_start = b
172        .frame
173        .x_axis
174        .dot(a.frame.y_axis)
175        .atan2(b.frame.x_axis.dot(a.frame.x_axis));
176    let (b_lo, b_hi) = if alignment > 0.0 {
177        (b_start, b_start + b.sweep)
178    } else {
179        (b_start - b.sweep, b_start)
180    };
181    let tau = std::f64::consts::TAU;
182    let mut intervals = Vec::new();
183    for shift in [-tau, 0.0, tau] {
184        let lo = (b_lo + shift).max(0.0);
185        let hi = (b_hi + shift).min(a.sweep);
186        if hi - lo > 1e-9 {
187            intervals.push((lo, hi));
188        }
189    }
190    if intervals.is_empty() {
191        return Some(Vec::new());
192    }
193    let middle = (intervals[0].0 + intervals[0].1) / 2.0;
194    let generatrix = rotate_curve_about_axis(&a.generatrix, a.frame.origin, a.frame.axis, middle)?;
195    let hits = crate::intersect_curve_surface(&generatrix, second, tolerance).ok()?;
196    if hits.len() > 8 {
197        // A hit spray means the profiles overlap (coincident surfaces);
198        // leave that to the cosurface machinery.
199        return None;
200    }
201    let radius_floor = (tolerance * 10.0).max(1e-9 * scale);
202    let mut seen: Vec<(f64, f64)> = Vec::new();
203    let mut curves = Vec::new();
204    for hit in hits {
205        // TANGENT-CONTACT SKIP (problemInbox equator-tangent: sphere r=R
206        // inscribed in a cylinder r=R): a meridian hit whose curve tangent
207        // lies IN the other surface (hit.tangential — computed exactly by
208        // the curve×surface intersector, no projector involved) sweeps a
209        // CONTACT circle where the coaxial surfaces touch without crossing.
210        // Minting it duplicates the true section ring (the cap-plane circle)
211        // and strands one-use edges at assembly. A transversal meridian
212        // crossing keeps tangential=false and is unaffected. Escape hatch:
213        // BREP_COAXIAL_TANGENT_SKIP=0.
214        if hit.tangential
215            && std::env::var("BREP_COAXIAL_TANGENT_SKIP").as_deref() != Ok("0")
216        {
217            continue;
218        }
219        let (_, radius, axial) = a.frame.cylindrical(hit.point);
220        if radius <= radius_floor {
221            continue;
222        }
223        if seen
224            .iter()
225            .any(|(r, z)| (r - radius).abs() + (z - axial).abs() <= radius_floor)
226        {
227            continue;
228        }
229        seen.push((radius, axial));
230        for (lo, hi) in &intervals {
231            let center = a.frame.origin.add(a.frame.axis.scale(axial));
232            curves.push(
233                make_arc(center, a.frame.x_axis, a.frame.y_axis, radius, *lo, *hi).ok()?,
234            );
235        }
236    }
237    Some(curves)
238}