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::EllipticalCylinder(c) => {
376 positive(c.semi_axis_x, "elliptical cylinder semi-axis x")?;
377 positive(c.semi_axis_y, "elliptical cylinder semi-axis y")?;
378 let (a, b) = (c.semi_axis_x, c.semi_axis_y);
379 let (s, co) = u.sin_cos();
380 SurfaceJet {
381 point: elliptical_cylinder_point(c, u, v)?,
382 du: direct(&c.frame, Vec3::new(-a * s, b * co, 0.0)),
383 dv: c.frame.z,
384 duu: direct(&c.frame, Vec3::new(-a * co, -b * s, 0.0)),
385 duv: Vec3::ZERO,
386 dvv: Vec3::ZERO,
387 }
388 }
389 Surface::BSpline(b) => bspline_jet(b, u, v)?,
390 _ => {
391 return Err(GeomError::Unsupported {
392 backend: ScalarSurface::ID,
393 operation: axiolid_contracts::Operation::SurfaceEvaluation,
394 })
395 }
396 };
397 if [
398 value.point,
399 value.du,
400 value.dv,
401 value.duu,
402 value.duv,
403 value.dvv,
404 ]
405 .iter()
406 .all(|vector| vector.is_finite())
407 {
408 Ok(value)
409 } else {
410 Err(GeomError::Degenerate(
411 "surface differential jet is non-finite".to_owned(),
412 ))
413 }
414}
415
416pub fn normal(surface: &Surface, u: Scalar, v: Scalar) -> GeomResult<Vec3> {
422 finite(u, "surface parameter u")?;
423 finite(v, "surface parameter v")?;
424 finite_surface_frame(surface)?;
425 let n = match surface {
426 Surface::Plane(p) => p.frame.z,
427 Surface::Cylinder(c) => {
428 positive(c.radius, "cylinder radius")?;
429 let (s, co) = u.sin_cos();
430 direct(&c.frame, Vec3::new(co, s, 0.0))
431 }
432 Surface::Cone(c) => cone_normal(c, u)?,
433 Surface::EllipticalCylinder(c) => {
434 positive(c.semi_axis_x, "elliptical cylinder semi-axis x")?;
435 positive(c.semi_axis_y, "elliptical cylinder semi-axis y")?;
436 let (su, cu) = u.sin_cos();
442 let along_u = direct(
443 &c.frame,
444 Vec3::new(-c.semi_axis_x * su, c.semi_axis_y * cu, 0.0),
445 );
446 along_u.cross(c.frame.z)
447 }
448 Surface::Sphere(s) => {
449 positive(s.radius, "sphere radius")?;
450 let (su, cu) = u.sin_cos();
451 let (sv, cv) = v.sin_cos();
452 direct(&s.frame, Vec3::new(cv * cu, cv * su, sv))
453 }
454 Surface::Torus(t) => {
455 positive(t.minor_radius, "torus minor radius")?;
456 let (su, cu) = u.sin_cos();
457 let (sv, cv) = v.sin_cos();
458 direct(&t.frame, Vec3::new(cv * cu, cv * su, sv))
459 }
460 Surface::BSpline(b) => bspline_normal(b, u, v)?,
461 _ => {
462 return Err(GeomError::Unsupported {
463 backend: ScalarSurface::ID,
464 operation: axiolid_contracts::Operation::SurfaceEvaluation,
465 })
466 }
467 };
468 let length = n.length();
469 if !(length > 0.0 && n.is_finite()) {
470 return Err(GeomError::Degenerate(format!(
471 "surface normal is not orientable at ({u}, {v})"
472 )));
473 }
474 Ok(n / length)
475}
476
477fn plane_point(p: &Plane, u: Scalar, v: Scalar) -> Point3 {
478 place(&p.frame, Vec3::new(u, v, 0.0))
479}
480
481fn cylinder_point(c: &Cylinder, u: Scalar, v: Scalar) -> GeomResult<Point3> {
482 positive(c.radius, "cylinder radius")?;
483 let (s, co) = u.sin_cos();
484 Ok(place(&c.frame, Vec3::new(c.radius * co, c.radius * s, v)))
485}
486
487fn elliptical_cylinder_point(c: &EllipticalCylinder, u: Scalar, v: Scalar) -> GeomResult<Point3> {
488 positive(c.semi_axis_x, "elliptical cylinder semi-axis x")?;
489 positive(c.semi_axis_y, "elliptical cylinder semi-axis y")?;
490 let (s, co) = u.sin_cos();
491 Ok(place(
492 &c.frame,
493 Vec3::new(c.semi_axis_x * co, c.semi_axis_y * s, v),
494 ))
495}
496
497fn cone_point(c: &Cone, u: Scalar, v: Scalar) -> GeomResult<Point3> {
498 finite(c.radius, "cone radius")?;
499 finite(c.semi_angle, "cone semi-angle")?;
500 let r = c.radius + v * c.semi_angle.tan();
503 if r < 0.0 {
504 return Err(GeomError::Degenerate(format!(
505 "cone radius is negative at v = {v}: the patch crosses the apex"
506 )));
507 }
508 let (s, co) = u.sin_cos();
509 Ok(place(&c.frame, Vec3::new(r * co, r * s, v)))
510}
511
512fn cone_normal(c: &Cone, u: Scalar) -> GeomResult<Vec3> {
513 finite(c.semi_angle, "cone semi-angle")?;
514 let (s, co) = u.sin_cos();
515 let (sa, ca) = c.semi_angle.sin_cos();
518 Ok(direct(&c.frame, Vec3::new(ca * co, ca * s, -sa)))
519}
520
521fn sphere_point(s: &Sphere, u: Scalar, v: Scalar) -> GeomResult<Point3> {
522 positive(s.radius, "sphere radius")?;
523 let (su, cu) = u.sin_cos();
524 let (sv, cv) = v.sin_cos();
525 Ok(place(
526 &s.frame,
527 Vec3::new(s.radius * cv * cu, s.radius * cv * su, s.radius * sv),
528 ))
529}
530
531fn torus_point(t: &Torus, u: Scalar, v: Scalar) -> GeomResult<Point3> {
532 positive(t.major_radius, "torus major radius")?;
533 positive(t.minor_radius, "torus minor radius")?;
534 let (su, cu) = u.sin_cos();
535 let (sv, cv) = v.sin_cos();
536 let ring = t.major_radius + t.minor_radius * cv;
537 Ok(place(
538 &t.frame,
539 Vec3::new(ring * cu, ring * su, t.minor_radius * sv),
540 ))
541}
542
543type Axis = SplineAxis;
546
547fn bspline_axes(b: &BSplineSurface) -> GeomResult<(Axis, Axis)> {
549 let rows = b.control_points.len();
550 if rows == 0 {
551 return Err(GeomError::InvalidInput(
552 "B-spline surface has no control points".to_owned(),
553 ));
554 }
555 let cols = b.control_points[0].len();
556 if cols == 0 {
557 return Err(GeomError::InvalidInput(
558 "B-spline surface control net has an empty row".to_owned(),
559 ));
560 }
561 if b.control_points.iter().any(|row| row.len() != cols) {
564 return Err(GeomError::InvalidInput(
565 "B-spline surface control net is ragged".to_owned(),
566 ));
567 }
568 if b.control_points
569 .iter()
570 .flatten()
571 .any(|point| !point.is_finite())
572 {
573 return Err(GeomError::InvalidInput(
574 "B-spline surface control points must be finite".to_owned(),
575 ));
576 }
577 if let Some(w) = &b.weights {
578 if w.len() != rows || w.iter().any(|row| row.len() != cols) {
579 return Err(GeomError::InvalidInput(
580 "B-spline surface weight net does not match the control net".to_owned(),
581 ));
582 }
583 if w.iter()
584 .flatten()
585 .any(|weight| !weight.is_finite() || *weight <= 0.0)
586 {
587 return Err(GeomError::InvalidInput(
588 "B-spline surface weights must be finite and strictly positive".to_owned(),
589 ));
590 }
591 }
592 let u = Axis::new(&b.u_knots, &b.u_multiplicities, b.u_degree, rows, "u")?;
593 let v = Axis::new(&b.v_knots, &b.v_multiplicities, b.v_degree, cols, "v")?;
594 Ok((u, v))
595}
596
597fn bspline_point(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<Point3> {
603 let (ua, va) = bspline_axes(b)?;
604 let (uc, vc) = (ua.clamp(u), va.clamp(v));
605 let uspan = span_in(&ua.knots, ua.count, ua.degree, uc);
606 let vspan = span_in(&va.knots, va.count, va.degree, vc);
607
608 let mut row_points: Vec<[Scalar; 3]> = Vec::with_capacity(ua.degree + 1);
610 let mut row_weights: Vec<Scalar> = Vec::with_capacity(ua.degree + 1);
611 for i in 0..=ua.degree {
612 let row = uspan - ua.degree + i;
613 let mut pts: Vec<[Scalar; 3]> = Vec::with_capacity(va.degree + 1);
614 let mut wts: Vec<Scalar> = Vec::with_capacity(va.degree + 1);
615 for j in 0..=va.degree {
616 let col = vspan - va.degree + j;
617 let w = b.weights.as_ref().map_or(1.0, |ws| ws[row][col]);
618 let p = b.control_points[row][col];
619 let homogeneous = [p.x * w, p.y * w, p.z * w];
620 if homogeneous.iter().any(|value| !value.is_finite()) {
621 return Err(GeomError::Degenerate(
622 "B-spline surface homogeneous control point overflowed".to_owned(),
623 ));
624 }
625 pts.push(homogeneous);
626 wts.push(w);
627 }
628 de_boor_recurrence(&va.knots, vspan, va.degree, vc, &mut pts, &mut wts);
629 row_points.push(pts[va.degree]);
630 row_weights.push(wts[va.degree]);
631 }
632
633 de_boor_recurrence(
635 &ua.knots,
636 uspan,
637 ua.degree,
638 uc,
639 &mut row_points,
640 &mut row_weights,
641 );
642
643 let w = row_weights[ua.degree];
644 if !w.is_finite() || w == 0.0 {
645 return Err(GeomError::Degenerate(
646 "B-spline surface weight collapsed to zero".to_owned(),
647 ));
648 }
649 let p = row_points[ua.degree];
650 Ok(Point3::new(p[0] / w, p[1] / w, p[2] / w))
651}
652
653#[derive(Clone, Copy)]
655struct HomogeneousAxes<'a> {
656 u_knots: &'a [Scalar],
657 u_degree: usize,
658 v_knots: &'a [Scalar],
659 v_degree: usize,
660}
661
662fn eval_tensor_homogeneous(
664 axes: HomogeneousAxes<'_>,
665 points: &[Vec<[Scalar; 3]>],
666 weights: &[Vec<Scalar>],
667 u: Scalar,
668 v: Scalar,
669) -> ([Scalar; 3], Scalar) {
670 let mut row_points = Vec::with_capacity(points.len());
671 let mut row_weights = Vec::with_capacity(points.len());
672 for (row_points_h, row_weights_h) in points.iter().zip(weights) {
673 let (point, weight) =
674 eval_homogeneous(axes.v_knots, axes.v_degree, row_points_h, row_weights_h, v);
675 row_points.push(point);
676 row_weights.push(weight);
677 }
678 eval_homogeneous(axes.u_knots, axes.u_degree, &row_points, &row_weights, u)
679}
680
681type HomogeneousPointNet = Vec<Vec<[Scalar; 3]>>;
682type HomogeneousWeightNet = Vec<Vec<Scalar>>;
683
684fn homogeneous_control_net(
686 b: &BSplineSurface,
687) -> GeomResult<(HomogeneousPointNet, HomogeneousWeightNet)> {
688 let mut points = Vec::with_capacity(b.control_points.len());
689 let mut weights = Vec::with_capacity(b.control_points.len());
690 for (i, row) in b.control_points.iter().enumerate() {
691 let mut point_row = Vec::with_capacity(row.len());
692 let mut weight_row = Vec::with_capacity(row.len());
693 for (j, point) in row.iter().enumerate() {
694 let weight = b.weights.as_ref().map_or(1.0, |net| net[i][j]);
695 let homogeneous = [point.x * weight, point.y * weight, point.z * weight];
696 if homogeneous.iter().any(|value| !value.is_finite()) {
697 return Err(GeomError::Degenerate(
698 "B-spline surface homogeneous control point overflowed".to_owned(),
699 ));
700 }
701 point_row.push(homogeneous);
702 weight_row.push(weight);
703 }
704 points.push(point_row);
705 weights.push(weight_row);
706 }
707 Ok((points, weights))
708}
709
710fn derivative_net_u(
712 points: &[Vec<[Scalar; 3]>],
713 weights: &[Vec<Scalar>],
714 knots: &[Scalar],
715 degree: usize,
716) -> (Vec<Vec<[Scalar; 3]>>, Vec<Vec<Scalar>>) {
717 let rows = points.len() - 1;
718 let cols = points[0].len();
719 let mut derivative_points = Vec::with_capacity(rows);
720 let mut derivative_weights = Vec::with_capacity(rows);
721 for i in 0..rows {
722 let denominator = knots[i + degree + 1] - knots[i + 1];
723 let factor = if denominator.abs() > 0.0 {
724 degree as Scalar / denominator
725 } else {
726 0.0
727 };
728 let mut point_row = Vec::with_capacity(cols);
729 let mut weight_row = Vec::with_capacity(cols);
730 for j in 0..cols {
731 point_row.push(core::array::from_fn(|k| {
732 factor * (points[i + 1][j][k] - points[i][j][k])
733 }));
734 weight_row.push(factor * (weights[i + 1][j] - weights[i][j]));
735 }
736 derivative_points.push(point_row);
737 derivative_weights.push(weight_row);
738 }
739 (derivative_points, derivative_weights)
740}
741
742fn derivative_net_v(
744 points: &[Vec<[Scalar; 3]>],
745 weights: &[Vec<Scalar>],
746 knots: &[Scalar],
747 degree: usize,
748) -> (Vec<Vec<[Scalar; 3]>>, Vec<Vec<Scalar>>) {
749 let rows = points.len();
750 let cols = points[0].len() - 1;
751 let mut derivative_points = Vec::with_capacity(rows);
752 let mut derivative_weights = Vec::with_capacity(rows);
753 for i in 0..rows {
754 let mut point_row = Vec::with_capacity(cols);
755 let mut weight_row = Vec::with_capacity(cols);
756 for j in 0..cols {
757 let denominator = knots[j + degree + 1] - knots[j + 1];
758 let factor = if denominator.abs() > 0.0 {
759 degree as Scalar / denominator
760 } else {
761 0.0
762 };
763 point_row.push(core::array::from_fn(|k| {
764 factor * (points[i][j + 1][k] - points[i][j][k])
765 }));
766 weight_row.push(factor * (weights[i][j + 1] - weights[i][j]));
767 }
768 derivative_points.push(point_row);
769 derivative_weights.push(weight_row);
770 }
771 (derivative_points, derivative_weights)
772}
773
774fn project_derivative(
776 point: [Scalar; 3],
777 weight: Scalar,
778 derivative: [Scalar; 3],
779 derivative_weight: Scalar,
780 axis: &str,
781) -> GeomResult<Vec3> {
782 if !weight.is_finite() || weight == 0.0 {
783 return Err(GeomError::Degenerate(
784 "B-spline surface weight collapsed to a non-finite or zero value".to_owned(),
785 ));
786 }
787 let value = Vec3::new(
788 (derivative[0] - point[0] * derivative_weight / weight) / weight,
789 (derivative[1] - point[1] * derivative_weight / weight) / weight,
790 (derivative[2] - point[2] * derivative_weight / weight) / weight,
791 );
792 if !value.is_finite() {
793 return Err(GeomError::Degenerate(format!(
794 "B-spline surface {axis} derivative is non-finite"
795 )));
796 }
797 Ok(value)
798}
799
800fn bspline_partials(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<(Vec3, Vec3)> {
802 let (ua, va) = bspline_axes(b)?;
803 let (uc, vc) = (ua.clamp(u), va.clamp(v));
804 let (points, weights) = homogeneous_control_net(b)?;
805 let (point, weight) = eval_tensor_homogeneous(
806 HomogeneousAxes {
807 u_knots: &ua.knots,
808 u_degree: ua.degree,
809 v_knots: &va.knots,
810 v_degree: va.degree,
811 },
812 &points,
813 &weights,
814 uc,
815 vc,
816 );
817
818 let (u_points, u_weights) = derivative_net_u(&points, &weights, &ua.knots, ua.degree);
819 let (du, du_weight) = eval_tensor_homogeneous(
820 HomogeneousAxes {
821 u_knots: &ua.knots[1..ua.knots.len() - 1],
822 u_degree: ua.degree - 1,
823 v_knots: &va.knots,
824 v_degree: va.degree,
825 },
826 &u_points,
827 &u_weights,
828 uc,
829 vc,
830 );
831
832 let (v_points, v_weights) = derivative_net_v(&points, &weights, &va.knots, va.degree);
833 let (dv, dv_weight) = eval_tensor_homogeneous(
834 HomogeneousAxes {
835 u_knots: &ua.knots,
836 u_degree: ua.degree,
837 v_knots: &va.knots[1..va.knots.len() - 1],
838 v_degree: va.degree - 1,
839 },
840 &v_points,
841 &v_weights,
842 uc,
843 vc,
844 );
845
846 Ok((
847 project_derivative(point, weight, du, du_weight, "u")?,
848 project_derivative(point, weight, dv, dv_weight, "v")?,
849 ))
850}
851
852pub fn bspline_jet(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<SurfaceJet> {
855 let (ua, va) = bspline_axes(b)?;
856 let (uc, vc) = (ua.clamp(u), va.clamp(v));
857 let (points, weights) = homogeneous_control_net(b)?;
858 let base_axes = HomogeneousAxes {
859 u_knots: &ua.knots,
860 u_degree: ua.degree,
861 v_knots: &va.knots,
862 v_degree: va.degree,
863 };
864 let (point, weight) = eval_tensor_homogeneous(base_axes, &points, &weights, uc, vc);
865 if !weight.is_finite() || weight == 0.0 {
866 return Err(GeomError::Degenerate(
867 "B-spline surface weight collapsed to a non-finite or zero value".to_owned(),
868 ));
869 }
870 let position = Point3::new(point[0] / weight, point[1] / weight, point[2] / weight);
871
872 let (u_points, u_weights) = derivative_net_u(&points, &weights, &ua.knots, ua.degree);
873 let u_knots = &ua.knots[1..ua.knots.len() - 1];
874 let (du_h, du_weight) = eval_tensor_homogeneous(
875 HomogeneousAxes {
876 u_knots,
877 u_degree: ua.degree - 1,
878 v_knots: &va.knots,
879 v_degree: va.degree,
880 },
881 &u_points,
882 &u_weights,
883 uc,
884 vc,
885 );
886 let du = project_derivative(point, weight, du_h, du_weight, "u")?;
887
888 let (v_points, v_weights) = derivative_net_v(&points, &weights, &va.knots, va.degree);
889 let v_knots = &va.knots[1..va.knots.len() - 1];
890 let (dv_h, dv_weight) = eval_tensor_homogeneous(
891 HomogeneousAxes {
892 u_knots: &ua.knots,
893 u_degree: ua.degree,
894 v_knots,
895 v_degree: va.degree - 1,
896 },
897 &v_points,
898 &v_weights,
899 uc,
900 vc,
901 );
902 let dv = project_derivative(point, weight, dv_h, dv_weight, "v")?;
903
904 let (duu_h, duu_weight) = if ua.degree >= 2 {
905 let (net, net_weights) = derivative_net_u(&u_points, &u_weights, u_knots, ua.degree - 1);
906 eval_tensor_homogeneous(
907 HomogeneousAxes {
908 u_knots: &u_knots[1..u_knots.len() - 1],
909 u_degree: ua.degree - 2,
910 v_knots: &va.knots,
911 v_degree: va.degree,
912 },
913 &net,
914 &net_weights,
915 uc,
916 vc,
917 )
918 } else {
919 ([0.0; 3], 0.0)
920 };
921 let duu = project_second(point, weight, du, duu_h, du_weight, duu_weight, "uu")?;
922
923 let (dvv_h, dvv_weight) = if va.degree >= 2 {
924 let (net, net_weights) = derivative_net_v(&v_points, &v_weights, v_knots, va.degree - 1);
925 eval_tensor_homogeneous(
926 HomogeneousAxes {
927 u_knots: &ua.knots,
928 u_degree: ua.degree,
929 v_knots: &v_knots[1..v_knots.len() - 1],
930 v_degree: va.degree - 2,
931 },
932 &net,
933 &net_weights,
934 uc,
935 vc,
936 )
937 } else {
938 ([0.0; 3], 0.0)
939 };
940 let dvv = project_second(point, weight, dv, dvv_h, dv_weight, dvv_weight, "vv")?;
941
942 let (uv_points, uv_weights) = derivative_net_v(&u_points, &u_weights, &va.knots, va.degree);
943 let (duv_h, duv_weight) = eval_tensor_homogeneous(
944 HomogeneousAxes {
945 u_knots,
946 u_degree: ua.degree - 1,
947 v_knots,
948 v_degree: va.degree - 1,
949 },
950 &uv_points,
951 &uv_weights,
952 uc,
953 vc,
954 );
955 let duv = project_mixed(
956 point, weight, du, du_weight, dv, dv_weight, duv_h, duv_weight,
957 )?;
958
959 Ok(SurfaceJet {
960 point: position,
961 du,
962 dv,
963 duu,
964 duv,
965 dvv,
966 })
967}
968
969fn project_second(
970 point: [Scalar; 3],
971 weight: Scalar,
972 first: Vec3,
973 second: [Scalar; 3],
974 first_weight: Scalar,
975 second_weight: Scalar,
976 axis: &str,
977) -> GeomResult<Vec3> {
978 let position = Vec3::new(point[0], point[1], point[2]) / weight;
979 let value = (Vec3::new(second[0], second[1], second[2])
980 - 2.0 * first_weight * first
981 - second_weight * position)
982 / weight;
983 if value.is_finite() {
984 Ok(value)
985 } else {
986 Err(GeomError::Degenerate(format!(
987 "B-spline surface {axis} second derivative is non-finite"
988 )))
989 }
990}
991
992#[allow(clippy::too_many_arguments)]
993fn project_mixed(
994 point: [Scalar; 3],
995 weight: Scalar,
996 du: Vec3,
997 du_weight: Scalar,
998 dv: Vec3,
999 dv_weight: Scalar,
1000 mixed: [Scalar; 3],
1001 mixed_weight: Scalar,
1002) -> GeomResult<Vec3> {
1003 let position = Vec3::new(point[0], point[1], point[2]) / weight;
1004 let value = (Vec3::new(mixed[0], mixed[1], mixed[2])
1005 - du_weight * dv
1006 - dv_weight * du
1007 - mixed_weight * position)
1008 / weight;
1009 if value.is_finite() {
1010 Ok(value)
1011 } else {
1012 Err(GeomError::Degenerate(
1013 "B-spline surface uv mixed derivative is non-finite".to_owned(),
1014 ))
1015 }
1016}
1017
1018fn bspline_normal(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<Vec3> {
1020 let (du, dv) = bspline_partials(b, u, v)?;
1021 Ok(du.cross(dv))
1022}
1023
1024#[derive(Debug, Default, Clone, Copy)]
1027pub struct ScalarSurface;
1028
1029impl ScalarSurface {
1030 pub const ID: BackendId = BackendId::new("scalar-reference");
1032}
1033
1034impl axiolid_surface::SurfaceEvaluator<Surface> for ScalarSurface {
1035 type Error = GeomError;
1036
1037 fn evaluate(
1038 &self,
1039 surface: &Surface,
1040 u: Scalar,
1041 v: Scalar,
1042 _tolerance: axiolid_core::Tolerance,
1043 ) -> Result<Point3, Self::Error> {
1044 evaluate(surface, u, v)
1045 }
1046
1047 fn normal(
1048 &self,
1049 surface: &Surface,
1050 u: Scalar,
1051 v: Scalar,
1052 _tolerance: axiolid_core::Tolerance,
1053 ) -> Result<Vec3, Self::Error> {
1054 normal(surface, u, v)
1055 }
1056}
1057
1058pub fn invert(
1075 surface: &Surface,
1076 point: Point3,
1077 tolerance: axiolid_core::Tolerance,
1078) -> GeomResult<(Scalar, Scalar)> {
1079 let (u, v) = match surface {
1080 Surface::Plane(p) => {
1081 let local = to_local(&p.frame, point, tolerance)?;
1082 (local.x, local.y)
1083 }
1084 Surface::Cylinder(c) => {
1085 positive(c.radius, "cylinder radius")?;
1086 let local = to_local(&c.frame, point, tolerance)?;
1087 (angle_about_axis(local, "cylinder")?, local.z)
1088 }
1089 Surface::EllipticalCylinder(c) => {
1093 positive(c.semi_axis_x, "elliptical cylinder semi-axis x")?;
1094 positive(c.semi_axis_y, "elliptical cylinder semi-axis y")?;
1095 let local = to_local(&c.frame, point, tolerance)?;
1096 let scaled = Vec3::new(local.x / c.semi_axis_x, local.y / c.semi_axis_y, local.z);
1097 (angle_about_axis(scaled, "elliptical cylinder")?, local.z)
1098 }
1099 Surface::Cone(c) => {
1100 finite(c.radius, "cone radius")?;
1101 finite(c.semi_angle, "cone semi-angle")?;
1102 let local = to_local(&c.frame, point, tolerance)?;
1103 (angle_about_axis(local, "cone")?, local.z)
1107 }
1108 Surface::Sphere(s) => {
1109 positive(s.radius, "sphere radius")?;
1110 let local = to_local(&s.frame, point, tolerance)?;
1111 let sin_v = (local.z / s.radius).clamp(-1.0, 1.0);
1114 (angle_about_axis(local, "sphere")?, sin_v.asin())
1115 }
1116 Surface::Torus(t) => {
1117 positive(t.major_radius, "torus major radius")?;
1118 positive(t.minor_radius, "torus minor radius")?;
1119 let local = to_local(&t.frame, point, tolerance)?;
1120 let ring = (local.x * local.x + local.y * local.y).sqrt();
1121 (
1122 angle_about_axis(local, "torus")?,
1123 (local.z).atan2(ring - t.major_radius),
1124 )
1125 }
1126 _ => {
1133 return Err(GeomError::Unsupported {
1134 backend: ScalarSurface::ID,
1135 operation: axiolid_contracts::Operation::SurfaceEvaluation,
1136 });
1137 }
1138 };
1139 let round_trip = evaluate(surface, u, v)?;
1142 let residual = (round_trip - point).length();
1143 if residual > tolerance.linear() {
1144 return Err(GeomError::Degenerate(format!(
1145 "point is {residual} from the surface, beyond the {} tolerance: \
1146 inversion names a point ON the surface and does not project",
1147 tolerance.linear()
1148 )));
1149 }
1150 Ok((u, v))
1151}
1152
1153pub fn project(
1177 surface: &Surface,
1178 point: Point3,
1179 tolerance: axiolid_core::Tolerance,
1180) -> GeomResult<(Scalar, Scalar)> {
1181 let ambiguous = |what: &str| {
1182 GeomError::Degenerate(format!(
1183 "{what}: the closest point is not unique, so no projection names it"
1184 ))
1185 };
1186 let (u, v) = match surface {
1187 Surface::Plane(p) => {
1190 let local = to_local(&p.frame, point, tolerance)?;
1191 (local.x, local.y)
1192 }
1193 Surface::Cylinder(c) => {
1196 positive(c.radius, "cylinder radius")?;
1197 let local = to_local(&c.frame, point, tolerance)?;
1198 let ring = (local.x * local.x + local.y * local.y).sqrt();
1199 if ring <= tolerance.linear() {
1200 return Err(ambiguous("point lies on the cylinder axis"));
1201 }
1202 (local.y.atan2(local.x), local.z)
1203 }
1204 Surface::Sphere(s) => {
1206 positive(s.radius, "sphere radius")?;
1207 let local = to_local(&s.frame, point, tolerance)?;
1208 let distance = (local.x * local.x + local.y * local.y + local.z * local.z).sqrt();
1209 if distance <= tolerance.linear() {
1210 return Err(ambiguous("point lies at the sphere centre"));
1211 }
1212 let ring = (local.x * local.x + local.y * local.y).sqrt();
1213 if ring <= tolerance.linear() {
1214 return Err(ambiguous("point lies on the sphere's polar axis"));
1215 }
1216 let sin_v = (local.z / distance).clamp(-1.0, 1.0);
1217 (local.y.atan2(local.x), sin_v.asin())
1218 }
1219 Surface::Cone(c) => {
1223 finite(c.radius, "cone radius")?;
1224 finite(c.semi_angle, "cone semi-angle")?;
1225 let local = to_local(&c.frame, point, tolerance)?;
1226 let ring = (local.x * local.x + local.y * local.y).sqrt();
1227 if ring <= tolerance.linear() {
1228 return Err(ambiguous("point lies on the cone axis"));
1229 }
1230 let slope = c.semi_angle.tan();
1231 if !slope.is_finite() {
1232 return Err(GeomError::Degenerate(
1233 "cone semi-angle is a right angle: the surface degenerates to a plane".into(),
1234 ));
1235 }
1236 let length = (slope * slope + 1.0).sqrt();
1239 let (dr, dz) = (slope / length, 1.0 / length);
1240 let step = (ring - c.radius) * dr + local.z * dz;
1241 let foot_radius = c.radius + step * dr;
1242 if foot_radius < 0.0 {
1245 return Err(GeomError::Degenerate(
1246 "closest point on the cone lies beyond the apex, on the opposite nappe".into(),
1247 ));
1248 }
1249 (local.y.atan2(local.x), step * dz)
1250 }
1251 Surface::Torus(t) => {
1255 positive(t.major_radius, "torus major radius")?;
1256 positive(t.minor_radius, "torus minor radius")?;
1257 let local = to_local(&t.frame, point, tolerance)?;
1258 let ring = (local.x * local.x + local.y * local.y).sqrt();
1259 if ring <= tolerance.linear() {
1260 return Err(ambiguous("point lies on the torus axis"));
1261 }
1262 let planar = ring - t.major_radius;
1263 if planar.abs() <= tolerance.linear() && local.z.abs() <= tolerance.linear() {
1266 return Err(ambiguous("point lies on the torus tube centre circle"));
1267 }
1268 (local.y.atan2(local.x), local.z.atan2(planar))
1269 }
1270 _ => {
1276 return Err(GeomError::Unsupported {
1277 backend: ScalarSurface::ID,
1278 operation: axiolid_contracts::Operation::SurfaceEvaluation,
1279 });
1280 }
1281 };
1282 let landed = evaluate(surface, u, v)?;
1287 invert(surface, landed, tolerance).map_err(|_| {
1288 GeomError::Degenerate(
1289 "projection produced parameters that do not name a point on the surface".into(),
1290 )
1291 })?;
1292 Ok((u, v))
1293}
1294
1295fn to_local(frame: &Frame3, point: Point3, tolerance: Tolerance) -> GeomResult<Vec3> {
1305 let validated = SpaceFrame::new(frame.origin, frame.x, frame.y, frame.z, tolerance)
1311 .map_err(|error| GeomError::Degenerate(format!("surface frame is invalid: {error}")))?;
1312 Ok(validated.to_local(point))
1313}
1314
1315fn angle_about_axis(local: Vec3, surface: &str) -> GeomResult<Scalar> {
1322 let radial = (local.x * local.x + local.y * local.y).sqrt();
1323 if radial <= 1e-12 {
1324 return Err(GeomError::Degenerate(format!(
1325 "{surface} point lies on the axis, where every u names it: \
1326 the angular parameter is not recoverable"
1327 )));
1328 }
1329 Ok(local.y.atan2(local.x))
1330}
1331
1332pub fn locate(
1342 surface: &Surface,
1343 point: Point3,
1344 tolerance: axiolid_core::Tolerance,
1345) -> GeomResult<(Scalar, Scalar)> {
1346 match (invert(surface, point, tolerance), surface) {
1347 (Ok(uv), _) => Ok(uv),
1348 (Err(_), Surface::BSpline(b)) => {
1349 let (u, v) = spline_parameters(b, point)?;
1350 let residual = (evaluate(surface, u, v)? - point).length();
1351 if residual > tolerance.linear() {
1352 return Err(GeomError::Degenerate(format!(
1353 "point is {residual} from the B-spline surface, beyond the {} tolerance",
1354 tolerance.linear()
1355 )));
1356 }
1357 Ok((u, v))
1358 }
1359 (Err(error), _) => Err(error),
1360 }
1361}
1362
1363fn spline_parameters(b: &BSplineSurface, point: Point3) -> GeomResult<(Scalar, Scalar)> {
1367 let ((u0, u1), (v0, v1)) = b
1368 .domain()
1369 .ok_or_else(|| GeomError::InvalidInput("malformed B-spline surface".to_owned()))?;
1370 let n = 24;
1371 let mut best = (Scalar::INFINITY, u0, v0);
1372 for i in 0..=n {
1373 for j in 0..=n {
1374 let (u, v) = (
1375 u0 + (u1 - u0) * i as Scalar / n as Scalar,
1376 v0 + (v1 - v0) * j as Scalar / n as Scalar,
1377 );
1378 if let Some(jet) = b.jet(u, v) {
1379 let d = (jet.point - point).length();
1380 if d < best.0 {
1381 best = (d, u, v);
1382 }
1383 }
1384 }
1385 }
1386 let (_, mut u, mut v) = best;
1387 for _ in 0..60 {
1388 let jet = b
1389 .jet(u, v)
1390 .ok_or_else(|| GeomError::Degenerate("B-spline weight vanished".to_owned()))?;
1391 let r = jet.point - point;
1392 let (a, bb, c) = (jet.u.dot(jet.u), jet.u.dot(jet.v), jet.v.dot(jet.v));
1394 let (g0, g1) = (-jet.u.dot(r), -jet.v.dot(r));
1395 let det = a * c - bb * bb;
1396 if det == 0.0 || !det.is_finite() {
1397 break;
1398 }
1399 let (du, dv) = ((c * g0 - bb * g1) / det, (a * g1 - bb * g0) / det);
1400 let (nu, nv) = ((u + du).clamp(u0, u1), (v + dv).clamp(v0, v1));
1401 let moved = (nu - u).abs() + (nv - v).abs();
1402 (u, v) = (nu, nv);
1403 if moved <= 4.0 * Scalar::EPSILON * (1.0 + u.abs() + v.abs()) {
1404 break;
1405 }
1406 }
1407 Ok((u, v))
1408}