Skip to main content

neco_brep/
brep.rs

1//! B-Rep core data structures: Shell, Surface, Edge, Face, Curve3D.
2
3use crate::bezier::{de_casteljau_rational_2d, de_casteljau_rational_3d, interpolate_frame};
4use crate::vec3;
5
6pub type VertexId = usize;
7pub type EdgeId = usize;
8pub type FaceId = usize;
9
10const VERTEX_TOL: f64 = 1e-10;
11
12/// Surface type.
13#[derive(Debug, Clone)]
14pub enum Surface {
15    Plane {
16        origin: [f64; 3],
17        normal: [f64; 3],
18    },
19    Cylinder {
20        origin: [f64; 3],
21        axis: [f64; 3],
22        radius: f64,
23    },
24    Cone {
25        origin: [f64; 3],
26        axis: [f64; 3],
27        half_angle: f64,
28    },
29    Sphere {
30        center: [f64; 3],
31        radius: f64,
32    },
33    Ellipsoid {
34        center: [f64; 3],
35        rx: f64,
36        ry: f64,
37        rz: f64,
38    },
39    Torus {
40        center: [f64; 3],
41        axis: [f64; 3],
42        major_radius: f64,
43        minor_radius: f64,
44    },
45    SurfaceOfRevolution {
46        center: [f64; 3],
47        axis: [f64; 3],
48        /// Unit vector for theta=0 direction.
49        frame_u: [f64; 3],
50        /// Unit vector for theta=pi/2 direction.
51        frame_v: [f64; 3],
52        /// Profile curve: 2D rational Bezier in (r, z) on the meridional plane.
53        profile_control_points: Vec<[f64; 2]>,
54        profile_weights: Vec<f64>,
55        profile_degree: u32,
56        /// Number of Bezier spans in the profile.
57        n_profile_spans: u32,
58        /// Start angle (rad).
59        theta_start: f64,
60        /// Angular sweep (rad).
61        theta_range: f64,
62    },
63    SurfaceOfSweep {
64        /// Bezier-decomposed spine control points.
65        spine_control_points: Vec<[f64; 3]>,
66        spine_weights: Vec<f64>,
67        spine_degree: u32,
68        /// Cross-section profile (x, y) Bezier spans.
69        profile_control_points: Vec<[f64; 2]>,
70        profile_weights: Vec<f64>,
71        profile_degree: u32,
72        n_profile_spans: u32,
73        /// RMF frames at span endpoints: (normal, binormal, tangent).
74        frames: Vec<[[f64; 3]; 3]>,
75    },
76    NurbsSurface {
77        data: Box<neco_nurbs::NurbsSurface3D>,
78    },
79}
80
81impl Surface {
82    /// Evaluate surface point at parameter (u, v).
83    pub fn evaluate(&self, u: f64, v: f64) -> [f64; 3] {
84        match self {
85            Surface::Plane { origin, normal } => {
86                let n = vec3::normalized(*normal);
87                let up = if n[0].abs() < 0.9 {
88                    [1.0, 0.0, 0.0]
89                } else {
90                    [0.0, 1.0, 0.0]
91                };
92                let u_vec = vec3::normalized(vec3::cross(n, up));
93                let v_vec = vec3::cross(n, u_vec);
94                vec3::add(
95                    vec3::add(*origin, vec3::scale(u_vec, u)),
96                    vec3::scale(v_vec, v),
97                )
98            }
99            Surface::Cylinder {
100                origin,
101                axis,
102                radius,
103            } => {
104                let axis_len = vec3::length(*axis);
105                let axis_n = vec3::normalized(*axis);
106                let (bu, bv) = vec3::orthonormal_basis(*axis);
107                let h = v.clamp(0.0, axis_len);
108                vec3::add(
109                    vec3::add(*origin, vec3::scale(axis_n, h)),
110                    vec3::add(
111                        vec3::scale(bu, radius * u.cos()),
112                        vec3::scale(bv, radius * u.sin()),
113                    ),
114                )
115            }
116            Surface::Cone {
117                origin,
118                axis,
119                half_angle,
120            } => {
121                let axis_len = vec3::length(*axis);
122                let axis_n = vec3::normalized(*axis);
123                let (bu, bv) = vec3::orthonormal_basis(*axis);
124                let t = v.clamp(0.0, axis_len);
125                let r = t * half_angle.tan();
126                vec3::add(
127                    vec3::add(*origin, vec3::scale(axis_n, t)),
128                    vec3::add(vec3::scale(bu, r * u.cos()), vec3::scale(bv, r * u.sin())),
129                )
130            }
131            Surface::Sphere { center, radius } => {
132                let sin_v = v.sin();
133                let cos_v = v.cos();
134                [
135                    center[0] + radius * sin_v * u.cos(),
136                    center[1] + radius * sin_v * u.sin(),
137                    center[2] + radius * cos_v,
138                ]
139            }
140            Surface::Ellipsoid { center, rx, ry, rz } => {
141                let sin_v = v.sin();
142                let cos_v = v.cos();
143                [
144                    center[0] + rx * sin_v * u.cos(),
145                    center[1] + ry * sin_v * u.sin(),
146                    center[2] + rz * cos_v,
147                ]
148            }
149            Surface::Torus {
150                center,
151                axis,
152                major_radius,
153                minor_radius,
154            } => {
155                let axis_n = vec3::normalized(*axis);
156                let (bu, bv) = vec3::orthonormal_basis(*axis);
157                let r = major_radius + minor_radius * v.cos();
158                let dir = vec3::add(vec3::scale(bu, u.cos()), vec3::scale(bv, u.sin()));
159                vec3::add(
160                    vec3::add(*center, vec3::scale(dir, r)),
161                    vec3::scale(axis_n, minor_radius * v.sin()),
162                )
163            }
164            Surface::SurfaceOfRevolution {
165                center,
166                axis,
167                frame_u,
168                frame_v,
169                profile_control_points,
170                profile_weights,
171                profile_degree,
172                n_profile_spans,
173                ..
174            } => {
175                let (r, z) = eval_revolution_profile(
176                    profile_control_points,
177                    profile_weights,
178                    *profile_degree,
179                    *n_profile_spans,
180                    v,
181                );
182                let axis_n = vec3::normalized(*axis);
183                let bu = vec3::normalized(*frame_u);
184                let bv = vec3::normalized(*frame_v);
185                let theta = u;
186                vec3::add(
187                    vec3::add(
188                        *center,
189                        vec3::add(
190                            vec3::scale(bu, r * theta.cos()),
191                            vec3::scale(bv, r * theta.sin()),
192                        ),
193                    ),
194                    vec3::scale(axis_n, z),
195                )
196            }
197            Surface::SurfaceOfSweep {
198                spine_control_points,
199                spine_weights,
200                spine_degree,
201                profile_control_points,
202                profile_weights,
203                profile_degree,
204                n_profile_spans,
205                frames,
206            } => {
207                let deg = *spine_degree as usize;
208                let n_spine_spans = if deg > 0 {
209                    (spine_control_points.len() - 1) / deg
210                } else {
211                    1
212                };
213
214                let (spine_pt, normal, binormal) = if n_spine_spans <= 1 {
215                    let pt = de_casteljau_rational_3d(spine_control_points, spine_weights, u);
216                    let (n, b) = interpolate_frame(frames, u);
217                    (pt, n, b)
218                } else {
219                    let u_clamped = u.clamp(0.0, 1.0);
220                    let span_f = u_clamped * n_spine_spans as f64;
221                    let span_idx = (span_f as usize).min(n_spine_spans - 1);
222                    let local_u = span_f - span_idx as f64;
223                    let start_cp = span_idx * deg;
224                    let end_cp = start_cp + deg + 1;
225                    let pt = de_casteljau_rational_3d(
226                        &spine_control_points[start_cp..end_cp],
227                        &spine_weights[start_cp..end_cp],
228                        local_u,
229                    );
230                    let (n, b) = interpolate_frame(&frames[span_idx..span_idx + 2], local_u);
231                    (pt, n, b)
232                };
233
234                let n_prof = *n_profile_spans as usize;
235                let p_deg = *profile_degree as usize;
236                let cps_per_span = p_deg + 1;
237                let (px, py) = if n_prof <= 1 {
238                    de_casteljau_rational_2d(profile_control_points, profile_weights, v)
239                } else {
240                    let v_clamped = v.clamp(0.0, 1.0);
241                    let span_f = v_clamped * n_prof as f64;
242                    let span_idx = (span_f as usize).min(n_prof - 1);
243                    let local_v = span_f - span_idx as f64;
244                    let start = span_idx * cps_per_span;
245                    let end = start + cps_per_span;
246                    de_casteljau_rational_2d(
247                        &profile_control_points[start..end],
248                        &profile_weights[start..end],
249                        local_v,
250                    )
251                };
252
253                [
254                    spine_pt[0] + px * normal[0] + py * binormal[0],
255                    spine_pt[1] + px * normal[1] + py * binormal[1],
256                    spine_pt[2] + px * normal[2] + py * binormal[2],
257                ]
258            }
259            Surface::NurbsSurface { data } => data.evaluate(u, v),
260        }
261    }
262
263    /// Outward unit normal at parameter (u, v).
264    pub fn normal_at(&self, u: f64, v: f64) -> [f64; 3] {
265        match self {
266            Surface::Plane { normal, .. } => vec3::normalized(*normal),
267            Surface::Cylinder { axis, .. } => {
268                let (bu, bv) = vec3::orthonormal_basis(*axis);
269                vec3::normalized(vec3::add(
270                    vec3::scale(bu, u.cos()),
271                    vec3::scale(bv, u.sin()),
272                ))
273            }
274            Surface::Cone {
275                axis, half_angle, ..
276            } => {
277                let (bu, bv) = vec3::orthonormal_basis(*axis);
278                let cos_ha = half_angle.cos();
279                let sin_ha = half_angle.sin();
280                let a = vec3::normalized(*axis);
281                let radial = vec3::add(vec3::scale(bu, u.cos()), vec3::scale(bv, u.sin()));
282                vec3::normalized(vec3::add(
283                    vec3::scale(radial, cos_ha),
284                    vec3::scale(a, -sin_ha),
285                ))
286            }
287            Surface::Sphere { center, radius } => {
288                let p = self.evaluate(u, v);
289                [
290                    (p[0] - center[0]) / radius,
291                    (p[1] - center[1]) / radius,
292                    (p[2] - center[2]) / radius,
293                ]
294            }
295            Surface::Ellipsoid { center, rx, ry, rz } => {
296                let p = self.evaluate(u, v);
297                let nx = (p[0] - center[0]) / (rx * rx);
298                let ny = (p[1] - center[1]) / (ry * ry);
299                let nz = (p[2] - center[2]) / (rz * rz);
300                let len = (nx * nx + ny * ny + nz * nz).sqrt();
301                if len < 1e-30 {
302                    [0.0, 0.0, 1.0]
303                } else {
304                    [nx / len, ny / len, nz / len]
305                }
306            }
307            Surface::Torus {
308                center,
309                axis,
310                major_radius,
311                ..
312            } => {
313                let (bu, bv) = vec3::orthonormal_basis(*axis);
314                let p = self.evaluate(u, v);
315                let dir = vec3::add(vec3::scale(bu, u.cos()), vec3::scale(bv, u.sin()));
316                let ring_center = vec3::add(*center, vec3::scale(dir, *major_radius));
317                vec3::normalized(vec3::sub(p, ring_center))
318            }
319            Surface::SurfaceOfRevolution {
320                axis,
321                frame_u,
322                frame_v,
323                profile_control_points,
324                profile_weights,
325                profile_degree,
326                n_profile_spans,
327                ..
328            } => {
329                let (r, _z) = eval_revolution_profile(
330                    profile_control_points,
331                    profile_weights,
332                    *profile_degree,
333                    *n_profile_spans,
334                    v,
335                );
336                let axis_n = vec3::normalized(*axis);
337                let bu = vec3::normalized(*frame_u);
338                let bv = vec3::normalized(*frame_v);
339                let theta = u;
340                let cos_t = theta.cos();
341                let sin_t = theta.sin();
342
343                let ds_du = vec3::add(vec3::scale(bu, -r * sin_t), vec3::scale(bv, r * cos_t));
344
345                let eps = 1e-6;
346                let v_lo = (v - eps).max(0.0);
347                let v_hi = (v + eps).min(1.0);
348                let (r_lo, z_lo) = eval_revolution_profile(
349                    profile_control_points,
350                    profile_weights,
351                    *profile_degree,
352                    *n_profile_spans,
353                    v_lo,
354                );
355                let (r_hi, z_hi) = eval_revolution_profile(
356                    profile_control_points,
357                    profile_weights,
358                    *profile_degree,
359                    *n_profile_spans,
360                    v_hi,
361                );
362                let dt = v_hi - v_lo;
363                let dr = (r_hi - r_lo) / dt;
364                let dz = (z_hi - z_lo) / dt;
365                let ds_dv = vec3::add(
366                    vec3::add(vec3::scale(bu, dr * cos_t), vec3::scale(bv, dr * sin_t)),
367                    vec3::scale(axis_n, dz),
368                );
369
370                let normal = vec3::cross(ds_du, ds_dv);
371                let len = vec3::length(normal);
372                if len < 1e-30 {
373                    axis_n
374                } else {
375                    vec3::scale(normal, 1.0 / len)
376                }
377            }
378            Surface::SurfaceOfSweep { .. } => {
379                let eps = 1e-6;
380                let u_lo = (u - eps).max(0.0);
381                let u_hi = (u + eps).min(1.0);
382                let v_lo = (v - eps).max(0.0);
383                let v_hi = (v + eps).min(1.0);
384                let du = vec3::sub(self.evaluate(u_hi, v), self.evaluate(u_lo, v));
385                let dv = vec3::sub(self.evaluate(u, v_hi), self.evaluate(u, v_lo));
386                let n = vec3::cross(du, dv);
387                let len = vec3::length(n);
388                if len < 1e-12 {
389                    [0.0, 0.0, 1.0]
390                } else {
391                    vec3::scale(n, 1.0 / len)
392                }
393            }
394            Surface::NurbsSurface { data } => data.normal(u, v),
395        }
396    }
397
398    /// Convert SurfaceOfSweep/Revolution to NurbsSurface3D.
399    pub fn to_nurbs_surface(&self) -> Option<neco_nurbs::NurbsSurface3D> {
400        match self {
401            Surface::SurfaceOfSweep {
402                spine_control_points,
403                spine_weights,
404                spine_degree,
405                profile_control_points,
406                profile_weights,
407                profile_degree,
408                n_profile_spans,
409                frames,
410            } => {
411                let s_deg = *spine_degree as usize;
412                let n_spine_spans = if s_deg > 0 {
413                    (spine_control_points.len() - 1) / s_deg
414                } else {
415                    1
416                };
417                let n_prof = *n_profile_spans as usize;
418
419                let (v_degree, prof_cps, prof_ws) = if n_prof <= 1 {
420                    let deg = profile_control_points.len() - 1;
421                    (deg, profile_control_points.clone(), profile_weights.clone())
422                } else {
423                    let p_deg = *profile_degree as usize;
424                    let cps_per_span = p_deg + 1;
425                    let mut cps = Vec::new();
426                    let mut ws = Vec::new();
427                    for span_j in 0..n_prof {
428                        let base = span_j * cps_per_span;
429                        let start_k = if span_j == 0 { 0 } else { 1 };
430                        for k in start_k..cps_per_span {
431                            cps.push(profile_control_points[base + k]);
432                            ws.push(profile_weights[base + k]);
433                        }
434                    }
435                    (p_deg, cps, ws)
436                };
437
438                let n_spine_pts = n_spine_spans + 1;
439                let mut spine_pts = Vec::with_capacity(n_spine_pts);
440                let mut spine_wts = Vec::with_capacity(n_spine_pts);
441                let mut normals = Vec::with_capacity(n_spine_pts);
442                let mut binormals = Vec::with_capacity(n_spine_pts);
443
444                for si in 0..n_spine_pts {
445                    let pt = if n_spine_spans <= 1 {
446                        let t = si as f64 / n_spine_spans.max(1) as f64;
447                        de_casteljau_rational_3d(spine_control_points, spine_weights, t)
448                    } else if si < n_spine_spans {
449                        spine_control_points[si * s_deg]
450                    } else {
451                        spine_control_points[spine_control_points.len() - 1]
452                    };
453                    let w = if n_spine_spans <= 1 {
454                        if si == 0 {
455                            spine_weights[0]
456                        } else {
457                            spine_weights[spine_weights.len() - 1]
458                        }
459                    } else if si < n_spine_spans {
460                        spine_weights[si * s_deg]
461                    } else {
462                        spine_weights[spine_weights.len() - 1]
463                    };
464
465                    let n = frames[si][0];
466                    let b = frames[si][1];
467
468                    spine_pts.push(pt);
469                    spine_wts.push(w);
470                    normals.push(n);
471                    binormals.push(b);
472                }
473
474                let n_v_cps = prof_cps.len();
475                let mut rows_cp: Vec<Vec<[f64; 3]>> = Vec::with_capacity(n_spine_pts);
476                let mut rows_w: Vec<Vec<f64>> = Vec::with_capacity(n_spine_pts);
477
478                for i in 0..n_spine_pts {
479                    let sp = spine_pts[i];
480                    let sw = spine_wts[i];
481                    let n = &normals[i];
482                    let b = &binormals[i];
483
484                    let mut row_cp = Vec::with_capacity(n_v_cps);
485                    let mut row_w = Vec::with_capacity(n_v_cps);
486
487                    for j in 0..n_v_cps {
488                        let [px, py] = prof_cps[j];
489                        let pw = prof_ws[j];
490                        let cp = [
491                            sp[0] + px * n[0] + py * b[0],
492                            sp[1] + px * n[1] + py * b[1],
493                            sp[2] + px * n[2] + py * b[2],
494                        ];
495                        row_cp.push(cp);
496                        row_w.push(sw * pw);
497                    }
498
499                    rows_cp.push(row_cp);
500                    rows_w.push(row_w);
501                }
502
503                let mut knots_u = vec![0.0; 2];
504                for i in 1..n_spine_spans {
505                    let t = i as f64 / n_spine_spans as f64;
506                    knots_u.push(t);
507                }
508                knots_u.extend(vec![1.0; 2]);
509
510                let knots_v = if n_prof <= 1 {
511                    let mut kv = vec![0.0; v_degree + 1];
512                    kv.extend(vec![1.0; v_degree + 1]);
513                    kv
514                } else {
515                    let p_deg = *profile_degree as usize;
516                    let mut kv = vec![0.0; p_deg + 1];
517                    for i in 1..n_prof {
518                        let t = i as f64 / n_prof as f64;
519                        for _ in 0..p_deg {
520                            kv.push(t);
521                        }
522                    }
523                    kv.extend(vec![1.0; p_deg + 1]);
524                    kv
525                };
526
527                Some(neco_nurbs::NurbsSurface3D {
528                    degree_u: 1,
529                    degree_v: v_degree,
530                    control_points: rows_cp,
531                    weights: rows_w,
532                    knots_u,
533                    knots_v,
534                })
535            }
536            Surface::SurfaceOfRevolution {
537                center,
538                axis,
539                frame_u,
540                frame_v,
541                profile_control_points,
542                profile_weights,
543                profile_degree,
544                n_profile_spans,
545                theta_start,
546                theta_range,
547            } => {
548                use std::f64::consts::FRAC_PI_2;
549
550                let axis_n = vec3::normalized(*axis);
551                let bu = vec3::normalized(*frame_u);
552                let bv = vec3::normalized(*frame_v);
553
554                let n_u_spans = (*theta_range / FRAC_PI_2).ceil().max(1.0) as usize;
555                let span_angle = *theta_range / n_u_spans as f64;
556                let half_angle = span_angle / 2.0;
557                let w_mid = half_angle.cos();
558
559                let n_u_cps = 2 * n_u_spans + 1;
560                let mut u_cos = Vec::with_capacity(n_u_cps);
561                let mut u_sin = Vec::with_capacity(n_u_cps);
562                let mut u_weights = Vec::with_capacity(n_u_cps);
563
564                for span_i in 0..n_u_spans {
565                    let theta0 = *theta_start + span_i as f64 * span_angle;
566                    let theta_mid = theta0 + half_angle;
567                    let theta1 = theta0 + span_angle;
568
569                    if span_i == 0 {
570                        u_cos.push(theta0.cos());
571                        u_sin.push(theta0.sin());
572                        u_weights.push(1.0);
573                    }
574
575                    u_cos.push(theta_mid.cos() / w_mid);
576                    u_sin.push(theta_mid.sin() / w_mid);
577                    u_weights.push(w_mid);
578
579                    u_cos.push(theta1.cos());
580                    u_sin.push(theta1.sin());
581                    u_weights.push(1.0);
582                }
583
584                let mut knots_u = vec![*theta_start; 3];
585                for i in 1..n_u_spans {
586                    let t = *theta_start + i as f64 * span_angle;
587                    knots_u.push(t);
588                    knots_u.push(t);
589                }
590                let theta_end = *theta_start + *theta_range;
591                knots_u.extend(vec![theta_end; 3]);
592
593                let n_prof = *n_profile_spans as usize;
594                let (v_degree, prof_cps, prof_ws) = if n_prof <= 1 {
595                    let deg = profile_control_points.len() - 1;
596                    (deg, profile_control_points.clone(), profile_weights.clone())
597                } else {
598                    let p_deg = *profile_degree as usize;
599                    let cps_per_span = p_deg + 1;
600                    let mut cps = Vec::new();
601                    let mut ws = Vec::new();
602                    for span_j in 0..n_prof {
603                        let base = span_j * cps_per_span;
604                        let start_k = if span_j == 0 { 0 } else { 1 };
605                        for k in start_k..cps_per_span {
606                            cps.push(profile_control_points[base + k]);
607                            ws.push(profile_weights[base + k]);
608                        }
609                    }
610                    (p_deg, cps, ws)
611                };
612
613                let knots_v = if n_prof <= 1 {
614                    let mut kv = vec![0.0; v_degree + 1];
615                    kv.extend(vec![1.0; v_degree + 1]);
616                    kv
617                } else {
618                    let p_deg = *profile_degree as usize;
619                    let mut kv = vec![0.0; p_deg + 1];
620                    for i in 1..n_prof {
621                        let t = i as f64 / n_prof as f64;
622                        for _ in 0..p_deg {
623                            kv.push(t);
624                        }
625                    }
626                    kv.extend(vec![1.0; p_deg + 1]);
627                    kv
628                };
629
630                let n_v_cps = prof_cps.len();
631                let mut rows_cp: Vec<Vec<[f64; 3]>> = Vec::with_capacity(n_u_cps);
632                let mut rows_w: Vec<Vec<f64>> = Vec::with_capacity(n_u_cps);
633
634                for ui in 0..n_u_cps {
635                    let c = u_cos[ui];
636                    let s = u_sin[ui];
637                    let uw = u_weights[ui];
638
639                    let mut row_cp = Vec::with_capacity(n_v_cps);
640                    let mut row_w = Vec::with_capacity(n_v_cps);
641
642                    for vj in 0..n_v_cps {
643                        let [r, z] = prof_cps[vj];
644                        let pw = prof_ws[vj];
645                        let cp = [
646                            center[0] + r * (c * bu[0] + s * bv[0]) + z * axis_n[0],
647                            center[1] + r * (c * bu[1] + s * bv[1]) + z * axis_n[1],
648                            center[2] + r * (c * bu[2] + s * bv[2]) + z * axis_n[2],
649                        ];
650                        row_cp.push(cp);
651                        row_w.push(uw * pw);
652                    }
653
654                    rows_cp.push(row_cp);
655                    rows_w.push(row_w);
656                }
657
658                Some(neco_nurbs::NurbsSurface3D {
659                    degree_u: 2,
660                    degree_v: v_degree,
661                    control_points: rows_cp,
662                    weights: rows_w,
663                    knots_u,
664                    knots_v,
665                })
666            }
667            _ => None,
668        }
669    }
670
671    /// Whether the surface supports analytical impostor rendering.
672    pub fn is_analytical(&self) -> bool {
673        !matches!(self, Surface::NurbsSurface { .. })
674    }
675
676    /// Default parameter range (u_min, u_max, v_min, v_max).
677    pub fn param_range(&self) -> (f64, f64, f64, f64) {
678        use std::f64::consts::{PI, TAU};
679        match self {
680            Surface::Plane { .. } => (0.0, 1.0, 0.0, 1.0),
681            Surface::Cylinder { axis, .. } => (0.0, TAU, 0.0, vec3::length(*axis)),
682            Surface::Cone { axis, .. } => (0.0, TAU, 0.0, vec3::length(*axis)),
683            Surface::Sphere { .. } => (0.0, TAU, 0.0, PI),
684            Surface::Ellipsoid { .. } => (0.0, TAU, 0.0, PI),
685            Surface::Torus { .. } => (0.0, TAU, 0.0, TAU),
686            Surface::SurfaceOfRevolution {
687                theta_start,
688                theta_range,
689                ..
690            } => (*theta_start, *theta_start + *theta_range, 0.0, 1.0),
691            Surface::SurfaceOfSweep { .. } => (0.0, 1.0, 0.0, 1.0),
692            Surface::NurbsSurface { data } => {
693                let (u0, u1) = data.u_range();
694                let (v0, v1) = data.v_range();
695                (u0, u1, v0, v1)
696            }
697        }
698    }
699
700    /// Inverse-project a 3D point to surface parameters.
701    pub fn inverse_project(&self, p: &[f64; 3]) -> Option<(f64, f64)> {
702        match self {
703            Surface::Plane { origin, normal } => {
704                let n = vec3::normalized(*normal);
705                let up = if n[0].abs() < 0.9 {
706                    [1.0, 0.0, 0.0]
707                } else {
708                    [0.0, 1.0, 0.0]
709                };
710                let u_vec = vec3::normalized(vec3::cross(n, up));
711                let v_vec = vec3::cross(n, u_vec);
712                let d = vec3::sub(*p, *origin);
713                Some((vec3::dot(d, u_vec), vec3::dot(d, v_vec)))
714            }
715            Surface::Cylinder { origin, axis, .. } => {
716                let axis_len = vec3::length(*axis);
717                if axis_len < 1e-30 {
718                    return None;
719                }
720                let axis_n = vec3::normalized(*axis);
721                let (bu, bv) = vec3::orthonormal_basis(*axis);
722                let q = vec3::sub(*p, *origin);
723                let h = vec3::dot(q, axis_n).clamp(0.0, axis_len);
724                let q_perp = vec3::sub(q, vec3::scale(axis_n, h));
725                let u = vec3::dot(q_perp, bv)
726                    .atan2(vec3::dot(q_perp, bu))
727                    .rem_euclid(std::f64::consts::TAU);
728                Some((u, h))
729            }
730            Surface::Cone { origin, axis, .. } => {
731                let axis_len = vec3::length(*axis);
732                if axis_len < 1e-30 {
733                    return None;
734                }
735                let axis_n = vec3::normalized(*axis);
736                let (bu, bv) = vec3::orthonormal_basis(*axis);
737                let q = vec3::sub(*p, *origin);
738                let h = vec3::dot(q, axis_n).clamp(0.0, axis_len);
739                let q_perp = vec3::sub(q, vec3::scale(axis_n, h));
740                let u = vec3::dot(q_perp, bv)
741                    .atan2(vec3::dot(q_perp, bu))
742                    .rem_euclid(std::f64::consts::TAU);
743                Some((u, h))
744            }
745            Surface::Sphere { center, radius } => {
746                let d = vec3::sub(*p, *center);
747                let len = vec3::length(d);
748                if len < 1e-30 || radius.abs() < 1e-30 {
749                    return None;
750                }
751                let u = d[1].atan2(d[0]).rem_euclid(std::f64::consts::TAU);
752                let cos_v = (d[2] / radius).clamp(-1.0, 1.0);
753                let v = cos_v.acos();
754                Some((u, v))
755            }
756            Surface::Ellipsoid { center, rx, ry, rz } => {
757                if rx.abs() < 1e-30 || ry.abs() < 1e-30 || rz.abs() < 1e-30 {
758                    return None;
759                }
760                let x = (p[0] - center[0]) / rx;
761                let y = (p[1] - center[1]) / ry;
762                let z = (p[2] - center[2]) / rz;
763                let u = y.atan2(x).rem_euclid(std::f64::consts::TAU);
764                let v = z.clamp(-1.0, 1.0).acos();
765                Some((u, v))
766            }
767            Surface::Torus {
768                center,
769                axis,
770                major_radius,
771                ..
772            } => {
773                let axis_n = vec3::normalized(*axis);
774                let (bu, bv) = vec3::orthonormal_basis(*axis);
775                let q = vec3::sub(*p, *center);
776                let axial = vec3::dot(q, axis_n);
777                let q_perp = vec3::sub(q, vec3::scale(axis_n, axial));
778                let r_perp = vec3::length(q_perp);
779                if r_perp < 1e-30 {
780                    return None;
781                }
782                let u = vec3::dot(q_perp, bv)
783                    .atan2(vec3::dot(q_perp, bu))
784                    .rem_euclid(std::f64::consts::TAU);
785                let v = axial
786                    .atan2(r_perp - major_radius)
787                    .rem_euclid(std::f64::consts::TAU);
788                Some((u, v))
789            }
790            Surface::SurfaceOfRevolution {
791                center,
792                axis,
793                frame_u,
794                frame_v,
795                profile_control_points,
796                profile_weights,
797                profile_degree,
798                n_profile_spans,
799                theta_start,
800                theta_range,
801            } => {
802                let axis_n = vec3::normalized(*axis);
803                let bu = vec3::normalized(*frame_u);
804                let bv = vec3::normalized(*frame_v);
805                let q = vec3::sub(*p, *center);
806                let theta_raw = vec3::dot(q, bv).atan2(vec3::dot(q, bu));
807                let theta = if *theta_range >= std::f64::consts::TAU - 1e-12 {
808                    let offset = (theta_raw - theta_start).rem_euclid(std::f64::consts::TAU);
809                    theta_start + offset
810                } else {
811                    let offset = (theta_raw - theta_start).rem_euclid(std::f64::consts::TAU);
812                    if offset <= *theta_range {
813                        theta_start + offset
814                    } else if offset.min(std::f64::consts::TAU - offset)
815                        <= (offset - theta_range).min(std::f64::consts::TAU - offset + theta_range)
816                    {
817                        *theta_start
818                    } else {
819                        *theta_start + *theta_range
820                    }
821                };
822                let radial_dir =
823                    vec3::add(vec3::scale(bu, theta.cos()), vec3::scale(bv, theta.sin()));
824                let rho = vec3::dot(q, radial_dir);
825                let z = vec3::dot(q, axis_n);
826                let v = find_closest_v_on_profile(
827                    rho,
828                    z,
829                    profile_control_points,
830                    profile_weights,
831                    *profile_degree,
832                    *n_profile_spans,
833                );
834                Some((theta, v))
835            }
836            Surface::SurfaceOfSweep { .. } => {
837                let (u0, u1, v0, v1) = self.param_range();
838                Some(grid_project_surface(self, p, u0, u1, v0, v1))
839            }
840            Surface::NurbsSurface { data } => {
841                let (u0, u1) = data.u_range();
842                let (v0, v1) = data.v_range();
843                Some(grid_project_surface(self, p, u0, u1, v0, v1))
844            }
845        }
846    }
847}
848
849fn grid_project_surface(
850    surface: &Surface,
851    p: &[f64; 3],
852    u0: f64,
853    u1: f64,
854    v0: f64,
855    v1: f64,
856) -> (f64, f64) {
857    let nu = 16;
858    let nv = 16;
859    let mut best_u = u0;
860    let mut best_v = v0;
861    let mut best_dist = f64::INFINITY;
862
863    for iu in 0..=nu {
864        for iv in 0..=nv {
865            let u = u0 + (u1 - u0) * (iu as f64 / nu as f64);
866            let v = v0 + (v1 - v0) * (iv as f64 / nv as f64);
867            let pt = surface.evaluate(u, v);
868            let d = vec3::distance(pt, *p);
869            if d < best_dist {
870                best_dist = d;
871                best_u = u;
872                best_v = v;
873            }
874        }
875    }
876
877    let eps = 1e-8;
878    for _ in 0..20 {
879        let pt = surface.evaluate(best_u, best_v);
880        let diff = vec3::sub(pt, *p);
881        if vec3::length(diff) < 1e-12 {
882            break;
883        }
884        let du = vec3::scale(
885            vec3::sub(
886                surface.evaluate(best_u + eps, best_v),
887                surface.evaluate(best_u - eps, best_v),
888            ),
889            0.5 / eps,
890        );
891        let dv = vec3::scale(
892            vec3::sub(
893                surface.evaluate(best_u, best_v + eps),
894                surface.evaluate(best_u, best_v - eps),
895            ),
896            0.5 / eps,
897        );
898
899        let a11 = vec3::dot(du, du);
900        let a12 = vec3::dot(du, dv);
901        let a22 = vec3::dot(dv, dv);
902        let b1 = -vec3::dot(du, diff);
903        let b2 = -vec3::dot(dv, diff);
904
905        let det = a11 * a22 - a12 * a12;
906        if det.abs() < 1e-30 {
907            break;
908        }
909
910        let delta_u = (a22 * b1 - a12 * b2) / det;
911        let delta_v = (a11 * b2 - a12 * b1) / det;
912        best_u = (best_u + delta_u).clamp(u0, u1);
913        best_v = (best_v + delta_v).clamp(v0, v1);
914    }
915
916    (best_u, best_v)
917}
918
919/// Evaluate multi-span rational Bezier profile.
920pub fn eval_revolution_profile(
921    profile_control_points: &[[f64; 2]],
922    profile_weights: &[f64],
923    profile_degree: u32,
924    n_profile_spans: u32,
925    v: f64,
926) -> (f64, f64) {
927    let n_prof = n_profile_spans as usize;
928    let p_deg = profile_degree as usize;
929    let cps_per_span = p_deg + 1;
930    if n_prof <= 1 {
931        de_casteljau_rational_2d(profile_control_points, profile_weights, v)
932    } else {
933        let v_clamped = v.clamp(0.0, 1.0);
934        let span_f = v_clamped * n_prof as f64;
935        let span_idx = (span_f as usize).min(n_prof - 1);
936        let local_v = span_f - span_idx as f64;
937        let start = span_idx * cps_per_span;
938        let end = start + cps_per_span;
939        de_casteljau_rational_2d(
940            &profile_control_points[start..end],
941            &profile_weights[start..end],
942            local_v,
943        )
944    }
945}
946
947/// Find the closest v parameter on the profile to (rho, z).
948pub fn find_closest_v_on_profile(
949    rho: f64,
950    z: f64,
951    profile_control_points: &[[f64; 2]],
952    profile_weights: &[f64],
953    profile_degree: u32,
954    n_profile_spans: u32,
955) -> f64 {
956    let n_prof = n_profile_spans as usize;
957    let n_search = if n_prof <= 1 { 32 } else { n_prof * 16 };
958    let mut best_v = 0.0;
959    let mut best_dist_sq = f64::INFINITY;
960
961    for i in 0..=n_search {
962        let v = i as f64 / n_search as f64;
963        let (r, zz) = eval_revolution_profile(
964            profile_control_points,
965            profile_weights,
966            profile_degree,
967            n_profile_spans,
968            v,
969        );
970        let dr = rho - r;
971        let dz = z - zz;
972        let d2 = dr * dr + dz * dz;
973        if d2 < best_dist_sq {
974            best_dist_sq = d2;
975            best_v = v;
976        }
977    }
978
979    let eps = 1e-8;
980    for _ in 0..20 {
981        let (r, zz) = eval_revolution_profile(
982            profile_control_points,
983            profile_weights,
984            profile_degree,
985            n_profile_spans,
986            best_v,
987        );
988        let (r_p, zz_p) = eval_revolution_profile(
989            profile_control_points,
990            profile_weights,
991            profile_degree,
992            n_profile_spans,
993            (best_v + eps).min(1.0),
994        );
995        let (r_m, zz_m) = eval_revolution_profile(
996            profile_control_points,
997            profile_weights,
998            profile_degree,
999            n_profile_spans,
1000            (best_v - eps).max(0.0),
1001        );
1002
1003        let dr_dv = (r_p - r_m) / (2.0 * eps);
1004        let dz_dv = (zz_p - zz_m) / (2.0 * eps);
1005
1006        let residual_r = rho - r;
1007        let residual_z = z - zz;
1008        let f_val = residual_r * (-dr_dv) + residual_z * (-dz_dv);
1009        let df_val = dr_dv * dr_dv + dz_dv * dz_dv;
1010
1011        if df_val.abs() < 1e-20 {
1012            break;
1013        }
1014
1015        let dv = f_val / df_val;
1016        best_v = (best_v - dv).clamp(0.0, 1.0);
1017
1018        if dv.abs() < 1e-12 {
1019            break;
1020        }
1021    }
1022
1023    best_v
1024}
1025
1026/// 3D curve.
1027#[derive(Debug, Clone)]
1028pub enum Curve3D {
1029    Line {
1030        start: [f64; 3],
1031        end: [f64; 3],
1032    },
1033    Arc {
1034        center: [f64; 3],
1035        axis: [f64; 3],
1036        start: [f64; 3],
1037        end: [f64; 3],
1038        radius: f64,
1039    },
1040    Ellipse {
1041        center: [f64; 3],
1042        axis_u: [f64; 3],
1043        axis_v: [f64; 3],
1044        t_start: f64,
1045        t_end: f64,
1046    },
1047    NurbsCurve3D {
1048        degree: usize,
1049        control_points: Vec<[f64; 3]>,
1050        weights: Vec<f64>,
1051        knots: Vec<f64>,
1052    },
1053}
1054
1055impl Curve3D {
1056    pub fn evaluate(&self, t: f64) -> [f64; 3] {
1057        match self {
1058            Curve3D::Line { start, end } => {
1059                vec3::add(*start, vec3::scale(vec3::sub(*end, *start), t))
1060            }
1061            Curve3D::Arc {
1062                center,
1063                axis,
1064                start,
1065                radius,
1066                ..
1067            } => {
1068                let r_vec = vec3::sub(*start, *center);
1069                let tangent = vec3::scale(vec3::normalized(vec3::cross(*axis, r_vec)), *radius);
1070                let cos_t = t.cos();
1071                let sin_t = t.sin();
1072                vec3::add(
1073                    vec3::add(*center, vec3::scale(r_vec, cos_t)),
1074                    vec3::scale(tangent, sin_t),
1075                )
1076            }
1077            Curve3D::Ellipse {
1078                center,
1079                axis_u,
1080                axis_v,
1081                ..
1082            } => vec3::add(
1083                vec3::add(*center, vec3::scale(*axis_u, t.cos())),
1084                vec3::scale(*axis_v, t.sin()),
1085            ),
1086            Curve3D::NurbsCurve3D {
1087                degree,
1088                control_points,
1089                weights,
1090                knots,
1091            } => {
1092                let n = control_points.len();
1093                let p = *degree;
1094                let t_min = knots[p];
1095                let t_max = knots[n];
1096                let t = t.clamp(t_min, t_max);
1097
1098                let k = {
1099                    if t >= knots[n] {
1100                        n - 1
1101                    } else {
1102                        let mut lo = p;
1103                        let mut hi = n;
1104                        while lo < hi {
1105                            let mid = (lo + hi) / 2;
1106                            if t < knots[mid] {
1107                                hi = mid;
1108                            } else {
1109                                lo = mid + 1;
1110                            }
1111                        }
1112                        lo - 1
1113                    }
1114                };
1115
1116                let mut hx = vec![0.0; p + 1];
1117                let mut hy = vec![0.0; p + 1];
1118                let mut hz = vec![0.0; p + 1];
1119                let mut hw = vec![0.0; p + 1];
1120
1121                for j in 0..=p {
1122                    let idx = k - p + j;
1123                    let w = weights[idx];
1124                    hx[j] = control_points[idx][0] * w;
1125                    hy[j] = control_points[idx][1] * w;
1126                    hz[j] = control_points[idx][2] * w;
1127                    hw[j] = w;
1128                }
1129
1130                for r in 1..=p {
1131                    for j in (r..=p).rev() {
1132                        let left = k + j - p;
1133                        let right = k + 1 + j - r;
1134                        let denom = knots[right] - knots[left];
1135                        if denom.abs() < 1e-30 {
1136                            continue;
1137                        }
1138                        let alpha = (t - knots[left]) / denom;
1139                        hx[j] = (1.0 - alpha) * hx[j - 1] + alpha * hx[j];
1140                        hy[j] = (1.0 - alpha) * hy[j - 1] + alpha * hy[j];
1141                        hz[j] = (1.0 - alpha) * hz[j - 1] + alpha * hz[j];
1142                        hw[j] = (1.0 - alpha) * hw[j - 1] + alpha * hw[j];
1143                    }
1144                }
1145
1146                let w = hw[p];
1147                if w.abs() < 1e-30 {
1148                    control_points.last().copied().unwrap_or([0.0; 3])
1149                } else {
1150                    [hx[p] / w, hy[p] / w, hz[p] / w]
1151                }
1152            }
1153        }
1154    }
1155
1156    pub fn midpoint(&self) -> [f64; 3] {
1157        match self {
1158            Curve3D::Line { start, end } => vec3::scale(vec3::add(*start, *end), 0.5),
1159            Curve3D::Arc { .. } => self.evaluate(0.5),
1160            Curve3D::Ellipse { t_start, t_end, .. } => self.evaluate((t_start + t_end) * 0.5),
1161            Curve3D::NurbsCurve3D { degree, knots, .. } => {
1162                let p = *degree;
1163                let n = knots.len() - p - 1;
1164                let mid = (knots[p] + knots[n]) * 0.5;
1165                self.evaluate(mid)
1166            }
1167        }
1168    }
1169
1170    /// Parameter range of the curve.
1171    pub fn param_range(&self) -> (f64, f64) {
1172        match self {
1173            Curve3D::Line { .. } => (0.0, 1.0),
1174            Curve3D::Arc {
1175                start,
1176                end,
1177                center,
1178                axis,
1179                radius,
1180            } => {
1181                let r_start = vec3::sub(*start, *center);
1182                let tangent = vec3::scale(vec3::normalized(vec3::cross(*axis, r_start)), *radius);
1183                let r_end = vec3::sub(*end, *center);
1184                let r_start_len = vec3::length(r_start);
1185                let r_end_len = vec3::length(r_end);
1186                let cos_angle = vec3::dot(r_start, r_end) / (r_start_len * r_end_len);
1187                let sin_angle = vec3::dot(tangent, r_end) / (vec3::length(tangent) * r_end_len);
1188                let angle = sin_angle.atan2(cos_angle);
1189                let angle = if angle.abs() < 1e-10 && vec3::length(vec3::sub(*start, *end)) < 1e-10
1190                {
1191                    std::f64::consts::TAU
1192                } else {
1193                    angle.max(0.0)
1194                };
1195                (0.0, angle)
1196            }
1197            Curve3D::Ellipse { t_start, t_end, .. } => (*t_start, *t_end),
1198            Curve3D::NurbsCurve3D { degree, knots, .. } => {
1199                let p = *degree;
1200                let n = knots.len() - p - 1;
1201                (knots[p], knots[n])
1202            }
1203        }
1204    }
1205
1206    /// Convert to polyline via adaptive sampling.
1207    pub fn to_polyline(&self, chord_tol: f64) -> Vec<[f64; 3]> {
1208        match self {
1209            Curve3D::Line { start, end } => vec![*start, *end],
1210            _ => {
1211                let (t0, t1) = self.param_range();
1212                let mut points = vec![self.evaluate(t0)];
1213                self.adaptive_sample(t0, t1, chord_tol, &mut points);
1214                points
1215            }
1216        }
1217    }
1218
1219    fn adaptive_sample(&self, t0: f64, t1: f64, tol: f64, points: &mut Vec<[f64; 3]>) {
1220        if (t1 - t0).abs() < 1e-10 {
1221            points.push(self.evaluate(t1));
1222            return;
1223        }
1224        let mid_t = (t0 + t1) * 0.5;
1225        let p0 = self.evaluate(t0);
1226        let p1 = self.evaluate(t1);
1227        let mid_curve = self.evaluate(mid_t);
1228        let mid_chord = vec3::scale(vec3::add(p0, p1), 0.5);
1229        let chord_height = vec3::length(vec3::sub(mid_curve, mid_chord));
1230        if chord_height > tol {
1231            self.adaptive_sample(t0, mid_t, tol, points);
1232            self.adaptive_sample(mid_t, t1, tol, points);
1233        } else {
1234            points.push(self.evaluate(t1));
1235        }
1236    }
1237}
1238
1239/// Oriented edge reference.
1240#[derive(Debug, Clone, Copy)]
1241pub struct EdgeRef {
1242    pub edge_id: EdgeId,
1243    pub forward: bool,
1244}
1245
1246/// Topological edge.
1247#[derive(Debug, Clone)]
1248pub struct Edge {
1249    pub v_start: VertexId,
1250    pub v_end: VertexId,
1251    pub curve: Curve3D,
1252}
1253
1254/// Face (loop of edge references + surface).
1255#[derive(Debug, Clone)]
1256pub struct Face {
1257    pub loop_edges: Vec<EdgeRef>,
1258    pub surface: Surface,
1259    pub orientation_reversed: bool,
1260}
1261
1262/// Sub-face: intermediate representation for boolean operations.
1263#[derive(Clone, Debug)]
1264pub struct SubFace {
1265    pub surface: Surface,
1266    pub polygon: Vec<[f64; 3]>,
1267    pub candidate_curves: Vec<Curve3D>,
1268    pub flipped: bool,
1269    pub source_shell: usize,
1270    pub source_face: usize,
1271}
1272
1273impl SubFace {
1274    pub fn centroid(&self) -> [f64; 3] {
1275        let n = self.polygon.len() as f64;
1276        let (sx, sy, sz) = self.polygon.iter().fold((0.0, 0.0, 0.0), |(x, y, z), p| {
1277            (x + p[0], y + p[1], z + p[2])
1278        });
1279        [sx / n, sy / n, sz / n]
1280    }
1281}
1282
1283/// B-Rep shell.
1284#[derive(Debug, Clone, Default)]
1285pub struct Shell {
1286    pub vertices: Vec<[f64; 3]>,
1287    pub edges: Vec<Edge>,
1288    pub faces: Vec<Face>,
1289}
1290
1291impl Shell {
1292    pub fn new() -> Self {
1293        Self {
1294            vertices: Vec::new(),
1295            edges: Vec::new(),
1296            faces: Vec::new(),
1297        }
1298    }
1299
1300    /// Add vertex, reusing an existing one within tolerance.
1301    pub fn add_vertex(&mut self, p: [f64; 3]) -> VertexId {
1302        for (i, v) in self.vertices.iter().enumerate() {
1303            if vec3::distance(*v, p) < VERTEX_TOL {
1304                return i;
1305            }
1306        }
1307        let id = self.vertices.len();
1308        self.vertices.push(p);
1309        id
1310    }
1311
1312    /// Add an edge.
1313    pub fn add_edge(&mut self, v_start: VertexId, v_end: VertexId, curve: Curve3D) -> EdgeId {
1314        let id = self.edges.len();
1315        self.edges.push(Edge {
1316            v_start,
1317            v_end,
1318            curve,
1319        });
1320        id
1321    }
1322
1323    /// Bounding box enclosing all vertices and sampled curve edges.
1324    pub fn bounding_box(&self) -> ([f64; 3], [f64; 3]) {
1325        let mut min = [f64::INFINITY; 3];
1326        let mut max = [f64::NEG_INFINITY; 3];
1327
1328        let mut extend = |p: [f64; 3]| {
1329            for i in 0..3 {
1330                min[i] = min[i].min(p[i]);
1331                max[i] = max[i].max(p[i]);
1332            }
1333        };
1334
1335        for v in &self.vertices {
1336            extend(*v);
1337        }
1338
1339        for edge in &self.edges {
1340            match &edge.curve {
1341                Curve3D::Line { .. } => {}
1342                curve => {
1343                    let polyline = curve.to_polyline(0.01);
1344                    for p in &polyline {
1345                        extend(*p);
1346                    }
1347                }
1348            }
1349        }
1350
1351        (min, max)
1352    }
1353}
1354
1355#[cfg(test)]
1356mod tests {
1357    use super::*;
1358
1359    #[test]
1360    fn sphere_evaluate_poles() {
1361        let s = Surface::Sphere {
1362            center: [0.0, 0.0, 0.0],
1363            radius: 1.0,
1364        };
1365        let north = s.evaluate(0.0, 0.0);
1366        assert!((north[2] - 1.0).abs() < 1e-10);
1367        let south = s.evaluate(0.0, std::f64::consts::PI);
1368        assert!((south[2] + 1.0).abs() < 1e-10);
1369    }
1370
1371    #[test]
1372    fn sphere_normal_outward() {
1373        let s = Surface::Sphere {
1374            center: [1.0, 2.0, 3.0],
1375            radius: 2.0,
1376        };
1377        let n = s.normal_at(0.0, std::f64::consts::FRAC_PI_2);
1378        let p = s.evaluate(0.0, std::f64::consts::FRAC_PI_2);
1379        let expected_x = (p[0] - 1.0) / 2.0;
1380        assert!((n[0] - expected_x).abs() < 1e-10);
1381    }
1382
1383    #[test]
1384    fn cylinder_evaluate() {
1385        let s = Surface::Cylinder {
1386            origin: [0.0, 0.0, 0.0],
1387            axis: [0.0, 0.0, 2.0],
1388            radius: 1.0,
1389        };
1390        let p = s.evaluate(0.0, 1.0);
1391        assert!((p[2] - 1.0).abs() < 1e-10);
1392        let r = (p[0] * p[0] + p[1] * p[1]).sqrt();
1393        assert!((r - 1.0).abs() < 1e-10);
1394    }
1395
1396    fn make_multispan_revolution() -> Surface {
1397        let profile_control_points = vec![
1398            [1.0, 0.0],
1399            [1.0, 1.0],
1400            [1.0, 2.0],
1401            [2.0, 3.0],
1402            [2.0, 3.0],
1403            [2.0, 4.0],
1404            [2.0, 5.0],
1405            [1.0, 6.0],
1406        ];
1407        let profile_weights = vec![1.0; 8];
1408        Surface::SurfaceOfRevolution {
1409            center: [0.0, 0.0, 0.0],
1410            axis: [0.0, 0.0, 1.0],
1411            frame_u: [1.0, 0.0, 0.0],
1412            frame_v: [0.0, 1.0, 0.0],
1413            profile_control_points,
1414            profile_weights,
1415            profile_degree: 3,
1416            n_profile_spans: 2,
1417            theta_start: 0.0,
1418            theta_range: std::f64::consts::TAU,
1419        }
1420    }
1421
1422    #[test]
1423    fn revolution_multispan_evaluate_endpoints() {
1424        let s = make_multispan_revolution();
1425        let p0 = s.evaluate(0.0, 0.0);
1426        assert!((p0[0] - 1.0).abs() < 1e-10);
1427        assert!(p0[2].abs() < 1e-10);
1428
1429        let p1 = s.evaluate(0.0, 1.0);
1430        assert!((p1[0] - 1.0).abs() < 1e-10);
1431        assert!((p1[2] - 6.0).abs() < 1e-10);
1432    }
1433
1434    #[test]
1435    fn revolution_multispan_evaluate_midpoint() {
1436        let s = make_multispan_revolution();
1437        let pm = s.evaluate(0.0, 0.5);
1438        assert!((pm[0] - 2.0).abs() < 1e-10);
1439        assert!((pm[2] - 3.0).abs() < 1e-10);
1440    }
1441
1442    #[test]
1443    fn revolution_multispan_normal_at() {
1444        let s = make_multispan_revolution();
1445        let n = s.normal_at(0.0, 0.5);
1446        let len = vec3::length(n);
1447        assert!((len - 1.0).abs() < 1e-6);
1448    }
1449
1450    #[test]
1451    fn param_range_sphere() {
1452        let s = Surface::Sphere {
1453            center: [0.0, 0.0, 0.0],
1454            radius: 1.0,
1455        };
1456        let (u0, u1, v0, v1) = s.param_range();
1457        assert!(u0.abs() < 1e-10);
1458        assert!((u1 - std::f64::consts::TAU).abs() < 1e-10);
1459        assert!(v0.abs() < 1e-10);
1460        assert!((v1 - std::f64::consts::PI).abs() < 1e-10);
1461    }
1462
1463    #[test]
1464    fn sphere_inverse_project_round_trip() {
1465        let s = Surface::Sphere {
1466            center: [1.0, 2.0, 3.0],
1467            radius: 2.0,
1468        };
1469        let p = s.evaluate(0.7, 1.2);
1470        let (u, v) = s.inverse_project(&p).unwrap();
1471        let q = s.evaluate(u, v);
1472        assert!(vec3::distance(p, q) < 1e-10);
1473    }
1474
1475    #[test]
1476    fn torus_inverse_project_round_trip() {
1477        let s = Surface::Torus {
1478            center: [0.0, 0.0, 0.0],
1479            axis: [0.0, 0.0, 1.0],
1480            major_radius: 3.0,
1481            minor_radius: 1.0,
1482        };
1483        let p = s.evaluate(1.1, 2.2);
1484        let (u, v) = s.inverse_project(&p).unwrap();
1485        let q = s.evaluate(u, v);
1486        assert!(vec3::distance(p, q) < 1e-10);
1487    }
1488
1489    #[test]
1490    fn revolution_inverse_project_round_trip() {
1491        let s = make_multispan_revolution();
1492        let p = s.evaluate(1.3, 0.4);
1493        let (u, v) = s.inverse_project(&p).unwrap();
1494        let q = s.evaluate(u, v);
1495        assert!(vec3::distance(p, q) < 1e-6);
1496    }
1497}