1use axiolid_contracts::BackendId;
38use axiolid_contracts::{GeomError, GeomResult};
39use axiolid_core::{Frame3, Point3, Scalar, Vec3};
40use axiolid_surface::{BSplineSurface, Cone, Cylinder, Plane, Sphere, Surface, Torus};
41
42use crate::curve::{de_boor_recurrence, eval_homogeneous, span_in};
43use crate::nurbs::SplineAxis;
44
45#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct Patch {
52 pub u_start: Scalar,
54 pub u_end: Scalar,
56 pub v_start: Scalar,
58 pub v_end: Scalar,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq)]
64pub struct SurfaceJet {
65 pub point: Point3,
67 pub du: Vec3,
69 pub dv: Vec3,
71 pub duu: Vec3,
73 pub duv: Vec3,
75 pub dvv: Vec3,
77}
78
79impl Patch {
80 pub fn new(u_start: Scalar, u_end: Scalar, v_start: Scalar, v_end: Scalar) -> GeomResult<Self> {
82 let all = [u_start, u_end, v_start, v_end];
83 if !all.iter().all(|value| value.is_finite()) {
84 return Err(GeomError::InvalidInput(format!(
85 "patch bounds must be finite, got {all:?}"
86 )));
87 }
88 if !(u_end > u_start && v_end > v_start) {
89 return Err(GeomError::Degenerate(format!(
90 "patch must have positive extent, got u {u_start}..{u_end}, v {v_start}..{v_end}"
91 )));
92 }
93 Ok(Self {
94 u_start,
95 u_end,
96 v_start,
97 v_end,
98 })
99 }
100
101 pub fn full_turn(v_start: Scalar, v_end: Scalar) -> GeomResult<Self> {
103 Self::new(0.0, core::f64::consts::TAU, v_start, v_end)
104 }
105}
106
107fn place(frame: &Frame3, local: Vec3) -> Point3 {
109 frame.origin + frame.x * local.x + frame.y * local.y + frame.z * local.z
110}
111
112fn direct(frame: &Frame3, local: Vec3) -> Vec3 {
114 frame.x * local.x + frame.y * local.y + frame.z * local.z
115}
116
117fn finite(value: Scalar, what: &str) -> GeomResult<()> {
118 if value.is_finite() {
119 Ok(())
120 } else {
121 Err(GeomError::InvalidInput(format!(
122 "{what} must be finite, got {value}"
123 )))
124 }
125}
126
127fn positive(value: Scalar, what: &str) -> GeomResult<()> {
128 finite(value, what)?;
129 if value > 0.0 {
130 Ok(())
131 } else {
132 Err(GeomError::InvalidInput(format!(
133 "{what} must be positive, got {value}"
134 )))
135 }
136}
137
138fn finite_surface_frame(surface: &Surface) -> GeomResult<()> {
139 let frame = match surface {
140 Surface::Plane(value) => Some(&value.frame),
141 Surface::Cylinder(value) => Some(&value.frame),
142 Surface::Cone(value) => Some(&value.frame),
143 Surface::Sphere(value) => Some(&value.frame),
144 Surface::Torus(value) => Some(&value.frame),
145 _ => None,
146 };
147 if frame.is_none_or(|frame| {
148 frame.origin.is_finite()
149 && frame.x.is_finite()
150 && frame.y.is_finite()
151 && frame.z.is_finite()
152 }) {
153 Ok(())
154 } else {
155 Err(GeomError::InvalidInput(
156 "surface frame must be finite".to_owned(),
157 ))
158 }
159}
160
161pub fn evaluate(surface: &Surface, u: Scalar, v: Scalar) -> GeomResult<Point3> {
163 finite(u, "surface parameter u")?;
164 finite(v, "surface parameter v")?;
165 finite_surface_frame(surface)?;
166 let point = match surface {
167 Surface::Plane(p) => Ok(plane_point(p, u, v)),
168 Surface::Cylinder(c) => cylinder_point(c, u, v),
169 Surface::Cone(c) => cone_point(c, u, v),
170 Surface::Sphere(s) => sphere_point(s, u, v),
171 Surface::Torus(t) => torus_point(t, u, v),
172 Surface::BSpline(b) => bspline_point(b, u, v),
173 _ => Err(GeomError::Unsupported {
174 backend: ScalarSurface::ID,
175 operation: axiolid_contracts::Operation::SurfaceEvaluation,
176 }),
177 }?;
178 if point.is_finite() {
179 Ok(point)
180 } else {
181 Err(GeomError::Degenerate(
182 "surface point is non-finite".to_owned(),
183 ))
184 }
185}
186
187pub fn partials(surface: &Surface, u: Scalar, v: Scalar) -> GeomResult<(Vec3, Vec3)> {
193 finite(u, "surface parameter u")?;
194 finite(v, "surface parameter v")?;
195 finite_surface_frame(surface)?;
196 let value = match surface {
197 Surface::Plane(p) => Ok((p.frame.x, p.frame.y)),
198 Surface::Cylinder(c) => {
199 positive(c.radius, "cylinder radius")?;
200 let (s, co) = u.sin_cos();
201 Ok((
202 direct(&c.frame, Vec3::new(-c.radius * s, c.radius * co, 0.0)),
203 c.frame.z,
204 ))
205 }
206 Surface::Cone(c) => {
207 finite(c.radius, "cone radius")?;
208 finite(c.semi_angle, "cone semi-angle")?;
209 let slope = c.semi_angle.tan();
210 let radius = c.radius + v * slope;
211 if radius < 0.0 {
212 return Err(GeomError::Degenerate(format!(
213 "cone radius is negative at v = {v}: the patch crosses the apex"
214 )));
215 }
216 let (s, co) = u.sin_cos();
217 Ok((
218 direct(&c.frame, Vec3::new(-radius * s, radius * co, 0.0)),
219 direct(&c.frame, Vec3::new(slope * co, slope * s, 1.0)),
220 ))
221 }
222 Surface::Sphere(sphere) => {
223 positive(sphere.radius, "sphere radius")?;
224 let (su, cu) = u.sin_cos();
225 let (sv, cv) = v.sin_cos();
226 Ok((
227 direct(
228 &sphere.frame,
229 Vec3::new(-sphere.radius * cv * su, sphere.radius * cv * cu, 0.0),
230 ),
231 direct(
232 &sphere.frame,
233 Vec3::new(
234 -sphere.radius * sv * cu,
235 -sphere.radius * sv * su,
236 sphere.radius * cv,
237 ),
238 ),
239 ))
240 }
241 Surface::Torus(torus) => {
242 positive(torus.major_radius, "torus major radius")?;
243 positive(torus.minor_radius, "torus minor radius")?;
244 let (su, cu) = u.sin_cos();
245 let (sv, cv) = v.sin_cos();
246 let ring = torus.major_radius + torus.minor_radius * cv;
247 Ok((
248 direct(&torus.frame, Vec3::new(-ring * su, ring * cu, 0.0)),
249 direct(
250 &torus.frame,
251 Vec3::new(
252 -torus.minor_radius * sv * cu,
253 -torus.minor_radius * sv * su,
254 torus.minor_radius * cv,
255 ),
256 ),
257 ))
258 }
259 Surface::BSpline(b) => bspline_partials(b, u, v),
260 _ => Err(GeomError::Unsupported {
261 backend: ScalarSurface::ID,
262 operation: axiolid_contracts::Operation::SurfaceEvaluation,
263 }),
264 }?;
265 if value.0.is_finite() && value.1.is_finite() {
266 Ok(value)
267 } else {
268 Err(GeomError::Degenerate(
269 "surface partial is non-finite".to_owned(),
270 ))
271 }
272}
273
274pub fn jet(surface: &Surface, u: Scalar, v: Scalar) -> GeomResult<SurfaceJet> {
279 finite(u, "surface parameter u")?;
280 finite(v, "surface parameter v")?;
281 finite_surface_frame(surface)?;
282 let value = match surface {
283 Surface::Plane(p) => SurfaceJet {
284 point: plane_point(p, u, v),
285 du: p.frame.x,
286 dv: p.frame.y,
287 duu: Vec3::ZERO,
288 duv: Vec3::ZERO,
289 dvv: Vec3::ZERO,
290 },
291 Surface::Cylinder(c) => {
292 positive(c.radius, "cylinder radius")?;
293 let (s, co) = u.sin_cos();
294 SurfaceJet {
295 point: cylinder_point(c, u, v)?,
296 du: direct(&c.frame, Vec3::new(-c.radius * s, c.radius * co, 0.0)),
297 dv: c.frame.z,
298 duu: direct(&c.frame, Vec3::new(-c.radius * co, -c.radius * s, 0.0)),
299 duv: Vec3::ZERO,
300 dvv: Vec3::ZERO,
301 }
302 }
303 Surface::Cone(c) => {
304 finite(c.radius, "cone radius")?;
305 finite(c.semi_angle, "cone semi-angle")?;
306 let slope = c.semi_angle.tan();
307 let radius = c.radius + v * slope;
308 if radius < 0.0 {
309 return Err(GeomError::Degenerate(format!(
310 "cone radius is negative at v = {v}: the patch crosses the apex"
311 )));
312 }
313 let (s, co) = u.sin_cos();
314 SurfaceJet {
315 point: cone_point(c, u, v)?,
316 du: direct(&c.frame, Vec3::new(-radius * s, radius * co, 0.0)),
317 dv: direct(&c.frame, Vec3::new(slope * co, slope * s, 1.0)),
318 duu: direct(&c.frame, Vec3::new(-radius * co, -radius * s, 0.0)),
319 duv: direct(&c.frame, Vec3::new(-slope * s, slope * co, 0.0)),
320 dvv: Vec3::ZERO,
321 }
322 }
323 Surface::Sphere(sphere) => {
324 positive(sphere.radius, "sphere radius")?;
325 let r = sphere.radius;
326 let (su, cu) = u.sin_cos();
327 let (sv, cv) = v.sin_cos();
328 SurfaceJet {
329 point: sphere_point(sphere, u, v)?,
330 du: direct(&sphere.frame, Vec3::new(-r * cv * su, r * cv * cu, 0.0)),
331 dv: direct(&sphere.frame, Vec3::new(-r * sv * cu, -r * sv * su, r * cv)),
332 duu: direct(&sphere.frame, Vec3::new(-r * cv * cu, -r * cv * su, 0.0)),
333 duv: direct(&sphere.frame, Vec3::new(r * sv * su, -r * sv * cu, 0.0)),
334 dvv: direct(
335 &sphere.frame,
336 Vec3::new(-r * cv * cu, -r * cv * su, -r * sv),
337 ),
338 }
339 }
340 Surface::Torus(torus) => {
341 positive(torus.major_radius, "torus major radius")?;
342 positive(torus.minor_radius, "torus minor radius")?;
343 let r = torus.minor_radius;
344 let (su, cu) = u.sin_cos();
345 let (sv, cv) = v.sin_cos();
346 let ring = torus.major_radius + r * cv;
347 SurfaceJet {
348 point: torus_point(torus, u, v)?,
349 du: direct(&torus.frame, Vec3::new(-ring * su, ring * cu, 0.0)),
350 dv: direct(&torus.frame, Vec3::new(-r * sv * cu, -r * sv * su, r * cv)),
351 duu: direct(&torus.frame, Vec3::new(-ring * cu, -ring * su, 0.0)),
352 duv: direct(&torus.frame, Vec3::new(r * sv * su, -r * sv * cu, 0.0)),
353 dvv: direct(&torus.frame, Vec3::new(-r * cv * cu, -r * cv * su, -r * sv)),
354 }
355 }
356 Surface::BSpline(b) => bspline_jet(b, u, v)?,
357 _ => {
358 return Err(GeomError::Unsupported {
359 backend: ScalarSurface::ID,
360 operation: axiolid_contracts::Operation::SurfaceEvaluation,
361 })
362 }
363 };
364 if [
365 value.point,
366 value.du,
367 value.dv,
368 value.duu,
369 value.duv,
370 value.dvv,
371 ]
372 .iter()
373 .all(|vector| vector.is_finite())
374 {
375 Ok(value)
376 } else {
377 Err(GeomError::Degenerate(
378 "surface differential jet is non-finite".to_owned(),
379 ))
380 }
381}
382
383pub fn normal(surface: &Surface, u: Scalar, v: Scalar) -> GeomResult<Vec3> {
389 finite(u, "surface parameter u")?;
390 finite(v, "surface parameter v")?;
391 finite_surface_frame(surface)?;
392 let n = match surface {
393 Surface::Plane(p) => p.frame.z,
394 Surface::Cylinder(c) => {
395 positive(c.radius, "cylinder radius")?;
396 let (s, co) = u.sin_cos();
397 direct(&c.frame, Vec3::new(co, s, 0.0))
398 }
399 Surface::Cone(c) => cone_normal(c, u)?,
400 Surface::Sphere(s) => {
401 positive(s.radius, "sphere radius")?;
402 let (su, cu) = u.sin_cos();
403 let (sv, cv) = v.sin_cos();
404 direct(&s.frame, Vec3::new(cv * cu, cv * su, sv))
405 }
406 Surface::Torus(t) => {
407 positive(t.minor_radius, "torus minor radius")?;
408 let (su, cu) = u.sin_cos();
409 let (sv, cv) = v.sin_cos();
410 direct(&t.frame, Vec3::new(cv * cu, cv * su, sv))
411 }
412 Surface::BSpline(b) => bspline_normal(b, u, v)?,
413 _ => {
414 return Err(GeomError::Unsupported {
415 backend: ScalarSurface::ID,
416 operation: axiolid_contracts::Operation::SurfaceEvaluation,
417 })
418 }
419 };
420 let length = n.length();
421 if !(length > 0.0 && n.is_finite()) {
422 return Err(GeomError::Degenerate(format!(
423 "surface normal is not orientable at ({u}, {v})"
424 )));
425 }
426 Ok(n / length)
427}
428
429fn plane_point(p: &Plane, u: Scalar, v: Scalar) -> Point3 {
430 place(&p.frame, Vec3::new(u, v, 0.0))
431}
432
433fn cylinder_point(c: &Cylinder, u: Scalar, v: Scalar) -> GeomResult<Point3> {
434 positive(c.radius, "cylinder radius")?;
435 let (s, co) = u.sin_cos();
436 Ok(place(&c.frame, Vec3::new(c.radius * co, c.radius * s, v)))
437}
438
439fn cone_point(c: &Cone, u: Scalar, v: Scalar) -> GeomResult<Point3> {
440 finite(c.radius, "cone radius")?;
441 finite(c.semi_angle, "cone semi-angle")?;
442 let r = c.radius + v * c.semi_angle.tan();
445 if r < 0.0 {
446 return Err(GeomError::Degenerate(format!(
447 "cone radius is negative at v = {v}: the patch crosses the apex"
448 )));
449 }
450 let (s, co) = u.sin_cos();
451 Ok(place(&c.frame, Vec3::new(r * co, r * s, v)))
452}
453
454fn cone_normal(c: &Cone, u: Scalar) -> GeomResult<Vec3> {
455 finite(c.semi_angle, "cone semi-angle")?;
456 let (s, co) = u.sin_cos();
457 let (sa, ca) = c.semi_angle.sin_cos();
460 Ok(direct(&c.frame, Vec3::new(ca * co, ca * s, -sa)))
461}
462
463fn sphere_point(s: &Sphere, u: Scalar, v: Scalar) -> GeomResult<Point3> {
464 positive(s.radius, "sphere radius")?;
465 let (su, cu) = u.sin_cos();
466 let (sv, cv) = v.sin_cos();
467 Ok(place(
468 &s.frame,
469 Vec3::new(s.radius * cv * cu, s.radius * cv * su, s.radius * sv),
470 ))
471}
472
473fn torus_point(t: &Torus, u: Scalar, v: Scalar) -> GeomResult<Point3> {
474 positive(t.major_radius, "torus major radius")?;
475 positive(t.minor_radius, "torus minor radius")?;
476 let (su, cu) = u.sin_cos();
477 let (sv, cv) = v.sin_cos();
478 let ring = t.major_radius + t.minor_radius * cv;
479 Ok(place(
480 &t.frame,
481 Vec3::new(ring * cu, ring * su, t.minor_radius * sv),
482 ))
483}
484
485type Axis = SplineAxis;
488
489fn bspline_axes(b: &BSplineSurface) -> GeomResult<(Axis, Axis)> {
491 let rows = b.control_points.len();
492 if rows == 0 {
493 return Err(GeomError::InvalidInput(
494 "B-spline surface has no control points".to_owned(),
495 ));
496 }
497 let cols = b.control_points[0].len();
498 if cols == 0 {
499 return Err(GeomError::InvalidInput(
500 "B-spline surface control net has an empty row".to_owned(),
501 ));
502 }
503 if b.control_points.iter().any(|row| row.len() != cols) {
506 return Err(GeomError::InvalidInput(
507 "B-spline surface control net is ragged".to_owned(),
508 ));
509 }
510 if b.control_points
511 .iter()
512 .flatten()
513 .any(|point| !point.is_finite())
514 {
515 return Err(GeomError::InvalidInput(
516 "B-spline surface control points must be finite".to_owned(),
517 ));
518 }
519 if let Some(w) = &b.weights {
520 if w.len() != rows || w.iter().any(|row| row.len() != cols) {
521 return Err(GeomError::InvalidInput(
522 "B-spline surface weight net does not match the control net".to_owned(),
523 ));
524 }
525 if w.iter()
526 .flatten()
527 .any(|weight| !weight.is_finite() || *weight <= 0.0)
528 {
529 return Err(GeomError::InvalidInput(
530 "B-spline surface weights must be finite and strictly positive".to_owned(),
531 ));
532 }
533 }
534 let u = Axis::new(&b.u_knots, &b.u_multiplicities, b.u_degree, rows, "u")?;
535 let v = Axis::new(&b.v_knots, &b.v_multiplicities, b.v_degree, cols, "v")?;
536 Ok((u, v))
537}
538
539fn bspline_point(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<Point3> {
545 let (ua, va) = bspline_axes(b)?;
546 let (uc, vc) = (ua.clamp(u), va.clamp(v));
547 let uspan = span_in(&ua.knots, ua.count, ua.degree, uc);
548 let vspan = span_in(&va.knots, va.count, va.degree, vc);
549
550 let mut row_points: Vec<[Scalar; 3]> = Vec::with_capacity(ua.degree + 1);
552 let mut row_weights: Vec<Scalar> = Vec::with_capacity(ua.degree + 1);
553 for i in 0..=ua.degree {
554 let row = uspan - ua.degree + i;
555 let mut pts: Vec<[Scalar; 3]> = Vec::with_capacity(va.degree + 1);
556 let mut wts: Vec<Scalar> = Vec::with_capacity(va.degree + 1);
557 for j in 0..=va.degree {
558 let col = vspan - va.degree + j;
559 let w = b.weights.as_ref().map_or(1.0, |ws| ws[row][col]);
560 let p = b.control_points[row][col];
561 let homogeneous = [p.x * w, p.y * w, p.z * w];
562 if homogeneous.iter().any(|value| !value.is_finite()) {
563 return Err(GeomError::Degenerate(
564 "B-spline surface homogeneous control point overflowed".to_owned(),
565 ));
566 }
567 pts.push(homogeneous);
568 wts.push(w);
569 }
570 de_boor_recurrence(&va.knots, vspan, va.degree, vc, &mut pts, &mut wts);
571 row_points.push(pts[va.degree]);
572 row_weights.push(wts[va.degree]);
573 }
574
575 de_boor_recurrence(
577 &ua.knots,
578 uspan,
579 ua.degree,
580 uc,
581 &mut row_points,
582 &mut row_weights,
583 );
584
585 let w = row_weights[ua.degree];
586 if !w.is_finite() || w == 0.0 {
587 return Err(GeomError::Degenerate(
588 "B-spline surface weight collapsed to zero".to_owned(),
589 ));
590 }
591 let p = row_points[ua.degree];
592 Ok(Point3::new(p[0] / w, p[1] / w, p[2] / w))
593}
594
595#[derive(Clone, Copy)]
597struct HomogeneousAxes<'a> {
598 u_knots: &'a [Scalar],
599 u_degree: usize,
600 v_knots: &'a [Scalar],
601 v_degree: usize,
602}
603
604fn eval_tensor_homogeneous(
606 axes: HomogeneousAxes<'_>,
607 points: &[Vec<[Scalar; 3]>],
608 weights: &[Vec<Scalar>],
609 u: Scalar,
610 v: Scalar,
611) -> ([Scalar; 3], Scalar) {
612 let mut row_points = Vec::with_capacity(points.len());
613 let mut row_weights = Vec::with_capacity(points.len());
614 for (row_points_h, row_weights_h) in points.iter().zip(weights) {
615 let (point, weight) =
616 eval_homogeneous(axes.v_knots, axes.v_degree, row_points_h, row_weights_h, v);
617 row_points.push(point);
618 row_weights.push(weight);
619 }
620 eval_homogeneous(axes.u_knots, axes.u_degree, &row_points, &row_weights, u)
621}
622
623type HomogeneousPointNet = Vec<Vec<[Scalar; 3]>>;
624type HomogeneousWeightNet = Vec<Vec<Scalar>>;
625
626fn homogeneous_control_net(
628 b: &BSplineSurface,
629) -> GeomResult<(HomogeneousPointNet, HomogeneousWeightNet)> {
630 let mut points = Vec::with_capacity(b.control_points.len());
631 let mut weights = Vec::with_capacity(b.control_points.len());
632 for (i, row) in b.control_points.iter().enumerate() {
633 let mut point_row = Vec::with_capacity(row.len());
634 let mut weight_row = Vec::with_capacity(row.len());
635 for (j, point) in row.iter().enumerate() {
636 let weight = b.weights.as_ref().map_or(1.0, |net| net[i][j]);
637 let homogeneous = [point.x * weight, point.y * weight, point.z * weight];
638 if homogeneous.iter().any(|value| !value.is_finite()) {
639 return Err(GeomError::Degenerate(
640 "B-spline surface homogeneous control point overflowed".to_owned(),
641 ));
642 }
643 point_row.push(homogeneous);
644 weight_row.push(weight);
645 }
646 points.push(point_row);
647 weights.push(weight_row);
648 }
649 Ok((points, weights))
650}
651
652fn derivative_net_u(
654 points: &[Vec<[Scalar; 3]>],
655 weights: &[Vec<Scalar>],
656 knots: &[Scalar],
657 degree: usize,
658) -> (Vec<Vec<[Scalar; 3]>>, Vec<Vec<Scalar>>) {
659 let rows = points.len() - 1;
660 let cols = points[0].len();
661 let mut derivative_points = Vec::with_capacity(rows);
662 let mut derivative_weights = Vec::with_capacity(rows);
663 for i in 0..rows {
664 let denominator = knots[i + degree + 1] - knots[i + 1];
665 let factor = if denominator.abs() > 0.0 {
666 degree as Scalar / denominator
667 } else {
668 0.0
669 };
670 let mut point_row = Vec::with_capacity(cols);
671 let mut weight_row = Vec::with_capacity(cols);
672 for j in 0..cols {
673 point_row.push(core::array::from_fn(|k| {
674 factor * (points[i + 1][j][k] - points[i][j][k])
675 }));
676 weight_row.push(factor * (weights[i + 1][j] - weights[i][j]));
677 }
678 derivative_points.push(point_row);
679 derivative_weights.push(weight_row);
680 }
681 (derivative_points, derivative_weights)
682}
683
684fn derivative_net_v(
686 points: &[Vec<[Scalar; 3]>],
687 weights: &[Vec<Scalar>],
688 knots: &[Scalar],
689 degree: usize,
690) -> (Vec<Vec<[Scalar; 3]>>, Vec<Vec<Scalar>>) {
691 let rows = points.len();
692 let cols = points[0].len() - 1;
693 let mut derivative_points = Vec::with_capacity(rows);
694 let mut derivative_weights = Vec::with_capacity(rows);
695 for i in 0..rows {
696 let mut point_row = Vec::with_capacity(cols);
697 let mut weight_row = Vec::with_capacity(cols);
698 for j in 0..cols {
699 let denominator = knots[j + degree + 1] - knots[j + 1];
700 let factor = if denominator.abs() > 0.0 {
701 degree as Scalar / denominator
702 } else {
703 0.0
704 };
705 point_row.push(core::array::from_fn(|k| {
706 factor * (points[i][j + 1][k] - points[i][j][k])
707 }));
708 weight_row.push(factor * (weights[i][j + 1] - weights[i][j]));
709 }
710 derivative_points.push(point_row);
711 derivative_weights.push(weight_row);
712 }
713 (derivative_points, derivative_weights)
714}
715
716fn project_derivative(
718 point: [Scalar; 3],
719 weight: Scalar,
720 derivative: [Scalar; 3],
721 derivative_weight: Scalar,
722 axis: &str,
723) -> GeomResult<Vec3> {
724 if !weight.is_finite() || weight == 0.0 {
725 return Err(GeomError::Degenerate(
726 "B-spline surface weight collapsed to a non-finite or zero value".to_owned(),
727 ));
728 }
729 let value = Vec3::new(
730 (derivative[0] - point[0] * derivative_weight / weight) / weight,
731 (derivative[1] - point[1] * derivative_weight / weight) / weight,
732 (derivative[2] - point[2] * derivative_weight / weight) / weight,
733 );
734 if !value.is_finite() {
735 return Err(GeomError::Degenerate(format!(
736 "B-spline surface {axis} derivative is non-finite"
737 )));
738 }
739 Ok(value)
740}
741
742fn bspline_partials(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<(Vec3, Vec3)> {
744 let (ua, va) = bspline_axes(b)?;
745 let (uc, vc) = (ua.clamp(u), va.clamp(v));
746 let (points, weights) = homogeneous_control_net(b)?;
747 let (point, weight) = eval_tensor_homogeneous(
748 HomogeneousAxes {
749 u_knots: &ua.knots,
750 u_degree: ua.degree,
751 v_knots: &va.knots,
752 v_degree: va.degree,
753 },
754 &points,
755 &weights,
756 uc,
757 vc,
758 );
759
760 let (u_points, u_weights) = derivative_net_u(&points, &weights, &ua.knots, ua.degree);
761 let (du, du_weight) = eval_tensor_homogeneous(
762 HomogeneousAxes {
763 u_knots: &ua.knots[1..ua.knots.len() - 1],
764 u_degree: ua.degree - 1,
765 v_knots: &va.knots,
766 v_degree: va.degree,
767 },
768 &u_points,
769 &u_weights,
770 uc,
771 vc,
772 );
773
774 let (v_points, v_weights) = derivative_net_v(&points, &weights, &va.knots, va.degree);
775 let (dv, dv_weight) = eval_tensor_homogeneous(
776 HomogeneousAxes {
777 u_knots: &ua.knots,
778 u_degree: ua.degree,
779 v_knots: &va.knots[1..va.knots.len() - 1],
780 v_degree: va.degree - 1,
781 },
782 &v_points,
783 &v_weights,
784 uc,
785 vc,
786 );
787
788 Ok((
789 project_derivative(point, weight, du, du_weight, "u")?,
790 project_derivative(point, weight, dv, dv_weight, "v")?,
791 ))
792}
793
794pub fn bspline_jet(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<SurfaceJet> {
797 let (ua, va) = bspline_axes(b)?;
798 let (uc, vc) = (ua.clamp(u), va.clamp(v));
799 let (points, weights) = homogeneous_control_net(b)?;
800 let base_axes = HomogeneousAxes {
801 u_knots: &ua.knots,
802 u_degree: ua.degree,
803 v_knots: &va.knots,
804 v_degree: va.degree,
805 };
806 let (point, weight) = eval_tensor_homogeneous(base_axes, &points, &weights, uc, vc);
807 if !weight.is_finite() || weight == 0.0 {
808 return Err(GeomError::Degenerate(
809 "B-spline surface weight collapsed to a non-finite or zero value".to_owned(),
810 ));
811 }
812 let position = Point3::new(point[0] / weight, point[1] / weight, point[2] / weight);
813
814 let (u_points, u_weights) = derivative_net_u(&points, &weights, &ua.knots, ua.degree);
815 let u_knots = &ua.knots[1..ua.knots.len() - 1];
816 let (du_h, du_weight) = eval_tensor_homogeneous(
817 HomogeneousAxes {
818 u_knots,
819 u_degree: ua.degree - 1,
820 v_knots: &va.knots,
821 v_degree: va.degree,
822 },
823 &u_points,
824 &u_weights,
825 uc,
826 vc,
827 );
828 let du = project_derivative(point, weight, du_h, du_weight, "u")?;
829
830 let (v_points, v_weights) = derivative_net_v(&points, &weights, &va.knots, va.degree);
831 let v_knots = &va.knots[1..va.knots.len() - 1];
832 let (dv_h, dv_weight) = eval_tensor_homogeneous(
833 HomogeneousAxes {
834 u_knots: &ua.knots,
835 u_degree: ua.degree,
836 v_knots,
837 v_degree: va.degree - 1,
838 },
839 &v_points,
840 &v_weights,
841 uc,
842 vc,
843 );
844 let dv = project_derivative(point, weight, dv_h, dv_weight, "v")?;
845
846 let (duu_h, duu_weight) = if ua.degree >= 2 {
847 let (net, net_weights) = derivative_net_u(&u_points, &u_weights, u_knots, ua.degree - 1);
848 eval_tensor_homogeneous(
849 HomogeneousAxes {
850 u_knots: &u_knots[1..u_knots.len() - 1],
851 u_degree: ua.degree - 2,
852 v_knots: &va.knots,
853 v_degree: va.degree,
854 },
855 &net,
856 &net_weights,
857 uc,
858 vc,
859 )
860 } else {
861 ([0.0; 3], 0.0)
862 };
863 let duu = project_second(point, weight, du, duu_h, du_weight, duu_weight, "uu")?;
864
865 let (dvv_h, dvv_weight) = if va.degree >= 2 {
866 let (net, net_weights) = derivative_net_v(&v_points, &v_weights, v_knots, va.degree - 1);
867 eval_tensor_homogeneous(
868 HomogeneousAxes {
869 u_knots: &ua.knots,
870 u_degree: ua.degree,
871 v_knots: &v_knots[1..v_knots.len() - 1],
872 v_degree: va.degree - 2,
873 },
874 &net,
875 &net_weights,
876 uc,
877 vc,
878 )
879 } else {
880 ([0.0; 3], 0.0)
881 };
882 let dvv = project_second(point, weight, dv, dvv_h, dv_weight, dvv_weight, "vv")?;
883
884 let (uv_points, uv_weights) = derivative_net_v(&u_points, &u_weights, &va.knots, va.degree);
885 let (duv_h, duv_weight) = eval_tensor_homogeneous(
886 HomogeneousAxes {
887 u_knots,
888 u_degree: ua.degree - 1,
889 v_knots,
890 v_degree: va.degree - 1,
891 },
892 &uv_points,
893 &uv_weights,
894 uc,
895 vc,
896 );
897 let duv = project_mixed(
898 point, weight, du, du_weight, dv, dv_weight, duv_h, duv_weight,
899 )?;
900
901 Ok(SurfaceJet {
902 point: position,
903 du,
904 dv,
905 duu,
906 duv,
907 dvv,
908 })
909}
910
911fn project_second(
912 point: [Scalar; 3],
913 weight: Scalar,
914 first: Vec3,
915 second: [Scalar; 3],
916 first_weight: Scalar,
917 second_weight: Scalar,
918 axis: &str,
919) -> GeomResult<Vec3> {
920 let position = Vec3::new(point[0], point[1], point[2]) / weight;
921 let value = (Vec3::new(second[0], second[1], second[2])
922 - 2.0 * first_weight * first
923 - second_weight * position)
924 / weight;
925 if value.is_finite() {
926 Ok(value)
927 } else {
928 Err(GeomError::Degenerate(format!(
929 "B-spline surface {axis} second derivative is non-finite"
930 )))
931 }
932}
933
934#[allow(clippy::too_many_arguments)]
935fn project_mixed(
936 point: [Scalar; 3],
937 weight: Scalar,
938 du: Vec3,
939 du_weight: Scalar,
940 dv: Vec3,
941 dv_weight: Scalar,
942 mixed: [Scalar; 3],
943 mixed_weight: Scalar,
944) -> GeomResult<Vec3> {
945 let position = Vec3::new(point[0], point[1], point[2]) / weight;
946 let value = (Vec3::new(mixed[0], mixed[1], mixed[2])
947 - du_weight * dv
948 - dv_weight * du
949 - mixed_weight * position)
950 / weight;
951 if value.is_finite() {
952 Ok(value)
953 } else {
954 Err(GeomError::Degenerate(
955 "B-spline surface uv mixed derivative is non-finite".to_owned(),
956 ))
957 }
958}
959
960fn bspline_normal(b: &BSplineSurface, u: Scalar, v: Scalar) -> GeomResult<Vec3> {
962 let (du, dv) = bspline_partials(b, u, v)?;
963 Ok(du.cross(dv))
964}
965
966#[derive(Debug, Default, Clone, Copy)]
969pub struct ScalarSurface;
970
971impl ScalarSurface {
972 pub const ID: BackendId = BackendId::new("scalar-reference");
974}
975
976impl axiolid_surface::SurfaceEvaluator<Surface> for ScalarSurface {
977 type Error = GeomError;
978
979 fn evaluate(
980 &self,
981 surface: &Surface,
982 u: Scalar,
983 v: Scalar,
984 _tolerance: axiolid_core::Tolerance,
985 ) -> Result<Point3, Self::Error> {
986 evaluate(surface, u, v)
987 }
988
989 fn normal(
990 &self,
991 surface: &Surface,
992 u: Scalar,
993 v: Scalar,
994 _tolerance: axiolid_core::Tolerance,
995 ) -> Result<Vec3, Self::Error> {
996 normal(surface, u, v)
997 }
998}
999
1000pub fn invert(
1017 surface: &Surface,
1018 point: Point3,
1019 tolerance: axiolid_core::Tolerance,
1020) -> GeomResult<(Scalar, Scalar)> {
1021 let (u, v) = match surface {
1022 Surface::Plane(p) => {
1023 let local = to_local(&p.frame, point)?;
1024 (local.x, local.y)
1025 }
1026 Surface::Cylinder(c) => {
1027 positive(c.radius, "cylinder radius")?;
1028 let local = to_local(&c.frame, point)?;
1029 (angle_about_axis(local, "cylinder")?, local.z)
1030 }
1031 Surface::Cone(c) => {
1032 finite(c.radius, "cone radius")?;
1033 finite(c.semi_angle, "cone semi-angle")?;
1034 let local = to_local(&c.frame, point)?;
1035 (angle_about_axis(local, "cone")?, local.z)
1039 }
1040 Surface::Sphere(s) => {
1041 positive(s.radius, "sphere radius")?;
1042 let local = to_local(&s.frame, point)?;
1043 let sin_v = (local.z / s.radius).clamp(-1.0, 1.0);
1046 (angle_about_axis(local, "sphere")?, sin_v.asin())
1047 }
1048 Surface::Torus(t) => {
1049 positive(t.major_radius, "torus major radius")?;
1050 positive(t.minor_radius, "torus minor radius")?;
1051 let local = to_local(&t.frame, point)?;
1052 let ring = (local.x * local.x + local.y * local.y).sqrt();
1053 (
1054 angle_about_axis(local, "torus")?,
1055 (local.z).atan2(ring - t.major_radius),
1056 )
1057 }
1058 _ => {
1064 return Err(GeomError::Unsupported {
1065 backend: ScalarSurface::ID,
1066 operation: axiolid_contracts::Operation::SurfaceEvaluation,
1067 });
1068 }
1069 };
1070 let round_trip = evaluate(surface, u, v)?;
1073 let residual = (round_trip - point).length();
1074 if residual > tolerance.linear() {
1075 return Err(GeomError::Degenerate(format!(
1076 "point is {residual} from the surface, beyond the {} tolerance: \
1077 inversion names a point ON the surface and does not project",
1078 tolerance.linear()
1079 )));
1080 }
1081 Ok((u, v))
1082}
1083
1084fn to_local(frame: &Frame3, point: Point3) -> GeomResult<Vec3> {
1094 let axes = [frame.x, frame.y, frame.z];
1095 for (axis, name) in axes.iter().zip(["x", "y", "z"]) {
1096 let length = axis.length();
1097 if (length - 1.0).abs() > 1e-9 {
1098 return Err(GeomError::Degenerate(format!(
1099 "surface frame {name} axis has length {length}, expected 1"
1100 )));
1101 }
1102 }
1103 for (a, b, pair) in [
1104 (frame.x, frame.y, "x/y"),
1105 (frame.y, frame.z, "y/z"),
1106 (frame.z, frame.x, "z/x"),
1107 ] {
1108 let dot = a.dot(b);
1109 if dot.abs() > 1e-9 {
1110 return Err(GeomError::Degenerate(format!(
1111 "surface frame {pair} axes are not perpendicular: dot {dot}"
1112 )));
1113 }
1114 }
1115 let offset = point - frame.origin;
1116 Ok(Vec3::new(
1117 offset.dot(frame.x),
1118 offset.dot(frame.y),
1119 offset.dot(frame.z),
1120 ))
1121}
1122
1123fn angle_about_axis(local: Vec3, surface: &str) -> GeomResult<Scalar> {
1130 let radial = (local.x * local.x + local.y * local.y).sqrt();
1131 if radial <= 1e-12 {
1132 return Err(GeomError::Degenerate(format!(
1133 "{surface} point lies on the axis, where every u names it: \
1134 the angular parameter is not recoverable"
1135 )));
1136 }
1137 Ok(local.y.atan2(local.x))
1138}