1use 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#[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 frame_u: [f64; 3],
50 frame_v: [f64; 3],
52 profile_control_points: Vec<[f64; 2]>,
54 profile_weights: Vec<f64>,
55 profile_degree: u32,
56 n_profile_spans: u32,
58 theta_start: f64,
60 theta_range: f64,
62 },
63 SurfaceOfSweep {
64 spine_control_points: Vec<[f64; 3]>,
66 spine_weights: Vec<f64>,
67 spine_degree: u32,
68 profile_control_points: Vec<[f64; 2]>,
70 profile_weights: Vec<f64>,
71 profile_degree: u32,
72 n_profile_spans: u32,
73 frames: Vec<[[f64; 3]; 3]>,
75 },
76 NurbsSurface {
77 data: Box<neco_nurbs::NurbsSurface3D>,
78 },
79}
80
81impl Surface {
82 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 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 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 pub fn is_analytical(&self) -> bool {
673 !matches!(self, Surface::NurbsSurface { .. })
674 }
675
676 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 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
919pub 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
947pub 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#[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 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 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#[derive(Debug, Clone, Copy)]
1241pub struct EdgeRef {
1242 pub edge_id: EdgeId,
1243 pub forward: bool,
1244}
1245
1246#[derive(Debug, Clone)]
1248pub struct Edge {
1249 pub v_start: VertexId,
1250 pub v_end: VertexId,
1251 pub curve: Curve3D,
1252}
1253
1254#[derive(Debug, Clone)]
1256pub struct Face {
1257 pub loop_edges: Vec<EdgeRef>,
1258 pub surface: Surface,
1259 pub orientation_reversed: bool,
1260}
1261
1262#[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#[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 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 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 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}