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}