1use crate::{make_arc, make_revolution, NurbsCurve, NurbsSurface, SurfaceProjection, Vec3};
15
16const RECOGNITION_TOLERANCE: f64 = 1e-9;
17
18#[derive(Clone, Debug)]
19pub struct RevolutionFrame {
20 pub origin: Vec3,
22 pub axis: Vec3,
24 pub x_axis: Vec3,
26 pub y_axis: Vec3,
28}
29
30impl RevolutionFrame {
31 fn cylindrical(&self, point: Vec3) -> (Option<f64>, f64, f64) {
35 let d = point.sub(self.origin);
36 let axial = d.dot(self.axis);
37 let radial_vector = d.sub(self.axis.scale(axial));
38 let radius = radial_vector.length();
39 if radius <= 1e-14 * (1.0 + axial.abs()) {
40 return (None, radius, axial);
41 }
42 let mut theta = radial_vector
43 .dot(self.y_axis)
44 .atan2(radial_vector.dot(self.x_axis));
45 if theta < 0.0 {
46 theta += std::f64::consts::TAU;
47 }
48 (Some(theta), radius, axial)
49 }
50}
51
52#[derive(Clone, Debug)]
53pub enum AnalyticSurface {
54 Plane {
56 origin: Vec3,
57 u_dir: Vec3,
58 v_dir: Vec3,
59 u_domain: [f64; 2],
60 v_domain: [f64; 2],
61 },
62 RuledRevolution {
65 frame: RevolutionFrame,
66 rho0: f64,
67 rho1: f64,
68 height: f64,
69 },
70 Sphere { frame: RevolutionFrame, radius: f64 },
72 Torus {
74 frame: RevolutionFrame,
75 major_radius: f64,
76 minor_radius: f64,
77 },
78 Revolution {
88 frame: RevolutionFrame,
89 spans: usize,
90 sweep: f64,
91 generatrix: NurbsCurve,
92 },
93}
94
95impl AnalyticSurface {
96 pub fn kind_label(&self) -> &'static str {
106 match self {
107 AnalyticSurface::Plane { .. } => "Plane",
108 AnalyticSurface::RuledRevolution { rho0, rho1, .. } => {
109 let scale = rho0.abs().max(rho1.abs()).max(1.0);
110 if (rho0 - rho1).abs() <= RECOGNITION_TOLERANCE * scale {
111 "Cylinder"
112 } else {
113 "Cone"
114 }
115 }
116 AnalyticSurface::Sphere { .. } => "Sphere",
117 AnalyticSurface::Torus { .. } => "Torus",
118 AnalyticSurface::Revolution { .. } => "Surface of revolution",
119 }
120 }
121}
122
123pub fn circle_angle_to_parameter(spans: usize, sweep: f64, angle: f64) -> f64 {
128 let segment = sweep / spans as f64;
129 let clamped = angle.clamp(0.0, sweep);
130 let mut span = (clamped / segment).floor() as usize;
131 if span >= spans {
132 span = spans - 1;
133 }
134 let local = clamped - span as f64 * segment;
135 let half = (0.5 * local).tan();
139 let s = (0.5 * segment).sin();
140 let c = (0.5 * segment).cos();
141 let t = half / (s + half * (1.0 - c));
142 (span as f64 + t.clamp(0.0, 1.0)) / spans as f64
143}
144
145#[path = "analytic_surface/recognition.rs"]
146mod recognition;
147#[path = "analytic_surface/intersect.rs"]
148mod intersect;
149#[path = "analytic_surface/revolution.rs"]
150mod revolution;
151#[path = "analytic_surface/canonical.rs"]
152mod canonical;
153
154pub use intersect::{intersect_analytic_pair, plane_ruled_section_arc};
155pub(crate) use intersect::{intersect_analytic_pair_with, ruled_gate, ruled_gate_census, RuledGate};
156pub use recognition::recognize;
157pub(crate) use canonical::{canonical_carriers_enabled, canonicalise_carriers, CanonicalReport, CanonicalRewrite};
158pub use revolution::{revolution_structure, RevolutionStructure};
159pub use revolution::circumcenter;
160use recognition::generatrix_is_meridional_half_ray;
161
162impl AnalyticSurface {
163 pub fn project(&self, surface: &NurbsSurface, point: Vec3) -> Option<SurfaceProjection> {
167 let (u, v) = match self {
168 AnalyticSurface::Plane {
169 origin,
170 u_dir,
171 v_dir,
172 u_domain,
173 v_domain,
174 } => {
175 let d = point.sub(*origin);
178 let a = u_dir.dot(*u_dir);
179 let b = u_dir.dot(*v_dir);
180 let c = v_dir.dot(*v_dir);
181 let determinant = a * c - b * b;
182 if determinant.abs() <= 1e-16 * (a * c).max(1.0) {
183 return None;
184 }
185 let fu = u_dir.dot(d);
186 let fv = v_dir.dot(d);
187 let u =
188 ((fu * c - fv * b) / determinant + u_domain[0]).clamp(u_domain[0], u_domain[1]);
189 let v =
190 ((fv * a - fu * b) / determinant + v_domain[0]).clamp(v_domain[0], v_domain[1]);
191 (u, v)
192 }
193 AnalyticSurface::RuledRevolution {
194 frame,
195 rho0,
196 rho1,
197 height,
198 } => {
199 let (theta, rho, z) = frame.cylindrical(point);
200 let u = theta
201 .map(|angle| circle_angle_to_parameter(4, std::f64::consts::TAU, angle))
202 .unwrap_or(0.0);
203 let delta_rho = rho1 - rho0;
206 let length_squared = delta_rho * delta_rho + height * height;
207 let t = (((rho - rho0) * delta_rho + z * height) / length_squared).clamp(0.0, 1.0);
208 (u, t)
209 }
210 AnalyticSurface::Sphere { frame, radius: _ } => {
211 let (theta, rho, z) = frame.cylindrical(point);
212 let u = theta
213 .map(|angle| circle_angle_to_parameter(4, std::f64::consts::TAU, angle))
214 .unwrap_or(0.0);
215 let beta = rho.atan2(-z);
217 let v = circle_angle_to_parameter(2, std::f64::consts::PI, beta);
218 (u, v)
219 }
220 AnalyticSurface::Torus {
221 frame,
222 major_radius,
223 minor_radius: _,
224 } => {
225 let (theta, rho, z) = frame.cylindrical(point);
226 let u = theta
227 .map(|angle| circle_angle_to_parameter(4, std::f64::consts::TAU, angle))
228 .unwrap_or(0.0);
229 let mut psi = z.atan2(rho - major_radius);
230 if psi < 0.0 {
231 psi += std::f64::consts::TAU;
232 }
233 let v = circle_angle_to_parameter(4, std::f64::consts::TAU, psi);
234 (u, v)
235 }
236 AnalyticSurface::Revolution {
237 frame,
238 spans,
239 sweep,
240 generatrix,
241 } => {
242 if !generatrix_is_meridional_half_ray(generatrix, frame) {
243 return None;
244 }
245 let (theta, rho, z) = frame.cylindrical(point);
246 let full = *sweep >= std::f64::consts::TAU - 1e-9;
247 let point_at_angle = |angle: f64| {
251 let radial = frame
252 .x_axis
253 .scale(angle.cos())
254 .add(frame.y_axis.scale(angle.sin()));
255 frame.origin.add(radial.scale(rho)).add(frame.axis.scale(z))
256 };
257 let mut candidates: Vec<(f64, Vec3)> = Vec::with_capacity(2);
258 match theta {
259 None => candidates.push((0.0, point)),
260 Some(angle) if full || angle <= *sweep => {
261 candidates.push((
263 circle_angle_to_parameter(*spans, *sweep, angle),
264 point_at_angle(0.0),
265 ));
266 }
267 Some(angle) => {
268 candidates.push((0.0, point));
274 candidates.push((1.0, point_at_angle(angle - *sweep)));
275 }
276 }
277 let mut best: Option<(f64, f64, f64)> = None;
278 for (u, query) in candidates {
279 let Ok(projection) = crate::project_point_to_curve(generatrix, query) else {
280 return None;
281 };
282 if best
283 .map(|(_, _, distance)| projection.distance < distance)
284 .unwrap_or(true)
285 {
286 best = Some((u, projection.u, projection.distance));
287 }
288 }
289 let (u, v, _) = best?;
290 (u, v)
291 }
292 };
293 let projected = surface.evaluate(u, v).ok()?;
294 Some(SurfaceProjection {
295 u,
296 v,
297 point: projected,
298 distance: projected.sub(point).length(),
299 })
300 }
301
302 pub fn frame(&self) -> Option<&RevolutionFrame> {
305 match self {
306 AnalyticSurface::Plane { .. } => None,
307 AnalyticSurface::RuledRevolution { frame, .. }
308 | AnalyticSurface::Sphere { frame, .. }
309 | AnalyticSurface::Torus { frame, .. }
310 | AnalyticSurface::Revolution { frame, .. } => Some(frame),
311 }
312 }
313}
314
315pub(crate) use recognition::curve_scale;
316
317pub fn circumcenter_2d(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> Option<[f64; 2]> {
319 let scale = (b[0] - a[0]).abs().max((b[1] - a[1]).abs())
320 .max((c[0] - a[0]).abs()).max((c[1] - a[1]).abs());
321 let d = 2.0 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1]));
322 if !d.is_finite() || d.abs() <= 1e-12 * scale * scale { return None; }
323 let aa = a[0] * a[0] + a[1] * a[1];
324 let bb = b[0] * b[0] + b[1] * b[1];
325 let cc = c[0] * c[0] + c[1] * c[1];
326 Some([
327 (aa * (b[1] - c[1]) + bb * (c[1] - a[1]) + cc * (a[1] - b[1])) / d,
328 (aa * (c[0] - b[0]) + bb * (a[0] - c[0]) + cc * (b[0] - a[0])) / d,
329 ])
330}
331
332#[derive(Clone, Copy, Debug)]
333pub struct RecognizedCircle {
334 pub centre: Vec3,
335 pub axis: Vec3,
336 pub radius: f64,
337 pub worst: f64,
339}
340
341pub fn recognize_circle(curve: &NurbsCurve, t0: f64, t1: f64, tolerance: f64) -> Option<RecognizedCircle> {
343 if !(tolerance >= 0.0) || !t0.is_finite() || !t1.is_finite() || t0 == t1 {
344 return None;
345 }
346 let a = curve.evaluate(t0).ok()?;
347 let b = curve.evaluate(t0 + (t1 - t0) / 3.0).ok()?;
348 let c = curve.evaluate(t0 + 2.0 * (t1 - t0) / 3.0).ok()?;
349 let centre = circumcenter(a, b, c)?;
350 let normal = b.sub(a).cross(c.sub(a));
351 let axis = normal.scale(1.0 / normal.length());
352 let radius = a.sub(centre).length();
353 if !(radius > 0.0) || !radius.is_finite() { return None; }
354 let mut worst = 0.0_f64;
355 for i in 0..=64 {
356 let d = curve.evaluate(t0 + (t1 - t0) * i as f64 / 64.0).ok()?.sub(centre);
357 let error = (d.length() - radius).abs().max(d.dot(axis).abs());
358 if !error.is_finite() || error > tolerance { return None; }
359 worst = worst.max(error);
360 }
361 Some(RecognizedCircle { centre, axis, radius, worst })
362}
363