1use axiolid_contracts::BackendId;
38use axiolid_contracts::{GeomError, GeomResult};
39use axiolid_core::{Frame3, Point3, Scalar, SpaceFrame, Tolerance, Vec3};
40use axiolid_surface::{
41 BSplineSurface, Cone, Cylinder, EllipticalCylinder, Plane, Sphere, Surface, Torus,
42};
43
44use crate::curve::{de_boor_recurrence, eval_homogeneous, span_in};
45use crate::nurbs::SplineAxis;
46
47#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct Patch {
54 pub u_start: Scalar,
56 pub u_end: Scalar,
58 pub v_start: Scalar,
60 pub v_end: Scalar,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq)]
66pub struct SurfaceJet {
67 pub point: Point3,
69 pub du: Vec3,
71 pub dv: Vec3,
73 pub duu: Vec3,
75 pub duv: Vec3,
77 pub dvv: Vec3,
79}
80
81impl Patch {
82 pub fn new(u_start: Scalar, u_end: Scalar, v_start: Scalar, v_end: Scalar) -> GeomResult<Self> {
84 let all = [u_start, u_end, v_start, v_end];
85 if !all.iter().all(|value| value.is_finite()) {
86 return Err(GeomError::InvalidInput(format!(
87 "patch bounds must be finite, got {all:?}"
88 )));
89 }
90 if !(u_end > u_start && v_end > v_start) {
91 return Err(GeomError::Degenerate(format!(
92 "patch must have positive extent, got u {u_start}..{u_end}, v {v_start}..{v_end}"
93 )));
94 }
95 Ok(Self {
96 u_start,
97 u_end,
98 v_start,
99 v_end,
100 })
101 }
102
103 pub fn full_turn(v_start: Scalar, v_end: Scalar) -> GeomResult<Self> {
105 Self::new(0.0, core::f64::consts::TAU, v_start, v_end)
106 }
107}
108
109fn place(frame: &Frame3, local: Vec3) -> Point3 {
111 frame.origin + frame.x * local.x + frame.y * local.y + frame.z * local.z
112}
113
114fn direct(frame: &Frame3, local: Vec3) -> Vec3 {
116 frame.x * local.x + frame.y * local.y + frame.z * local.z
117}
118
119fn finite(value: Scalar, what: &str) -> GeomResult<()> {
120 if value.is_finite() {
121 Ok(())
122 } else {
123 Err(GeomError::InvalidInput(format!(
124 "{what} must be finite, got {value}"
125 )))
126 }
127}
128
129fn positive(value: Scalar, what: &str) -> GeomResult<()> {
130 finite(value, what)?;
131 if value > 0.0 {
132 Ok(())
133 } else {
134 Err(GeomError::InvalidInput(format!(
135 "{what} must be positive, got {value}"
136 )))
137 }
138}
139
140fn finite_surface_frame(surface: &Surface) -> GeomResult<()> {
141 let frame = match surface {
142 Surface::Plane(value) => Some(&value.frame),
143 Surface::Cylinder(value) => Some(&value.frame),
144 Surface::Cone(value) => Some(&value.frame),
145 Surface::Sphere(value) => Some(&value.frame),
146 Surface::Torus(value) => Some(&value.frame),
147 Surface::EllipticalCylinder(value) => Some(&value.frame),
148 _ => None,
149 };
150 if frame.is_none_or(|frame| {
151 frame.origin.is_finite()
152 && frame.x.is_finite()
153 && frame.y.is_finite()
154 && frame.z.is_finite()
155 }) {
156 Ok(())
157 } else {
158 Err(GeomError::InvalidInput(
159 "surface frame must be finite".to_owned(),
160 ))
161 }
162}
163
164pub fn evaluate(surface: &Surface, u: Scalar, v: Scalar) -> GeomResult<Point3> {
166 finite(u, "surface parameter u")?;
167 finite(v, "surface parameter v")?;
168 finite_surface_frame(surface)?;
169 let point = match surface {
170 Surface::Plane(p) => Ok(plane_point(p, u, v)),
171 Surface::Cylinder(c) => cylinder_point(c, u, v),
172 Surface::EllipticalCylinder(c) => elliptical_cylinder_point(c, u, v),
173 Surface::Cone(c) => cone_point(c, u, v),
174 Surface::Sphere(s) => sphere_point(s, u, v),
175 Surface::Torus(t) => torus_point(t, u, v),
176 Surface::BSpline(b) => bspline_point(b, u, v),
177 _ => Err(GeomError::Unsupported {
178 backend: ScalarSurface::ID,
179 operation: axiolid_contracts::Operation::SurfaceEvaluation,
180 }),
181 }?;
182 if point.is_finite() {
183 Ok(point)
184 } else {
185 Err(GeomError::Degenerate(
186 "surface point is non-finite".to_owned(),
187 ))
188 }
189}
190
191pub fn partials(surface: &Surface, u: Scalar, v: Scalar) -> GeomResult<(Vec3, Vec3)> {
197 finite(u, "surface parameter u")?;
198 finite(v, "surface parameter v")?;
199 finite_surface_frame(surface)?;
200 let value = match surface {
201 Surface::Plane(p) => Ok((p.frame.x, p.frame.y)),
202 Surface::Cylinder(c) => {
203 positive(c.radius, "cylinder radius")?;
204 let (s, co) = u.sin_cos();
205 Ok((
206 direct(&c.frame, Vec3::new(-c.radius * s, c.radius * co, 0.0)),
207 c.frame.z,
208 ))
209 }
210 Surface::Cone(c) => {
211 finite(c.radius, "cone radius")?;
212 finite(c.semi_angle, "cone semi-angle")?;
213 let slope = c.semi_angle.tan();
214 let radius = c.radius + v * slope;
215 if radius < 0.0 {
216 return Err(GeomError::Degenerate(format!(
217 "cone radius is negative at v = {v}: the patch crosses the apex"
218 )));
219 }
220 let (s, co) = u.sin_cos();
221 Ok((
222 direct(&c.frame, Vec3::new(-radius * s, radius * co, 0.0)),
223 direct(&c.frame, Vec3::new(slope * co, slope * s, 1.0)),
224 ))
225 }
226 Surface::Sphere(sphere) => {
227 positive(sphere.radius, "sphere radius")?;
228 let (su, cu) = u.sin_cos();
229 let (sv, cv) = v.sin_cos();
230 Ok((
231 direct(
232 &sphere.frame,
233 Vec3::new(-sphere.radius * cv * su, sphere.radius * cv * cu, 0.0),
234 ),
235 direct(
236 &sphere.frame,
237 Vec3::new(
238 -sphere.radius * sv * cu,
239 -sphere.radius * sv * su,
240 sphere.radius * cv,
241 ),
242 ),
243 ))
244 }
245 Surface::Torus(torus) => {
246 positive(torus.major_radius, "torus major radius")?;
247 positive(torus.minor_radius, "torus minor radius")?;
248 let (su, cu) = u.sin_cos();
249 let (sv, cv) = v.sin_cos();
250 let ring = torus.major_radius + torus.minor_radius * cv;
251 Ok((
252 direct(&torus.frame, Vec3::new(-ring * su, ring * cu, 0.0)),
253 direct(
254 &torus.frame,
255 Vec3::new(
256 -torus.minor_radius * sv * cu,
257 -torus.minor_radius * sv * su,
258 torus.minor_radius * cv,
259 ),
260 ),
261 ))
262 }
263 Surface::EllipticalCylinder(c) => {
264 positive(c.semi_axis_x, "elliptical cylinder semi-axis x")?;
265 positive(c.semi_axis_y, "elliptical cylinder semi-axis y")?;
266 let (su, cu) = u.sin_cos();
267 Ok((
271 direct(
272 &c.frame,
273 Vec3::new(-c.semi_axis_x * su, c.semi_axis_y * cu, 0.0),
274 ),
275 direct(&c.frame, Vec3::Z),
276 ))
277 }
278 Surface::BSpline(b) => bspline_partials(b, u, v),
279 _ => Err(GeomError::Unsupported {
280 backend: ScalarSurface::ID,
281 operation: axiolid_contracts::Operation::SurfaceEvaluation,
282 }),
283 }?;
284 if value.0.is_finite() && value.1.is_finite() {
285 Ok(value)
286 } else {
287 Err(GeomError::Degenerate(
288 "surface partial is non-finite".to_owned(),
289 ))
290 }
291}
292
293pub fn jet(surface: &Surface, u: Scalar, v: Scalar) -> GeomResult<SurfaceJet> {
298 finite(u, "surface parameter u")?;
299 finite(v, "surface parameter v")?;
300 finite_surface_frame(surface)?;
301 let value = match surface {
302 Surface::Plane(p) => SurfaceJet {
303 point: plane_point(p, u, v),
304 du: p.frame.x,
305 dv: p.frame.y,
306 duu: Vec3::ZERO,
307 duv: Vec3::ZERO,
308 dvv: Vec3::ZERO,
309 },
310 Surface::Cylinder(c) => {
311 positive(c.radius, "cylinder radius")?;
312 let (s, co) = u.sin_cos();
313 SurfaceJet {
314 point: cylinder_point(c, u, v)?,
315 du: direct(&c.frame, Vec3::new(-c.radius * s, c.radius * co, 0.0)),
316 dv: c.frame.z,
317 duu: direct(&c.frame, Vec3::new(-c.radius * co, -c.radius * s, 0.0)),
318 duv: Vec3::ZERO,
319 dvv: Vec3::ZERO,
320 }
321 }
322 Surface::Cone(c) => {
323 finite(c.radius, "cone radius")?;
324 finite(c.semi_angle, "cone semi-angle")?;
325 let slope = c.semi_angle.tan();
326 let radius = c.radius + v * slope;
327 if radius < 0.0 {
328 return Err(GeomError::Degenerate(format!(
329 "cone radius is negative at v = {v}: the patch crosses the apex"
330 )));
331 }
332 let (s, co) = u.sin_cos();
333 SurfaceJet {
334 point: cone_point(c, u, v)?,
335 du: direct(&c.frame, Vec3::new(-radius * s, radius * co, 0.0)),
336 dv: direct(&c.frame, Vec3::new(slope * co, slope * s, 1.0)),
337 duu: direct(&c.frame, Vec3::new(-radius * co, -radius * s, 0.0)),
338 duv: direct(&c.frame, Vec3::new(-slope * s, slope * co, 0.0)),
339 dvv: Vec3::ZERO,
340 }
341 }
342 Surface::Sphere(sphere) => {
343 positive(sphere.radius, "sphere radius")?;
344 let r = sphere.radius;
345 let (su, cu) = u.sin_cos();
346 let (sv, cv) = v.sin_cos();
347 SurfaceJet {
348 point: sphere_point(sphere, u, v)?,
349 du: direct(&sphere.frame, Vec3::new(-r * cv * su, r * cv * cu, 0.0)),
350 dv: direct(&sphere.frame, Vec3::new(-r * sv * cu, -r * sv * su, r * cv)),
351 duu: direct(&sphere.frame, Vec3::new(-r * cv * cu, -r * cv * su, 0.0)),
352 duv: direct(&sphere.frame, Vec3::new(r * sv * su, -r * sv * cu, 0.0)),
353 dvv: direct(
354 &sphere.frame,
355 Vec3::new(-r * cv * cu, -r * cv * su, -r * sv),
356 ),
357 }
358 }
359 Surface::Torus(torus) => {
360 positive(torus.major_radius, "torus major radius")?;
361 positive(torus.minor_radius, "torus minor radius")?;
362 let r = torus.minor_radius;
363 let (su, cu) = u.sin_cos();
364 let (sv, cv) = v.sin_cos();
365 let ring = torus.major_radius + r * cv;
366 SurfaceJet {
367 point: torus_point(torus, u, v)?,
368 du: direct(&torus.frame, Vec3::new(-ring * su, ring * cu, 0.0)),
369 dv: direct(&torus.frame, Vec3::new(-r * sv * cu, -r * sv * su, r * cv)),
370 duu: direct(&torus.frame, Vec3::new(-ring * cu, -ring * su, 0.0)),
371 duv: direct(&torus.frame, Vec3::new(r * sv * su, -r * sv * cu, 0.0)),
372 dvv: direct(&torus.frame, Vec3::new(-r * cv * cu, -r * cv * su, -r * sv)),
373 }
374 }
375 Surface::BSpline(b) => bspline_jet(b, u, v)?,
376 _ => {
377 return Err(GeomError::Unsupported {
378 backend: ScalarSurface::ID,
379 operation: axiolid_contracts::Operation::SurfaceEvaluation,
380 })
381 }
382 };
383 if [
384 value.point,
385 value.du,
386 value.dv,
387 value.duu,
388 value.duv,
389 value.dvv,
390 ]
391 .iter()
392 .all(|vector| vector.is_finite())
393 {
394 Ok(value)
395 } else {
396 Err(GeomError::Degenerate(
397 "surface differential jet is non-finite".to_owned(),
398 ))
399 }
400}
401
402pub fn normal(surface: &Surface, u: Scalar, v: Scalar) -> GeomResult<Vec3> {
408 finite(u, "surface parameter u")?;
409 finite(v, "surface parameter v")?;
410 finite_surface_frame(surface)?;
411 let n = match surface {
412 Surface::Plane(p) => p.frame.z,
413 Surface::Cylinder(c) => {
414 positive(c.radius, "cylinder radius")?;
415 let (s, co) = u.sin_cos();
416 direct(&c.frame, Vec3::new(co, s, 0.0))
417 }
418 Surface::Cone(c) => cone_normal(c, u)?,
419 Surface::EllipticalCylinder(c) => {
420 positive(c.semi_axis_x, "elliptical cylinder semi-axis x")?;
421 positive(c.semi_axis_y, "elliptical cylinder semi-axis y")?;
422 let (su, cu) = u.sin_cos();
428 let along_u = direct(
429 &c.frame,
430 Vec3::new(-c.semi_axis_x * su, c.semi_axis_y * cu, 0.0),
431 );
432 along_u.cross(c.frame.z)
433 }
434 Surface::Sphere(s) => {
435 positive(s.radius, "sphere radius")?;
436 let (su, cu) = u.sin_cos();
437 let (sv, cv) = v.sin_cos();
438 direct(&s.frame, Vec3::new(cv * cu, cv * su, sv))
439 }
440 Surface::Torus(t) => {
441 positive(t.minor_radius, "torus minor radius")?;
442 let (su, cu) = u.sin_cos();
443 let (sv, cv) = v.sin_cos();
444 direct(&t.frame, Vec3::new(cv * cu, cv * su, sv))
445 }
446 Surface::BSpline(b) => bspline_normal(b, u, v)?,
447 _ => {
448 return Err(GeomError::Unsupported {
449 backend: ScalarSurface::ID,
450 operation: axiolid_contracts::Operation::SurfaceEvaluation,
451 })
452 }
453 };
454 let length = n.length();
455 if !(length > 0.0 && n.is_finite()) {
456 return Err(GeomError::Degenerate(format!(
457 "surface normal is not orientable at ({u}, {v})"
458 )));
459 }
460 Ok(n / length)
461}
462
463fn plane_point(p: &Plane, u: Scalar, v: Scalar) -> Point3 {
464 place(&p.frame, Vec3::new(u, v, 0.0))
465}
466
467fn cylinder_point(c: &Cylinder, u: Scalar, v: Scalar) -> GeomResult<Point3> {
468 positive(c.radius, "cylinder radius")?;
469 let (s, co) = u.sin_cos();
470 Ok(place(&c.frame, Vec3::new(c.radius * co, c.radius * s, v)))
471}
472
473fn elliptical_cylinder_point(c: &EllipticalCylinder, u: Scalar, v: Scalar) -> GeomResult<Point3> {
474 positive(c.semi_axis_x, "elliptical cylinder semi-axis x")?;
475 positive(c.semi_axis_y, "elliptical cylinder semi-axis y")?;
476 let (s, co) = u.sin_cos();
477 Ok(place(
478 &c.frame,
479 Vec3::new(c.semi_axis_x * co, c.semi_axis_y * s, v),
480 ))
481}
482
483fn cone_point(c: &Cone, u: Scalar, v: Scalar) -> GeomResult<Point3> {
484 finite(c.radius, "cone radius")?;
485 finite(c.semi_angle, "cone semi-angle")?;
486 let r = c.radius + v * c.semi_angle.tan();
489 if r < 0.0 {
490 return Err(GeomError::Degenerate(format!(
491 "cone radius is negative at v = {v}: the patch crosses the apex"
492 )));
493 }
494 let (s, co) = u.sin_cos();
495 Ok(place(&c.frame, Vec3::new(r * co, r * s, v)))
496}
497
498fn cone_normal(c: &Cone, u: Scalar) -> GeomResult<Vec3> {
499 finite(c.semi_angle, "cone semi-angle")?;
500 let (s, co) = u.sin_cos();
501 let (sa, ca) = c.semi_angle.sin_cos();
504 Ok(direct(&c.frame, Vec3::new(ca * co, ca * s, -sa)))
505}
506
507fn sphere_point(s: &Sphere, u: Scalar, v: Scalar) -> GeomResult<Point3> {
508 positive(s.radius, "sphere radius")?;
509 let (su, cu) = u.sin_cos();
510 let (sv, cv) = v.sin_cos();
511 Ok(place(
512 &s.frame,
513 Vec3::new(s.radius * cv * cu, s.radius * cv * su, s.radius * sv),
514 ))
515}
516
517fn torus_point(t: &Torus, u: Scalar, v: Scalar) -> GeomResult<Point3> {
518 positive(t.major_radius, "torus major radius")?;
519 positive(t.minor_radius, "torus minor radius")?;
520 let (su, cu) = u.sin_cos();
521 let (sv, cv) = v.sin_cos();
522 let ring = t.major_radius + t.minor_radius * cv;
523 Ok(place(
524 &t.frame,
525 Vec3::new(ring * cu, ring * su, t.minor_radius * sv),
526 ))
527}
528
529type Axis = SplineAxis;
532
533fn bspline_axes(b: &BSplineSurface) -> GeomResult<(Axis, Axis)> {
535 let rows = b.control_points.len();
536 if rows == 0 {
537 return Err(GeomError::InvalidInput(
538 "B-spline surface has no control points".to_owned(),
539 ));
540 }
541 let cols = b.control_points[0].len();
542 if cols == 0 {
543 return Err(GeomError::InvalidInput(
544 "B-spline surface control net has an empty row".to_owned(),
545 ));
546 }
547 if b.control_points.iter().any(|row| row.len() != cols) {
550 return Err(GeomError::InvalidInput(
551 "B-spline surface control net is ragged".to_owned(),
552 ));
553 }
554 if b.control_points
555 .iter()
556 .flatten()
557 .any(|point| !point.is_finite())
558 {
559 return Err(GeomError::InvalidInput(
560 "B-spline surface control points must be finite".to_owned(),
561 ));
562 }
563 if let Some(w) = &b.weights {
564 if w.len() != rows || w.iter().any(|row| row.len() != cols) {
565 return Err(GeomError::InvalidInput(
566 "B-spline surface weight net does not match the control net".to_owned(),
567 ));
568 }
569 if w.iter()
570 .flatten()
571 .any(|weight| !weight.is_finite() || *weight <= 0.0)
572 {
573 return Err(GeomError::InvalidInput(
574 "B-spline surface weights must be finite and strictly positive".to_owned(),
575 ));
576 }
577 }
578 let u = Axis::new(&b.u_knots, &b.u_multiplicities, b.u_degree, rows, "u")?;
579 let v = Axis::new(&b.v_knots, &b.v_multiplicities, b.v_degree, cols, "v")?;
580 Ok((u, v))
581}
582
583fn bspline_point(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<Point3> {
589 let (ua, va) = bspline_axes(b)?;
590 let (uc, vc) = (ua.clamp(u), va.clamp(v));
591 let uspan = span_in(&ua.knots, ua.count, ua.degree, uc);
592 let vspan = span_in(&va.knots, va.count, va.degree, vc);
593
594 let mut row_points: Vec<[Scalar; 3]> = Vec::with_capacity(ua.degree + 1);
596 let mut row_weights: Vec<Scalar> = Vec::with_capacity(ua.degree + 1);
597 for i in 0..=ua.degree {
598 let row = uspan - ua.degree + i;
599 let mut pts: Vec<[Scalar; 3]> = Vec::with_capacity(va.degree + 1);
600 let mut wts: Vec<Scalar> = Vec::with_capacity(va.degree + 1);
601 for j in 0..=va.degree {
602 let col = vspan - va.degree + j;
603 let w = b.weights.as_ref().map_or(1.0, |ws| ws[row][col]);
604 let p = b.control_points[row][col];
605 let homogeneous = [p.x * w, p.y * w, p.z * w];
606 if homogeneous.iter().any(|value| !value.is_finite()) {
607 return Err(GeomError::Degenerate(
608 "B-spline surface homogeneous control point overflowed".to_owned(),
609 ));
610 }
611 pts.push(homogeneous);
612 wts.push(w);
613 }
614 de_boor_recurrence(&va.knots, vspan, va.degree, vc, &mut pts, &mut wts);
615 row_points.push(pts[va.degree]);
616 row_weights.push(wts[va.degree]);
617 }
618
619 de_boor_recurrence(
621 &ua.knots,
622 uspan,
623 ua.degree,
624 uc,
625 &mut row_points,
626 &mut row_weights,
627 );
628
629 let w = row_weights[ua.degree];
630 if !w.is_finite() || w == 0.0 {
631 return Err(GeomError::Degenerate(
632 "B-spline surface weight collapsed to zero".to_owned(),
633 ));
634 }
635 let p = row_points[ua.degree];
636 Ok(Point3::new(p[0] / w, p[1] / w, p[2] / w))
637}
638
639#[derive(Clone, Copy)]
641struct HomogeneousAxes<'a> {
642 u_knots: &'a [Scalar],
643 u_degree: usize,
644 v_knots: &'a [Scalar],
645 v_degree: usize,
646}
647
648fn eval_tensor_homogeneous(
650 axes: HomogeneousAxes<'_>,
651 points: &[Vec<[Scalar; 3]>],
652 weights: &[Vec<Scalar>],
653 u: Scalar,
654 v: Scalar,
655) -> ([Scalar; 3], Scalar) {
656 let mut row_points = Vec::with_capacity(points.len());
657 let mut row_weights = Vec::with_capacity(points.len());
658 for (row_points_h, row_weights_h) in points.iter().zip(weights) {
659 let (point, weight) =
660 eval_homogeneous(axes.v_knots, axes.v_degree, row_points_h, row_weights_h, v);
661 row_points.push(point);
662 row_weights.push(weight);
663 }
664 eval_homogeneous(axes.u_knots, axes.u_degree, &row_points, &row_weights, u)
665}
666
667type HomogeneousPointNet = Vec<Vec<[Scalar; 3]>>;
668type HomogeneousWeightNet = Vec<Vec<Scalar>>;
669
670fn homogeneous_control_net(
672 b: &BSplineSurface,
673) -> GeomResult<(HomogeneousPointNet, HomogeneousWeightNet)> {
674 let mut points = Vec::with_capacity(b.control_points.len());
675 let mut weights = Vec::with_capacity(b.control_points.len());
676 for (i, row) in b.control_points.iter().enumerate() {
677 let mut point_row = Vec::with_capacity(row.len());
678 let mut weight_row = Vec::with_capacity(row.len());
679 for (j, point) in row.iter().enumerate() {
680 let weight = b.weights.as_ref().map_or(1.0, |net| net[i][j]);
681 let homogeneous = [point.x * weight, point.y * weight, point.z * weight];
682 if homogeneous.iter().any(|value| !value.is_finite()) {
683 return Err(GeomError::Degenerate(
684 "B-spline surface homogeneous control point overflowed".to_owned(),
685 ));
686 }
687 point_row.push(homogeneous);
688 weight_row.push(weight);
689 }
690 points.push(point_row);
691 weights.push(weight_row);
692 }
693 Ok((points, weights))
694}
695
696fn derivative_net_u(
698 points: &[Vec<[Scalar; 3]>],
699 weights: &[Vec<Scalar>],
700 knots: &[Scalar],
701 degree: usize,
702) -> (Vec<Vec<[Scalar; 3]>>, Vec<Vec<Scalar>>) {
703 let rows = points.len() - 1;
704 let cols = points[0].len();
705 let mut derivative_points = Vec::with_capacity(rows);
706 let mut derivative_weights = Vec::with_capacity(rows);
707 for i in 0..rows {
708 let denominator = knots[i + degree + 1] - knots[i + 1];
709 let factor = if denominator.abs() > 0.0 {
710 degree as Scalar / denominator
711 } else {
712 0.0
713 };
714 let mut point_row = Vec::with_capacity(cols);
715 let mut weight_row = Vec::with_capacity(cols);
716 for j in 0..cols {
717 point_row.push(core::array::from_fn(|k| {
718 factor * (points[i + 1][j][k] - points[i][j][k])
719 }));
720 weight_row.push(factor * (weights[i + 1][j] - weights[i][j]));
721 }
722 derivative_points.push(point_row);
723 derivative_weights.push(weight_row);
724 }
725 (derivative_points, derivative_weights)
726}
727
728fn derivative_net_v(
730 points: &[Vec<[Scalar; 3]>],
731 weights: &[Vec<Scalar>],
732 knots: &[Scalar],
733 degree: usize,
734) -> (Vec<Vec<[Scalar; 3]>>, Vec<Vec<Scalar>>) {
735 let rows = points.len();
736 let cols = points[0].len() - 1;
737 let mut derivative_points = Vec::with_capacity(rows);
738 let mut derivative_weights = Vec::with_capacity(rows);
739 for i in 0..rows {
740 let mut point_row = Vec::with_capacity(cols);
741 let mut weight_row = Vec::with_capacity(cols);
742 for j in 0..cols {
743 let denominator = knots[j + degree + 1] - knots[j + 1];
744 let factor = if denominator.abs() > 0.0 {
745 degree as Scalar / denominator
746 } else {
747 0.0
748 };
749 point_row.push(core::array::from_fn(|k| {
750 factor * (points[i][j + 1][k] - points[i][j][k])
751 }));
752 weight_row.push(factor * (weights[i][j + 1] - weights[i][j]));
753 }
754 derivative_points.push(point_row);
755 derivative_weights.push(weight_row);
756 }
757 (derivative_points, derivative_weights)
758}
759
760fn project_derivative(
762 point: [Scalar; 3],
763 weight: Scalar,
764 derivative: [Scalar; 3],
765 derivative_weight: Scalar,
766 axis: &str,
767) -> GeomResult<Vec3> {
768 if !weight.is_finite() || weight == 0.0 {
769 return Err(GeomError::Degenerate(
770 "B-spline surface weight collapsed to a non-finite or zero value".to_owned(),
771 ));
772 }
773 let value = Vec3::new(
774 (derivative[0] - point[0] * derivative_weight / weight) / weight,
775 (derivative[1] - point[1] * derivative_weight / weight) / weight,
776 (derivative[2] - point[2] * derivative_weight / weight) / weight,
777 );
778 if !value.is_finite() {
779 return Err(GeomError::Degenerate(format!(
780 "B-spline surface {axis} derivative is non-finite"
781 )));
782 }
783 Ok(value)
784}
785
786fn bspline_partials(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<(Vec3, Vec3)> {
788 let (ua, va) = bspline_axes(b)?;
789 let (uc, vc) = (ua.clamp(u), va.clamp(v));
790 let (points, weights) = homogeneous_control_net(b)?;
791 let (point, weight) = eval_tensor_homogeneous(
792 HomogeneousAxes {
793 u_knots: &ua.knots,
794 u_degree: ua.degree,
795 v_knots: &va.knots,
796 v_degree: va.degree,
797 },
798 &points,
799 &weights,
800 uc,
801 vc,
802 );
803
804 let (u_points, u_weights) = derivative_net_u(&points, &weights, &ua.knots, ua.degree);
805 let (du, du_weight) = eval_tensor_homogeneous(
806 HomogeneousAxes {
807 u_knots: &ua.knots[1..ua.knots.len() - 1],
808 u_degree: ua.degree - 1,
809 v_knots: &va.knots,
810 v_degree: va.degree,
811 },
812 &u_points,
813 &u_weights,
814 uc,
815 vc,
816 );
817
818 let (v_points, v_weights) = derivative_net_v(&points, &weights, &va.knots, va.degree);
819 let (dv, dv_weight) = eval_tensor_homogeneous(
820 HomogeneousAxes {
821 u_knots: &ua.knots,
822 u_degree: ua.degree,
823 v_knots: &va.knots[1..va.knots.len() - 1],
824 v_degree: va.degree - 1,
825 },
826 &v_points,
827 &v_weights,
828 uc,
829 vc,
830 );
831
832 Ok((
833 project_derivative(point, weight, du, du_weight, "u")?,
834 project_derivative(point, weight, dv, dv_weight, "v")?,
835 ))
836}
837
838pub fn bspline_jet(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<SurfaceJet> {
841 let (ua, va) = bspline_axes(b)?;
842 let (uc, vc) = (ua.clamp(u), va.clamp(v));
843 let (points, weights) = homogeneous_control_net(b)?;
844 let base_axes = HomogeneousAxes {
845 u_knots: &ua.knots,
846 u_degree: ua.degree,
847 v_knots: &va.knots,
848 v_degree: va.degree,
849 };
850 let (point, weight) = eval_tensor_homogeneous(base_axes, &points, &weights, uc, vc);
851 if !weight.is_finite() || weight == 0.0 {
852 return Err(GeomError::Degenerate(
853 "B-spline surface weight collapsed to a non-finite or zero value".to_owned(),
854 ));
855 }
856 let position = Point3::new(point[0] / weight, point[1] / weight, point[2] / weight);
857
858 let (u_points, u_weights) = derivative_net_u(&points, &weights, &ua.knots, ua.degree);
859 let u_knots = &ua.knots[1..ua.knots.len() - 1];
860 let (du_h, du_weight) = eval_tensor_homogeneous(
861 HomogeneousAxes {
862 u_knots,
863 u_degree: ua.degree - 1,
864 v_knots: &va.knots,
865 v_degree: va.degree,
866 },
867 &u_points,
868 &u_weights,
869 uc,
870 vc,
871 );
872 let du = project_derivative(point, weight, du_h, du_weight, "u")?;
873
874 let (v_points, v_weights) = derivative_net_v(&points, &weights, &va.knots, va.degree);
875 let v_knots = &va.knots[1..va.knots.len() - 1];
876 let (dv_h, dv_weight) = eval_tensor_homogeneous(
877 HomogeneousAxes {
878 u_knots: &ua.knots,
879 u_degree: ua.degree,
880 v_knots,
881 v_degree: va.degree - 1,
882 },
883 &v_points,
884 &v_weights,
885 uc,
886 vc,
887 );
888 let dv = project_derivative(point, weight, dv_h, dv_weight, "v")?;
889
890 let (duu_h, duu_weight) = if ua.degree >= 2 {
891 let (net, net_weights) = derivative_net_u(&u_points, &u_weights, u_knots, ua.degree - 1);
892 eval_tensor_homogeneous(
893 HomogeneousAxes {
894 u_knots: &u_knots[1..u_knots.len() - 1],
895 u_degree: ua.degree - 2,
896 v_knots: &va.knots,
897 v_degree: va.degree,
898 },
899 &net,
900 &net_weights,
901 uc,
902 vc,
903 )
904 } else {
905 ([0.0; 3], 0.0)
906 };
907 let duu = project_second(point, weight, du, duu_h, du_weight, duu_weight, "uu")?;
908
909 let (dvv_h, dvv_weight) = if va.degree >= 2 {
910 let (net, net_weights) = derivative_net_v(&v_points, &v_weights, v_knots, va.degree - 1);
911 eval_tensor_homogeneous(
912 HomogeneousAxes {
913 u_knots: &ua.knots,
914 u_degree: ua.degree,
915 v_knots: &v_knots[1..v_knots.len() - 1],
916 v_degree: va.degree - 2,
917 },
918 &net,
919 &net_weights,
920 uc,
921 vc,
922 )
923 } else {
924 ([0.0; 3], 0.0)
925 };
926 let dvv = project_second(point, weight, dv, dvv_h, dv_weight, dvv_weight, "vv")?;
927
928 let (uv_points, uv_weights) = derivative_net_v(&u_points, &u_weights, &va.knots, va.degree);
929 let (duv_h, duv_weight) = eval_tensor_homogeneous(
930 HomogeneousAxes {
931 u_knots,
932 u_degree: ua.degree - 1,
933 v_knots,
934 v_degree: va.degree - 1,
935 },
936 &uv_points,
937 &uv_weights,
938 uc,
939 vc,
940 );
941 let duv = project_mixed(
942 point, weight, du, du_weight, dv, dv_weight, duv_h, duv_weight,
943 )?;
944
945 Ok(SurfaceJet {
946 point: position,
947 du,
948 dv,
949 duu,
950 duv,
951 dvv,
952 })
953}
954
955fn project_second(
956 point: [Scalar; 3],
957 weight: Scalar,
958 first: Vec3,
959 second: [Scalar; 3],
960 first_weight: Scalar,
961 second_weight: Scalar,
962 axis: &str,
963) -> GeomResult<Vec3> {
964 let position = Vec3::new(point[0], point[1], point[2]) / weight;
965 let value = (Vec3::new(second[0], second[1], second[2])
966 - 2.0 * first_weight * first
967 - second_weight * position)
968 / weight;
969 if value.is_finite() {
970 Ok(value)
971 } else {
972 Err(GeomError::Degenerate(format!(
973 "B-spline surface {axis} second derivative is non-finite"
974 )))
975 }
976}
977
978#[allow(clippy::too_many_arguments)]
979fn project_mixed(
980 point: [Scalar; 3],
981 weight: Scalar,
982 du: Vec3,
983 du_weight: Scalar,
984 dv: Vec3,
985 dv_weight: Scalar,
986 mixed: [Scalar; 3],
987 mixed_weight: Scalar,
988) -> GeomResult<Vec3> {
989 let position = Vec3::new(point[0], point[1], point[2]) / weight;
990 let value = (Vec3::new(mixed[0], mixed[1], mixed[2])
991 - du_weight * dv
992 - dv_weight * du
993 - mixed_weight * position)
994 / weight;
995 if value.is_finite() {
996 Ok(value)
997 } else {
998 Err(GeomError::Degenerate(
999 "B-spline surface uv mixed derivative is non-finite".to_owned(),
1000 ))
1001 }
1002}
1003
1004fn bspline_normal(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<Vec3> {
1006 let (du, dv) = bspline_partials(b, u, v)?;
1007 Ok(du.cross(dv))
1008}
1009
1010#[derive(Debug, Default, Clone, Copy)]
1013pub struct ScalarSurface;
1014
1015impl ScalarSurface {
1016 pub const ID: BackendId = BackendId::new("scalar-reference");
1018}
1019
1020impl axiolid_surface::SurfaceEvaluator<Surface> for ScalarSurface {
1021 type Error = GeomError;
1022
1023 fn evaluate(
1024 &self,
1025 surface: &Surface,
1026 u: Scalar,
1027 v: Scalar,
1028 _tolerance: axiolid_core::Tolerance,
1029 ) -> Result<Point3, Self::Error> {
1030 evaluate(surface, u, v)
1031 }
1032
1033 fn normal(
1034 &self,
1035 surface: &Surface,
1036 u: Scalar,
1037 v: Scalar,
1038 _tolerance: axiolid_core::Tolerance,
1039 ) -> Result<Vec3, Self::Error> {
1040 normal(surface, u, v)
1041 }
1042}
1043
1044pub fn invert(
1061 surface: &Surface,
1062 point: Point3,
1063 tolerance: axiolid_core::Tolerance,
1064) -> GeomResult<(Scalar, Scalar)> {
1065 let (u, v) = match surface {
1066 Surface::Plane(p) => {
1067 let local = to_local(&p.frame, point, tolerance)?;
1068 (local.x, local.y)
1069 }
1070 Surface::Cylinder(c) => {
1071 positive(c.radius, "cylinder radius")?;
1072 let local = to_local(&c.frame, point, tolerance)?;
1073 (angle_about_axis(local, "cylinder")?, local.z)
1074 }
1075 Surface::Cone(c) => {
1076 finite(c.radius, "cone radius")?;
1077 finite(c.semi_angle, "cone semi-angle")?;
1078 let local = to_local(&c.frame, point, tolerance)?;
1079 (angle_about_axis(local, "cone")?, local.z)
1083 }
1084 Surface::Sphere(s) => {
1085 positive(s.radius, "sphere radius")?;
1086 let local = to_local(&s.frame, point, tolerance)?;
1087 let sin_v = (local.z / s.radius).clamp(-1.0, 1.0);
1090 (angle_about_axis(local, "sphere")?, sin_v.asin())
1091 }
1092 Surface::Torus(t) => {
1093 positive(t.major_radius, "torus major radius")?;
1094 positive(t.minor_radius, "torus minor radius")?;
1095 let local = to_local(&t.frame, point, tolerance)?;
1096 let ring = (local.x * local.x + local.y * local.y).sqrt();
1097 (
1098 angle_about_axis(local, "torus")?,
1099 (local.z).atan2(ring - t.major_radius),
1100 )
1101 }
1102 _ => {
1109 return Err(GeomError::Unsupported {
1110 backend: ScalarSurface::ID,
1111 operation: axiolid_contracts::Operation::SurfaceEvaluation,
1112 });
1113 }
1114 };
1115 let round_trip = evaluate(surface, u, v)?;
1118 let residual = (round_trip - point).length();
1119 if residual > tolerance.linear() {
1120 return Err(GeomError::Degenerate(format!(
1121 "point is {residual} from the surface, beyond the {} tolerance: \
1122 inversion names a point ON the surface and does not project",
1123 tolerance.linear()
1124 )));
1125 }
1126 Ok((u, v))
1127}
1128
1129pub fn project(
1153 surface: &Surface,
1154 point: Point3,
1155 tolerance: axiolid_core::Tolerance,
1156) -> GeomResult<(Scalar, Scalar)> {
1157 let ambiguous = |what: &str| {
1158 GeomError::Degenerate(format!(
1159 "{what}: the closest point is not unique, so no projection names it"
1160 ))
1161 };
1162 let (u, v) = match surface {
1163 Surface::Plane(p) => {
1166 let local = to_local(&p.frame, point, tolerance)?;
1167 (local.x, local.y)
1168 }
1169 Surface::Cylinder(c) => {
1172 positive(c.radius, "cylinder radius")?;
1173 let local = to_local(&c.frame, point, tolerance)?;
1174 let ring = (local.x * local.x + local.y * local.y).sqrt();
1175 if ring <= tolerance.linear() {
1176 return Err(ambiguous("point lies on the cylinder axis"));
1177 }
1178 (local.y.atan2(local.x), local.z)
1179 }
1180 Surface::Sphere(s) => {
1182 positive(s.radius, "sphere radius")?;
1183 let local = to_local(&s.frame, point, tolerance)?;
1184 let distance = (local.x * local.x + local.y * local.y + local.z * local.z).sqrt();
1185 if distance <= tolerance.linear() {
1186 return Err(ambiguous("point lies at the sphere centre"));
1187 }
1188 let ring = (local.x * local.x + local.y * local.y).sqrt();
1189 if ring <= tolerance.linear() {
1190 return Err(ambiguous("point lies on the sphere's polar axis"));
1191 }
1192 let sin_v = (local.z / distance).clamp(-1.0, 1.0);
1193 (local.y.atan2(local.x), sin_v.asin())
1194 }
1195 Surface::Cone(c) => {
1199 finite(c.radius, "cone radius")?;
1200 finite(c.semi_angle, "cone semi-angle")?;
1201 let local = to_local(&c.frame, point, tolerance)?;
1202 let ring = (local.x * local.x + local.y * local.y).sqrt();
1203 if ring <= tolerance.linear() {
1204 return Err(ambiguous("point lies on the cone axis"));
1205 }
1206 let slope = c.semi_angle.tan();
1207 if !slope.is_finite() {
1208 return Err(GeomError::Degenerate(
1209 "cone semi-angle is a right angle: the surface degenerates to a plane".into(),
1210 ));
1211 }
1212 let length = (slope * slope + 1.0).sqrt();
1215 let (dr, dz) = (slope / length, 1.0 / length);
1216 let step = (ring - c.radius) * dr + local.z * dz;
1217 let foot_radius = c.radius + step * dr;
1218 if foot_radius < 0.0 {
1221 return Err(GeomError::Degenerate(
1222 "closest point on the cone lies beyond the apex, on the opposite nappe".into(),
1223 ));
1224 }
1225 (local.y.atan2(local.x), step * dz)
1226 }
1227 Surface::Torus(t) => {
1231 positive(t.major_radius, "torus major radius")?;
1232 positive(t.minor_radius, "torus minor radius")?;
1233 let local = to_local(&t.frame, point, tolerance)?;
1234 let ring = (local.x * local.x + local.y * local.y).sqrt();
1235 if ring <= tolerance.linear() {
1236 return Err(ambiguous("point lies on the torus axis"));
1237 }
1238 let planar = ring - t.major_radius;
1239 if planar.abs() <= tolerance.linear() && local.z.abs() <= tolerance.linear() {
1242 return Err(ambiguous("point lies on the torus tube centre circle"));
1243 }
1244 (local.y.atan2(local.x), local.z.atan2(planar))
1245 }
1246 _ => {
1252 return Err(GeomError::Unsupported {
1253 backend: ScalarSurface::ID,
1254 operation: axiolid_contracts::Operation::SurfaceEvaluation,
1255 });
1256 }
1257 };
1258 let landed = evaluate(surface, u, v)?;
1263 invert(surface, landed, tolerance).map_err(|_| {
1264 GeomError::Degenerate(
1265 "projection produced parameters that do not name a point on the surface".into(),
1266 )
1267 })?;
1268 Ok((u, v))
1269}
1270
1271fn to_local(frame: &Frame3, point: Point3, tolerance: Tolerance) -> GeomResult<Vec3> {
1281 let validated = SpaceFrame::new(frame.origin, frame.x, frame.y, frame.z, tolerance)
1287 .map_err(|error| GeomError::Degenerate(format!("surface frame is invalid: {error}")))?;
1288 Ok(validated.to_local(point))
1289}
1290
1291fn angle_about_axis(local: Vec3, surface: &str) -> GeomResult<Scalar> {
1298 let radial = (local.x * local.x + local.y * local.y).sqrt();
1299 if radial <= 1e-12 {
1300 return Err(GeomError::Degenerate(format!(
1301 "{surface} point lies on the axis, where every u names it: \
1302 the angular parameter is not recoverable"
1303 )));
1304 }
1305 Ok(local.y.atan2(local.x))
1306}
1307
1308pub fn locate(
1318 surface: &Surface,
1319 point: Point3,
1320 tolerance: axiolid_core::Tolerance,
1321) -> GeomResult<(Scalar, Scalar)> {
1322 match (invert(surface, point, tolerance), surface) {
1323 (Ok(uv), _) => Ok(uv),
1324 (Err(_), Surface::BSpline(b)) => {
1325 let (u, v) = spline_parameters(b, point)?;
1326 let residual = (evaluate(surface, u, v)? - point).length();
1327 if residual > tolerance.linear() {
1328 return Err(GeomError::Degenerate(format!(
1329 "point is {residual} from the B-spline surface, beyond the {} tolerance",
1330 tolerance.linear()
1331 )));
1332 }
1333 Ok((u, v))
1334 }
1335 (Err(error), _) => Err(error),
1336 }
1337}
1338
1339fn spline_parameters(b: &BSplineSurface, point: Point3) -> GeomResult<(Scalar, Scalar)> {
1343 let ((u0, u1), (v0, v1)) = b
1344 .domain()
1345 .ok_or_else(|| GeomError::InvalidInput("malformed B-spline surface".to_owned()))?;
1346 let n = 24;
1347 let mut best = (Scalar::INFINITY, u0, v0);
1348 for i in 0..=n {
1349 for j in 0..=n {
1350 let (u, v) = (
1351 u0 + (u1 - u0) * i as Scalar / n as Scalar,
1352 v0 + (v1 - v0) * j as Scalar / n as Scalar,
1353 );
1354 if let Some(jet) = b.jet(u, v) {
1355 let d = (jet.point - point).length();
1356 if d < best.0 {
1357 best = (d, u, v);
1358 }
1359 }
1360 }
1361 }
1362 let (_, mut u, mut v) = best;
1363 for _ in 0..60 {
1364 let jet = b
1365 .jet(u, v)
1366 .ok_or_else(|| GeomError::Degenerate("B-spline weight vanished".to_owned()))?;
1367 let r = jet.point - point;
1368 let (a, bb, c) = (jet.u.dot(jet.u), jet.u.dot(jet.v), jet.v.dot(jet.v));
1370 let (g0, g1) = (-jet.u.dot(r), -jet.v.dot(r));
1371 let det = a * c - bb * bb;
1372 if det == 0.0 || !det.is_finite() {
1373 break;
1374 }
1375 let (du, dv) = ((c * g0 - bb * g1) / det, (a * g1 - bb * g0) / det);
1376 let (nu, nv) = ((u + du).clamp(u0, u1), (v + dv).clamp(v0, v1));
1377 let moved = (nu - u).abs() + (nv - v).abs();
1378 (u, v) = (nu, nv);
1379 if moved <= 4.0 * Scalar::EPSILON * (1.0 + u.abs() + v.abs()) {
1380 break;
1381 }
1382 }
1383 Ok((u, v))
1384}