Skip to main content

ogeom_geom/
traits.rs

1//! The adaptor traits: what every algorithm sees geometry through.
2//!
3//! Intersection, projection, extrema and tessellation are written against
4//! [`Curve3d`], [`Curve2d`] and [`Surface`], never against a concrete type.
5//! Nothing downstream needs to know whether it holds an analytic cylinder or a
6//! NURBS patch, which is what keeps one implementation of each algorithm rather
7//! than one per surface type.
8//!
9//! # Fast paths, opted into
10//!
11//! [`Curve3d::kind`] and [`Surface::kind`] let an algorithm *ask*. Plane/plane
12//! intersection is two lines of algebra and should never go through a marching
13//! intersector; a caller that wants that shortcut matches on the kind and takes
14//! it. The general path never has to know what it is looking at, so adding a
15//! surface type does not break existing algorithms; it only forgoes a
16//! shortcut until someone writes one.
17
18use ogeom_core::{OgeomResult, Tolerances};
19use ogeom_math::{Direction, Point, Point2, Transform, Vector, Vector2};
20
21/// How smooth a curve or surface is.
22///
23/// Ordered from least to most smooth, so `>=` is a meaningful test.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
25pub enum Continuity {
26    /// Positions agree; tangents may not. A corner.
27    C0,
28    /// Tangent *directions* agree, but their magnitudes need not. Enough for a
29    /// visually smooth join, and the usual requirement for a wire.
30    G1,
31    /// First derivatives agree exactly.
32    C1,
33    /// Curvature agrees.
34    G2,
35    /// Second derivatives agree exactly.
36    C2,
37    /// Differentiable to any order: an analytic surface away from its
38    /// degeneracies.
39    CInfinity,
40}
41
42/// What kind of curve this is, for analytic fast paths.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum CurveKind {
46    /// A straight line.
47    Line,
48    /// A circle or circular arc.
49    Circle,
50    /// An ellipse or elliptical arc.
51    Ellipse,
52    /// One branch of a hyperbola.
53    Hyperbola,
54    /// A parabola.
55    Parabola,
56    /// A polynomial or rational Bézier.
57    Bezier,
58    /// A polynomial or rational B-spline.
59    BSpline,
60    /// A helix about an axis.
61    Helix,
62    /// A restriction of another curve to a sub-interval.
63    Trimmed,
64    /// A curve offset from another.
65    Offset,
66    /// A pcurve composed with the surface it is drawn on.
67    OnSurface,
68    /// An affine-plus-trigonometric chart curve.
69    Trig,
70}
71
72/// What kind of surface this is, for analytic fast paths.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74#[non_exhaustive]
75pub enum SurfaceKind {
76    /// A plane.
77    Plane,
78    /// A circular cylinder.
79    Cylinder,
80    /// A circular cone.
81    Cone,
82    /// A sphere.
83    Sphere,
84    /// A torus.
85    Torus,
86    /// A polynomial or rational Bézier patch.
87    Bezier,
88    /// A polynomial or rational B-spline patch.
89    BSpline,
90    /// A surface of revolution.
91    Revolution,
92    /// A surface swept by translating a curve.
93    Extrusion,
94    /// A restriction of another surface to a sub-rectangle.
95    Trimmed,
96    /// A surface offset from another.
97    Offset,
98}
99
100impl SurfaceKind {
101    /// Whether this surface is a quadric: a plane, cylinder, cone or sphere.
102    ///
103    /// Quadric pairs have closed-form intersections, which is worth a great
104    /// deal: it is the difference between an exact conic and a marched
105    /// approximation of one.
106    #[must_use]
107    pub const fn is_quadric(self) -> bool {
108        matches!(
109            self,
110            Self::Plane | Self::Cylinder | Self::Cone | Self::Sphere
111        )
112    }
113
114    /// Whether this surface has an exact analytic form, as opposed to being
115    /// defined by control points.
116    #[must_use]
117    pub const fn is_analytic(self) -> bool {
118        matches!(
119            self,
120            Self::Plane | Self::Cylinder | Self::Cone | Self::Sphere | Self::Torus
121        )
122    }
123}
124
125/// A parametric curve in space.
126///
127/// Implementors must guarantee:
128///
129/// - [`Curve3d::domain`] returns `(a, b)` with `a < b`, both finite;
130/// - [`Curve3d::point_at`] and the derivative methods agree: `d1_at` is the
131///   derivative of `point_at`, and so on;
132/// - a periodic curve's period is exactly its domain width.
133pub trait Curve3d {
134    /// The parameter interval over which the curve is defined.
135    fn domain(&self) -> (f64, f64);
136
137    /// The point at `u`.
138    ///
139    /// # Errors
140    ///
141    /// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) if `u` is outside the
142    /// domain and the curve is not periodic.
143    fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point>;
144
145    /// The first derivative at `u`.
146    ///
147    /// # Errors
148    ///
149    /// As [`Curve3d::point_at`].
150    fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector>;
151
152    /// Derivatives up to order `n`, with `result[0]` the point itself.
153    ///
154    /// # Errors
155    ///
156    /// As [`Curve3d::point_at`].
157    fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>>;
158
159    /// What kind of curve this is.
160    fn kind(&self) -> CurveKind;
161
162    /// How smooth the curve is across its domain.
163    fn continuity(&self) -> Continuity;
164
165    /// Whether the curve's ends meet.
166    fn is_closed(&self, tol: Tolerances) -> bool;
167
168    /// Whether the curve continues past its domain by repeating.
169    ///
170    /// Distinct from being closed: a full circle is both, a closed B-spline
171    /// whose ends merely coincide is closed but not periodic, and evaluating
172    /// the latter outside its domain is an error.
173    fn is_periodic(&self) -> bool;
174
175    /// The unit tangent at `u`.
176    ///
177    /// # Errors
178    ///
179    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) at a cusp,
180    /// where the derivative vanishes.
181    fn tangent_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Direction> {
182        Direction::new(self.d1_at(u, tol)?, tol)
183    }
184
185    /// The curvature at `u`.
186    ///
187    /// # Errors
188    ///
189    /// As [`Curve3d::point_at`].
190    fn curvature_at(&self, u: f64, tol: Tolerances) -> OgeomResult<f64> {
191        let d = self.derivatives_at(u, 2, tol)?;
192        let speed = d[1].magnitude();
193        if speed == 0.0 {
194            return Ok(0.0);
195        }
196        Ok(d[1].cross(d[2]).magnitude() / (speed * speed * speed))
197    }
198
199    /// The start point.
200    ///
201    /// # Errors
202    ///
203    /// As [`Curve3d::point_at`].
204    fn start(&self, tol: Tolerances) -> OgeomResult<Point> {
205        self.point_at(self.domain().0, tol)
206    }
207
208    /// The end point.
209    ///
210    /// # Errors
211    ///
212    /// As [`Curve3d::point_at`].
213    fn end(&self, tol: Tolerances) -> OgeomResult<Point> {
214        self.point_at(self.domain().1, tol)
215    }
216
217    /// Bring `u` into the domain, wrapping if the curve is periodic.
218    ///
219    /// # Errors
220    ///
221    /// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) if `u` is outside the
222    /// domain of a non-periodic curve by more than `tol.parametric()`.
223    fn normalize_parameter(&self, u: f64, tol: Tolerances) -> OgeomResult<f64> {
224        let (a, b) = self.domain();
225        if self.is_periodic() {
226            let period = b - a;
227            return Ok(a + (u - a).rem_euclid(period));
228        }
229        if !u.is_finite() || u < a - tol.parametric() || u > b + tol.parametric() {
230            if std::env::var_os("OGEOM_DEBUG_DOMAIN").is_some() {
231                eprintln!(
232                    "DOMAIN {u} outside [{a}, {b}]:\n{}",
233                    std::backtrace::Backtrace::force_capture()
234                );
235            }
236            return Err(ogeom_core::ogeom_err!(
237                Domain,
238                "parameter {u} outside curve domain [{a}, {b}]"
239            ));
240        }
241        Ok(u.clamp(a, b))
242    }
243}
244
245/// A parametric curve in the plane.
246///
247/// The same contract as [`Curve3d`], one dimension down. Kept as a separate
248/// trait rather than a generic parameter because pcurves (curves in a
249/// surface's parameter space) are used differently enough from spatial curves
250/// that conflating them invites mistakes.
251pub trait Curve2d {
252    /// The parameter interval over which the curve is defined.
253    fn domain(&self) -> (f64, f64);
254
255    /// The point at `u`.
256    ///
257    /// # Errors
258    ///
259    /// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) if `u` is outside the
260    /// domain and the curve is not periodic.
261    fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point2>;
262
263    /// The first derivative at `u`.
264    ///
265    /// # Errors
266    ///
267    /// As [`Curve2d::point_at`].
268    fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector2>;
269
270    /// Derivatives up to order `n`, with `result[0]` the point itself.
271    ///
272    /// # Errors
273    ///
274    /// As [`Curve2d::point_at`].
275    fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector2>>;
276
277    /// What kind of curve this is.
278    fn kind(&self) -> CurveKind;
279
280    /// Whether the curve's ends meet.
281    fn is_closed(&self, tol: Tolerances) -> bool;
282
283    /// Whether the curve continues past its domain by repeating.
284    fn is_periodic(&self) -> bool;
285
286    /// The start point.
287    ///
288    /// # Errors
289    ///
290    /// As [`Curve2d::point_at`].
291    fn start(&self, tol: Tolerances) -> OgeomResult<Point2> {
292        self.point_at(self.domain().0, tol)
293    }
294
295    /// The end point.
296    ///
297    /// # Errors
298    ///
299    /// As [`Curve2d::point_at`].
300    fn end(&self, tol: Tolerances) -> OgeomResult<Point2> {
301        self.point_at(self.domain().1, tol)
302    }
303}
304
305/// A surface's point and its derivatives through second order, at one place.
306///
307/// What a foot-point solve needs in one go; see [`Surface::jet_at`].
308#[derive(Debug, Clone, Copy, PartialEq)]
309pub struct SurfaceJet {
310    /// The point at the parameters asked for.
311    pub point: Point,
312    /// The first derivative along `u`.
313    pub du: Vector,
314    /// The first derivative along `v`.
315    pub dv: Vector,
316    /// The second derivative along `u`.
317    pub d2u: Vector,
318    /// The mixed second derivative.
319    pub duv: Vector,
320    /// The second derivative along `v`.
321    pub d2v: Vector,
322}
323
324/// A surface's curvature at one place: the principal curvatures and the
325/// directions they are taken in, with the mean and Gaussian curvatures
326/// they combine to.
327///
328/// What curvature display, zebra analysis and a fillet's seat all ask.
329/// The principal directions are unit tangents in space, perpendicular to
330/// each other; at an umbilic (where every direction curves the same, as
331/// everywhere on a sphere or a plane) they are any perpendicular pair
332/// in the tangent plane, and [`SurfaceCurvature::is_umbilic`] says so.
333#[derive(Debug, Clone, Copy, PartialEq)]
334pub struct SurfaceCurvature {
335    /// The largest normal curvature.
336    pub max: f64,
337    /// The smallest normal curvature.
338    pub min: f64,
339    /// The tangent direction of the largest.
340    pub max_direction: Direction,
341    /// The tangent direction of the smallest.
342    pub min_direction: Direction,
343    /// The unit normal the curvatures are signed against: positive where
344    /// the surface curves towards it.
345    pub normal: Direction,
346}
347
348impl SurfaceCurvature {
349    /// The mean curvature, `(max + min) / 2`.
350    #[must_use]
351    pub fn mean(&self) -> f64 {
352        f64::midpoint(self.max, self.min)
353    }
354
355    /// The Gaussian curvature, `max · min`.
356    #[must_use]
357    pub fn gaussian(&self) -> f64 {
358        self.max * self.min
359    }
360
361    /// Whether the two principal curvatures are equal to within `tol`'s
362    /// angular resolution of their size, so no direction is principal
363    /// above another.
364    #[must_use]
365    pub fn is_umbilic(&self, tol: Tolerances) -> bool {
366        (self.max - self.min).abs() <= tol.angular() * self.max.abs().max(self.min.abs()).max(1.0)
367    }
368}
369
370/// A parametric surface.
371///
372/// Implementors must guarantee:
373///
374/// - both domain intervals are non-empty and finite;
375/// - the derivative methods agree with [`Surface::point_at`];
376/// - `du` and `dv` are the derivatives along the first and second parameters
377///   respectively, in that order, for every surface.
378pub trait Surface {
379    /// The `u` and `v` parameter intervals.
380    fn domain(&self) -> ((f64, f64), (f64, f64));
381
382    /// The point at `(u, v)`.
383    ///
384    /// # Errors
385    ///
386    /// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) if the parameters lie
387    /// outside the domain in a direction that is not periodic.
388    fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point>;
389
390    /// The two first derivatives at `(u, v)`.
391    ///
392    /// # Errors
393    ///
394    /// As [`Surface::point_at`].
395    fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)>;
396
397    /// The three second derivatives at `(u, v)`: `d2u`, `duv`, `d2v`.
398    ///
399    /// # Errors
400    ///
401    /// As [`Surface::point_at`].
402    fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)>;
403
404    /// The point and every derivative through second order, together.
405    ///
406    /// The default asks the three accessors above, which is right for a
407    /// surface carrying its answer in closed form: a plane or a cylinder costs
408    /// the same either way.
409    ///
410    /// It is not right for a tensor-product patch, where each accessor
411    /// re-locates the knot spans, rebuilds the basis functions and sums the
412    /// control grid again, three times, for numbers the order-two table
413    /// already holds. Anything walking a surface and asking for all six at a
414    /// point, as a foot-point solve does at every step, pays that over and
415    /// over. Such surfaces override this.
416    ///
417    /// **The contract is agreement to rounding, not to the bit.** A patch sums
418    /// its point by de Boor and its derivatives by basis functions, and those
419    /// reassociate differently; the combined answer may differ from the
420    /// separate accessors' in the last ulp. Callers needing one consistent
421    /// jet (every value from the same evaluation) should use this and not
422    /// mix it with the accessors at the same parameters.
423    ///
424    /// # Errors
425    ///
426    /// As [`Surface::point_at`].
427    fn jet_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<SurfaceJet> {
428        let point = self.point_at(u, v, tol)?;
429        let (du, dv) = self.d1_at(u, v, tol)?;
430        let (d2u, duv, d2v) = self.d2_at(u, v, tol)?;
431        Ok(SurfaceJet {
432            point,
433            du,
434            dv,
435            d2u,
436            duv,
437            d2v,
438        })
439    }
440
441    /// What kind of surface this is.
442    fn kind(&self) -> SurfaceKind;
443
444    /// How smooth the surface is across its domain.
445    fn continuity(&self) -> Continuity;
446
447    /// Whether the surface closes on itself along `u`.
448    fn is_closed_u(&self, tol: Tolerances) -> bool;
449
450    /// Whether the surface closes on itself along `v`.
451    fn is_closed_v(&self, tol: Tolerances) -> bool;
452
453    /// Whether `u` repeats past the domain.
454    fn is_periodic_u(&self) -> bool;
455
456    /// Whether `v` repeats past the domain.
457    fn is_periodic_v(&self) -> bool;
458
459    /// The unit normal at `(u, v)`, following `du x dv`.
460    ///
461    /// # Errors
462    ///
463    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) at a
464    /// degeneracy (a pole or an apex) where the tangents determine no normal.
465    fn normal_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Direction> {
466        let (du, dv) = self.d1_at(u, v, tol)?;
467        // Compared against the square of the larger tangent: at a pole one
468        // tangent vanishes, and a test relative to the *product* would find the
469        // cross product large by comparison and call the point healthy.
470        let scale = du.magnitude().max(dv.magnitude());
471        if du.cross(dv).magnitude() <= tol.angular() * scale * scale {
472            return Err(ogeom_core::ogeom_err!(
473                Construction,
474                "surface is degenerate at ({u}, {v}); no normal is determined"
475            ));
476        }
477        Direction::new(du.cross(dv), tol)
478    }
479
480    /// The principal curvatures and directions at `(u, v)`.
481    ///
482    /// From the two fundamental forms of the jet: the principal curvatures
483    /// are the eigenvalues of the shape operator `I⁻¹ II`, the directions
484    /// its eigenvectors carried into space. Signed against the surface's
485    /// own normal, [`Surface::normal_at`], so a sphere of radius `r` seen
486    /// from outside reads `-1/r` twice and a cylinder `-1/r` round and `0`
487    /// along.
488    ///
489    /// # Errors
490    ///
491    /// As [`Surface::normal_at`]: at a degenerate point there is no normal
492    /// to sign against.
493    fn curvature_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<SurfaceCurvature> {
494        let jet = self.jet_at(u, v, tol)?;
495        let normal = self.normal_at(u, v, tol)?;
496        let n = normal.vector();
497        let (e, f, g) = (jet.du.dot(jet.du), jet.du.dot(jet.dv), jet.dv.dot(jet.dv));
498        let (l, m, nn) = (jet.d2u.dot(n), jet.duv.dot(n), jet.d2v.dot(n));
499        let det = e * g - f * f;
500        let gaussian = (l * nn - m * m) / det;
501        let mean = (e * nn - 2.0 * f * m + g * l) / (2.0 * det);
502        let spread = (mean * mean - gaussian).max(0.0).sqrt();
503        let (max, min) = (mean + spread, mean - spread);
504        // A principal direction `a du + b dv` solves `(II - k I)(a, b) = 0`;
505        // either row of that matrix gives it, and the row with the larger
506        // entries is the one to trust. At an umbilic both rows vanish and
507        // any tangent will do: `du` and the normal turned against it.
508        let direction_for = |k: f64| -> OgeomResult<Vector> {
509            let row1 = (l - k * e, m - k * f);
510            let row2 = (m - k * f, nn - k * g);
511            let (a, b) = if row1.0.hypot(row1.1) >= row2.0.hypot(row2.1) {
512                (row1.1, -row1.0)
513            } else {
514                (row2.1, -row2.0)
515            };
516            let along = jet.du * a + jet.dv * b;
517            let scale = jet.du.magnitude().max(jet.dv.magnitude());
518            if along.magnitude() <= tol.angular() * scale * (a.abs() + b.abs()).max(1.0)
519                || a.abs() + b.abs() <= tol.angular() * (l.abs() + m.abs() + nn.abs()).max(1.0)
520            {
521                return Ok(jet.du);
522            }
523            Ok(along)
524        };
525        let max_direction = Direction::new(direction_for(max)?, tol)?;
526        let min_direction = if spread <= tol.angular() * max.abs().max(min.abs()).max(1.0) {
527            Direction::new(n.cross(max_direction.vector()), tol)?
528        } else {
529            Direction::new(direction_for(min)?, tol)?
530        };
531        Ok(SurfaceCurvature {
532            max,
533            min,
534            max_direction,
535            min_direction,
536            normal,
537        })
538    }
539
540    /// Whether the surface degenerates at `(u, v)`.
541    ///
542    /// # Errors
543    ///
544    /// As [`Surface::point_at`].
545    fn is_degenerate_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<bool> {
546        let (du, dv) = self.d1_at(u, v, tol)?;
547        let scale = du.magnitude().max(dv.magnitude());
548        Ok(du.cross(dv).magnitude() <= tol.angular() * scale * scale)
549    }
550
551    /// Bring `(u, v)` into the domain, wrapping in whichever directions are
552    /// periodic, or closed.
553    ///
554    /// A surface that merely closes on itself (a clamped B-spline tube
555    /// whose first and last control columns coincide) has the same points
556    /// at both ends of its domain exactly as a periodic one does, and a
557    /// parameter a whole period past the end names a point it has. A face
558    /// whose trim runs right round such a tube has a ring that straddles
559    /// the join whichever way it is slid, so somewhere it is asked past the
560    /// end; refusing there stopped three bodies of one assembly from
561    /// meshing at all. Closure is consulted only once a parameter is
562    /// actually outside, so the common case pays nothing for it.
563    ///
564    /// # Errors
565    ///
566    /// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) if a parameter is outside
567    /// a direction's domain by more than `tol.parametric()` and that
568    /// direction neither repeats nor closes.
569    fn normalize_parameters(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(f64, f64)> {
570        // Every evaluation of every surface passes through here, so the
571        // in-range path is kept to the two comparisons it always was, and
572        // everything rarer (wrapping a periodic direction, wrapping a
573        // closed one, refusing) lives out of line. Folding the rare cases
574        // into one closure with a `&dyn Fn` for closure cost a tenth of an
575        // assembly's meshing time, measured, for nothing on the common path.
576        let ((ua, ub), (va, vb)) = self.domain();
577        let slack = tol.parametric();
578        let u = if u >= ua - slack && u <= ub + slack && !self.is_periodic_u() {
579            u.clamp(ua, ub)
580        } else {
581            self.parameter_outside(u, ua, ub, self.is_periodic_u(), true, tol)?
582        };
583        let v = if v >= va - slack && v <= vb + slack && !self.is_periodic_v() {
584            v.clamp(va, vb)
585        } else {
586            self.parameter_outside(v, va, vb, self.is_periodic_v(), false, tol)?
587        };
588        Ok((u, v))
589    }
590
591    /// A parameter outside its domain, or on a periodic direction: wrapped
592    /// where the direction repeats or closes on itself, refused otherwise.
593    ///
594    /// A surface that merely closes on itself (a clamped B-spline tube
595    /// whose first and last control columns coincide) has the same points
596    /// at both ends of its domain exactly as a periodic one does, and a
597    /// parameter a whole period past the end names a point it has. A face
598    /// whose trim runs right round such a tube has a ring that straddles
599    /// the join whichever way it is slid, so somewhere it is asked past the
600    /// end; refusing there stopped three bodies of one assembly from
601    /// meshing at all. Closure is consulted only here, once a parameter is
602    /// actually outside.
603    ///
604    /// # Errors
605    ///
606    /// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) if the parameter is
607    /// outside by more than `tol.parametric()` and the direction neither
608    /// repeats nor closes.
609    #[cold]
610    fn parameter_outside(
611        &self,
612        t: f64,
613        a: f64,
614        b: f64,
615        periodic: bool,
616        across: bool,
617        tol: Tolerances,
618    ) -> OgeomResult<f64> {
619        if periodic {
620            return Ok(a + (t - a).rem_euclid(b - a));
621        }
622        let closed = if across {
623            self.is_closed_u(tol)
624        } else {
625            self.is_closed_v(tol)
626        };
627        if t.is_finite() && b > a && closed {
628            return Ok(a + (t - a).rem_euclid(b - a));
629        }
630        Err(ogeom_core::ogeom_err!(
631            Domain,
632            "{} parameter {t} outside [{a}, {b}]",
633            if across { "u" } else { "v" }
634        ))
635    }
636}
637
638/// Geometry that can be moved by a similarity.
639///
640/// Separate from the evaluation traits because a *view* of geometry (a
641/// borrowed adaptor over someone else's data) can be evaluated but not moved.
642pub trait Transformable: Sized {
643    /// This geometry moved by `t`.
644    ///
645    /// # Errors
646    ///
647    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
648    /// transformed geometry would be degenerate.
649    fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self>;
650}
651
652/// Reversible geometry: the same point set, traversed the other way.
653pub trait Reversible: Sized {
654    /// This geometry with its parameter direction reversed.
655    ///
656    /// The domain is preserved, so a curve reversed still runs over the same
657    /// interval; only the direction of travel changes. Preserving the domain
658    /// matters because trimming ranges elsewhere refer to it.
659    #[must_use]
660    fn reversed(&self) -> Self;
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666
667    #[test]
668    fn continuity_is_ordered_from_least_to_most_smooth() {
669        assert!(Continuity::C0 < Continuity::G1);
670        assert!(Continuity::G1 < Continuity::C1);
671        assert!(Continuity::C1 < Continuity::G2);
672        assert!(Continuity::G2 < Continuity::C2);
673        assert!(Continuity::C2 < Continuity::CInfinity);
674        // The ordering is what makes a requirement expressible as a comparison.
675        assert!(Continuity::CInfinity >= Continuity::C1);
676    }
677
678    #[test]
679    fn quadrics_and_analytics_are_classified_correctly() {
680        for k in [
681            SurfaceKind::Plane,
682            SurfaceKind::Cylinder,
683            SurfaceKind::Cone,
684            SurfaceKind::Sphere,
685        ] {
686            assert!(k.is_quadric(), "{k:?}");
687            assert!(k.is_analytic(), "{k:?}");
688        }
689        // A torus is analytic but quartic, not quadric, a distinction that
690        // matters, since quadric pairs have closed-form intersections and
691        // torus pairs do not.
692        assert!(!SurfaceKind::Torus.is_quadric());
693        assert!(SurfaceKind::Torus.is_analytic());
694
695        for k in [
696            SurfaceKind::BSpline,
697            SurfaceKind::Bezier,
698            SurfaceKind::Revolution,
699            SurfaceKind::Extrusion,
700            SurfaceKind::Trimmed,
701            SurfaceKind::Offset,
702        ] {
703            assert!(!k.is_quadric(), "{k:?}");
704            assert!(!k.is_analytic(), "{k:?}");
705        }
706    }
707}