Skip to main content

axiolid_brep/
transform.rs

1//! Rigid placement of an exact B-rep (#223).
2//!
3//! # What a rigid transform preserves
4//!
5//! A rotation, a translation and a reflection map every support family the
6//! catalog carries onto the same family: a plane stays a plane, a cylinder
7//! a cylinder of the same radius, a circle a circle, a B-spline a B-spline
8//! over the same knots and weights with its control points moved. So
9//! [`ExactBRep::transformed`] is a new exact B-rep of the same topology and
10//! the same families, never a tessellation or a refit. Any other affine map
11//! -- a scale, a shear -- turns circles into ellipses or worse and is
12//! refused, not approximated.
13//!
14//! Coordinates are mapped in `f64`: each frame axis, origin, control point
15//! and vertex is the transform applied once, rounded once. The result is the
16//! exact B-rep of those numbers, as every constructed B-rep is the exact
17//! B-rep of the numbers it was built from; the rounding is a few units in
18//! the last place of the coordinates, not a change of representation.
19//!
20//! # Proper rotations
21//!
22//! A rotation moves every frame rigidly. Every point keeps its parameters,
23//! so edge intervals, pcurves and pcurve intervals are unchanged, and the
24//! curves that carry their own surface (sections, traced and lifted
25//! curves) move with their carriers.
26//!
27//! # Reflections
28//!
29//! A reflection `M` (determinant -1) keeps a frame orthonormal but makes it
30//! left-handed, which the evaluators refuse. So each frame is re-chosen
31//! right-handed, with its parameterisation adjusted so every point keeps
32//! its place:
33//!
34//! - a plane, circle or ellipse takes `(M x, M y, -M z)`: points keep their
35//!   parameters;
36//! - a cylinder, elliptical cylinder, cone, sphere and torus take
37//!   `(M x, -M y, M z)`, which reads the angle about the axis backwards, so
38//!   a point at `u` sits at `2 pi - u` and each pcurve on the face is
39//!   reflected in `u` to match;
40//! - a B-spline keeps its parameters; its control points are reflected.
41//!
42//! A reflection reverses the winding of every loop about the mirrored
43//! outward normal, so every face is flipped: the solid stays outward
44//! oriented and measures a positive volume. A pcurve family that cannot be
45//! reflected in `u` in closed form here (graphs of quadric and torus
46//! sections, traced and lifted curves, intrinsic curves) on a curved face,
47//! and a curve that carries such a surface, are refused by name.
48//!
49//! # Refused for every transform
50//!
51//! An elevated alignment curve is a plan curve with a height law: only a
52//! motion that keeps the vertical axis keeps that form, and re-expressing
53//! the law is not implemented. An unknown support family is refused too.
54
55use std::f64::consts::TAU;
56use std::fmt;
57
58use axiolid_core::{Frame2, Frame3, Interval, Point2, Scalar, Transform3, Vec2, Vec3};
59use axiolid_curve::{
60    BSplineCurve, BSplineSurface, Carrier, Circle2, Circle3, Curve2, Curve3, Ellipse2, Ellipse3,
61    ImplicitSection3, Intrinsic3, LiftedCurve2, Line2, Line3, PairNode, PairSection3, Polyline,
62    RuledCarrier, RuledSection3, Sinusoid2, TorusCarrier, TorusSection3,
63};
64use axiolid_surface::{Cone, Cylinder, EllipticalCylinder, Plane, Sphere, Surface, Torus};
65use axiolid_topology::{Edge, Face, Loop, Orientation, Vertex};
66
67use crate::{ExactBRep, ExactTopology};
68
69/// How far the columns of a transform's linear part may stray from an
70/// orthonormal basis, in each dot product, and still be read as rigid.
71///
72/// A rotation assembled from unit axes or from sines and cosines is
73/// orthonormal to a few units in the last place; a matrix off by more than
74/// this carries a scale or shear that no rigid copy of the solid represents.
75pub const RIGID_TOLERANCE: Scalar = 1e-12;
76
77/// Why an exact B-rep could not be placed.
78#[non_exhaustive]
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum TransformError {
81    /// The transform has a non-finite entry.
82    NonFinite,
83    /// The linear part is not orthonormal within [`RIGID_TOLERANCE`]: a
84    /// scale or shear, which no rigid copy of the solid represents.
85    NotRigid,
86    /// A support the transform cannot carry exactly, named.
87    Unsupported(&'static str),
88}
89
90impl fmt::Display for TransformError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::NonFinite => f.write_str("the transform has a non-finite entry"),
94            Self::NotRigid => f.write_str(
95                "the transform is not rigid: its linear part is not orthonormal \
96                 (a scale or shear)",
97            ),
98            Self::Unsupported(what) => write!(f, "{what} cannot be placed exactly"),
99        }
100    }
101}
102
103impl std::error::Error for TransformError {}
104
105/// A validated rigid motion.
106#[derive(Debug, Clone, Copy)]
107struct Rigid {
108    transform: Transform3,
109    mirror: bool,
110}
111
112impl Rigid {
113    fn new(transform: &Transform3) -> Result<Self, TransformError> {
114        let m = transform.matrix3;
115        let columns = [m.x_axis, m.y_axis, m.z_axis];
116        if !columns.iter().all(|c| c.is_finite()) || !transform.translation.is_finite() {
117            return Err(TransformError::NonFinite);
118        }
119        for i in 0..3 {
120            for j in i..3 {
121                let want = if i == j { 1.0 } else { 0.0 };
122                if (columns[i].dot(columns[j]) - want).abs() > RIGID_TOLERANCE {
123                    return Err(TransformError::NotRigid);
124                }
125            }
126        }
127        Ok(Self {
128            transform: *transform,
129            mirror: m.determinant() < 0.0,
130        })
131    }
132
133    fn point(&self, p: Vec3) -> Vec3 {
134        self.transform.transform_point3(p)
135    }
136
137    fn vector(&self, v: Vec3) -> Vec3 {
138        self.transform.transform_vector3(v)
139    }
140
141    /// A frame whose points keep their parameters in the plane of `x, y`:
142    /// `(M x, M y, +-M z)`, right-handed either way.
143    fn planar_frame(&self, frame: &Frame3) -> Frame3 {
144        let z = self.vector(frame.z);
145        Frame3 {
146            origin: self.point(frame.origin),
147            x: self.vector(frame.x),
148            y: self.vector(frame.y),
149            z: if self.mirror { -z } else { z },
150        }
151    }
152
153    /// A frame about an axis: `(M x, -+M y, M z)`. Under a reflection the
154    /// angle about `z` reads backwards, `u -> 2 pi - u`.
155    fn axial_frame(&self, frame: &Frame3) -> Frame3 {
156        let y = self.vector(frame.y);
157        Frame3 {
158            origin: self.point(frame.origin),
159            x: self.vector(frame.x),
160            y: if self.mirror { -y } else { y },
161            z: self.vector(frame.z),
162        }
163    }
164
165    fn unsupported_mirrored(&self, what: &'static str) -> Result<(), TransformError> {
166        if self.mirror {
167            Err(TransformError::Unsupported(what))
168        } else {
169            Ok(())
170        }
171    }
172}
173
174/// How a transformed curve's parameter relates to the original's.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176enum Reparam {
177    /// The same point at the same parameter.
178    Same,
179    /// The point at `t` is now at `-t`.
180    Negated,
181    /// The point at `t` is now at `2 pi - t`: a graph over the angle `u`,
182    /// whose parameter is `u` itself, reflected in `u`.
183    Reflected,
184}
185
186impl Reparam {
187    fn interval(self, interval: Interval) -> Interval {
188        match self {
189            Self::Same => interval,
190            Self::Negated => Interval::new(-interval.start, -interval.end),
191            Self::Reflected => Interval::new(TAU - interval.start, TAU - interval.end),
192        }
193    }
194}
195
196impl ExactBRep {
197    /// This B-rep moved by the rigid motion `transform` (#223).
198    ///
199    /// # Exactness
200    ///
201    /// The result is exact for the transform *as given in `f64`*, after one
202    /// rounding per coordinate -- not for the ideal rotation the matrix
203    /// approximates. Every vertex position, frame origin and axis, line
204    /// origin and direction, and control point is `transform` applied once
205    /// in `f64`, so each coordinate carries a few units in the last place
206    /// of error relative to the ideal placement, and a rotated frame is
207    /// orthonormal only to that rounding. A matrix accepted within
208    /// [`RIGID_TOLERANCE`] is applied as given, not re-orthonormalised.
209    /// What IS exact: every curve and surface stays in its own family with
210    /// its radii, semi-axes, angles, knots and weights unchanged; the
211    /// topology, structural names and parameter intervals are kept (a
212    /// reflection reparameterises as below); nothing is tessellated,
213    /// sampled or refitted. Vertices and the curves through them are
214    /// mapped by the same rounded transform, so they agree to the same
215    /// rounding they agreed to before.
216    ///
217    /// A rotation and a translation keep every parameter. A reflection
218    /// (determinant -1) keeps each frame right-handed: curved surfaces
219    /// (cylinders, elliptical cylinders, cones, spheres, tori) then read
220    /// their angle as `u -> 2 pi - u` and their pcurves are reflected to
221    /// match, and every face is flipped, so a solid stays outward oriented
222    /// (see the module docs).
223    ///
224    /// # Errors
225    ///
226    /// For every transform:
227    /// - [`TransformError::NonFinite`]: a NaN or infinite matrix entry or
228    ///   translation;
229    /// - [`TransformError::NotRigid`]: a linear part whose columns are not
230    ///   orthonormal within [`RIGID_TOLERANCE`] in every dot product -- any
231    ///   scale (uniform or not) or shear;
232    /// - [`TransformError::Unsupported`]: an elevated alignment curve
233    ///   (`Curve3::Elevated`), or an unknown curve or surface family.
234    ///
235    /// Additionally under a reflection, [`TransformError::Unsupported`] for:
236    /// - an intrinsic space curve (its torsion would change sign);
237    /// - a ruled or torus section curve, and a traced or paired section on a
238    ///   ruled, spherical or toroidal carrier;
239    /// - a lifted pcurve on any face;
240    /// - on a curved (non-planar, non-B-spline) face, any pcurve other than
241    ///   a line, circle, ellipse, polyline, B-spline or sinusoid;
242    /// - a pcurve shared by faces that would need different reflections.
243    pub fn transformed(&self, transform: &Transform3) -> Result<ExactBRep, TransformError> {
244        let rigid = Rigid::new(transform)?;
245        let source = &self.topology;
246
247        let mut curves3 = Vec::with_capacity(self.curves3.len());
248        let mut curve_reparam = Vec::with_capacity(self.curves3.len());
249        for curve in &self.curves3 {
250            let (curve, reparam) = curve3(curve, &rigid)?;
251            curves3.push(curve);
252            curve_reparam.push(reparam);
253        }
254        let mut surfaces = Vec::with_capacity(self.surfaces.len());
255        let mut axial = Vec::with_capacity(self.surfaces.len());
256        for surface in &self.surfaces {
257            let (surface, reflected) = surface_of(surface, &rigid)?;
258            surfaces.push(surface);
259            axial.push(reflected);
260        }
261
262        // A pcurve is reflected in `u` when its face's surface reads its
263        // angle backwards; a pcurve shared by faces that disagree cannot
264        // be both, and is refused.
265        let mut pcurve_reflect: Vec<Option<bool>> = vec![None; self.curves2.len()];
266        for face in source.faces() {
267            let Some(surface) = face.surface else {
268                continue;
269            };
270            let reflect = axial.get(surface.index()).copied().unwrap_or(false);
271            for bound in &face.bounds {
272                let Some(wire) = source.loops().get(bound.loop_id.index()) else {
273                    continue;
274                };
275                for use_ in &wire.edges {
276                    let Some(pcurve) = use_.pcurve else {
277                        continue;
278                    };
279                    let Some(slot) = pcurve_reflect.get_mut(pcurve.index()) else {
280                        continue;
281                    };
282                    match slot {
283                        Some(existing) if *existing != reflect => {
284                            return Err(TransformError::Unsupported(
285                                "a pcurve shared by faces of different families under a reflection",
286                            ));
287                        }
288                        _ => *slot = Some(reflect),
289                    }
290                }
291            }
292        }
293        let mut curves2 = Vec::with_capacity(self.curves2.len());
294        let mut pcurve_reparam = Vec::with_capacity(self.curves2.len());
295        for (index, curve) in self.curves2.iter().enumerate() {
296            let (curve, reparam) = curve2(curve, &rigid, pcurve_reflect[index].unwrap_or(false))?;
297            curves2.push(curve);
298            pcurve_reparam.push(reparam);
299        }
300
301        let mut topology = ExactTopology::default();
302        for vertex in source.vertices() {
303            topology.add_vertex(Vertex {
304                position: rigid.point(vertex.position),
305            });
306        }
307        let mut edge_intervals = self.edge_intervals.clone();
308        for (index, edge) in source.edges().iter().enumerate() {
309            let id = topology.add_edge(Edge {
310                start: edge.start,
311                end: edge.end,
312                curve: edge.curve,
313            });
314            let reparam = edge
315                .curve
316                .and_then(|curve| curve_reparam.get(curve.index()).copied())
317                .unwrap_or(Reparam::Same);
318            debug_assert_eq!(source.edge_id_at(index), Some(id));
319            if let Some(interval) = edge_intervals.get_mut(&id) {
320                *interval = reparam.interval(*interval);
321            }
322        }
323        let mut pcurve_intervals = self.pcurve_intervals.clone();
324        for (index, wire) in source.loops().iter().enumerate() {
325            let id = topology.add_loop(Loop {
326                edges: wire.edges.clone(),
327            });
328            debug_assert_eq!(source.loop_id_at(index), Some(id));
329            for (use_index, use_) in wire.edges.iter().enumerate() {
330                let reparam = use_
331                    .pcurve
332                    .and_then(|curve| pcurve_reparam.get(curve.index()).copied())
333                    .unwrap_or(Reparam::Same);
334                if let Some(interval) = pcurve_intervals.get_mut(&(id, use_index)) {
335                    *interval = reparam.interval(*interval);
336                }
337            }
338        }
339        for face in source.faces() {
340            let orientation = match (face.orientation, rigid.mirror) {
341                (orientation, false) => orientation,
342                (Orientation::Forward, true) => Orientation::Reversed,
343                (Orientation::Reversed, true) => Orientation::Forward,
344            };
345            topology.add_face(Face {
346                surface: face.surface,
347                bounds: face.bounds.clone(),
348                orientation,
349            });
350        }
351        for shell in source.shells() {
352            topology.add_shell(shell.clone());
353        }
354        for solid in source.solids() {
355            topology.add_solid(solid.clone());
356        }
357
358        Ok(ExactBRep {
359            topology,
360            curves3,
361            curves2,
362            surfaces,
363            edge_intervals,
364            pcurve_intervals,
365            face_names: self.face_names.clone(),
366            edge_names: self.edge_names.clone(),
367        })
368    }
369}
370
371/// The surface moved, and whether its angle now reads backwards.
372fn surface_of(surface: &Surface, rigid: &Rigid) -> Result<(Surface, bool), TransformError> {
373    let axial = rigid.mirror;
374    Ok(match surface {
375        Surface::Plane(p) => (
376            Surface::Plane(Plane {
377                frame: rigid.planar_frame(&p.frame),
378            }),
379            false,
380        ),
381        Surface::Cylinder(c) => (
382            Surface::Cylinder(Cylinder {
383                frame: rigid.axial_frame(&c.frame),
384                ..*c
385            }),
386            axial,
387        ),
388        Surface::EllipticalCylinder(c) => (
389            Surface::EllipticalCylinder(EllipticalCylinder {
390                frame: rigid.axial_frame(&c.frame),
391                ..*c
392            }),
393            axial,
394        ),
395        Surface::Cone(c) => (
396            Surface::Cone(Cone {
397                frame: rigid.axial_frame(&c.frame),
398                ..*c
399            }),
400            axial,
401        ),
402        Surface::Sphere(s) => (
403            Surface::Sphere(Sphere {
404                frame: rigid.axial_frame(&s.frame),
405                ..*s
406            }),
407            axial,
408        ),
409        Surface::Torus(t) => (
410            Surface::Torus(Torus {
411                frame: rigid.axial_frame(&t.frame),
412                ..*t
413            }),
414            axial,
415        ),
416        Surface::BSpline(b) => (Surface::BSpline(spline_surface(b, rigid)), false),
417        _ => return Err(TransformError::Unsupported("an unknown surface family")),
418    })
419}
420
421fn spline_surface(surface: &BSplineSurface, rigid: &Rigid) -> BSplineSurface {
422    BSplineSurface {
423        control_points: surface
424            .control_points
425            .iter()
426            .map(|row| row.iter().map(|p| rigid.point(*p)).collect())
427            .collect(),
428        ..surface.clone()
429    }
430}
431
432fn carrier(value: &Carrier, rigid: &Rigid) -> Result<Carrier, TransformError> {
433    Ok(match value {
434        Carrier::Plane(frame) => Carrier::Plane(rigid.planar_frame(frame)),
435        Carrier::Ruled(ruled) => {
436            rigid.unsupported_mirrored("a curve on a ruled carrier under a reflection")?;
437            Carrier::Ruled(RuledCarrier {
438                frame: rigid.axial_frame(&ruled.frame),
439                ..*ruled
440            })
441        }
442        Carrier::Sphere { frame, radius } => {
443            rigid.unsupported_mirrored("a curve on a spherical carrier under a reflection")?;
444            Carrier::Sphere {
445                frame: rigid.axial_frame(frame),
446                radius: *radius,
447            }
448        }
449        Carrier::Torus(torus) => {
450            rigid.unsupported_mirrored("a curve on a toroidal carrier under a reflection")?;
451            Carrier::Torus(TorusCarrier {
452                frame: rigid.axial_frame(&torus.frame),
453                ..*torus
454            })
455        }
456        Carrier::Spline(surface) => Carrier::Spline(Box::new(spline_surface(surface, rigid))),
457    })
458}
459
460fn curve3(curve: &Curve3, rigid: &Rigid) -> Result<(Curve3, Reparam), TransformError> {
461    Ok(match curve {
462        Curve3::Line(line) => (
463            Curve3::Line(Line3 {
464                origin: rigid.point(line.origin),
465                direction: rigid.vector(line.direction),
466            }),
467            Reparam::Same,
468        ),
469        Curve3::Circle(circle) => (
470            Curve3::Circle(Circle3 {
471                frame: rigid.planar_frame(&circle.frame),
472                ..*circle
473            }),
474            Reparam::Same,
475        ),
476        Curve3::Ellipse(ellipse) => (
477            Curve3::Ellipse(Ellipse3 {
478                frame: rigid.planar_frame(&ellipse.frame),
479                ..*ellipse
480            }),
481            Reparam::Same,
482        ),
483        Curve3::Polyline(polyline) => (
484            Curve3::Polyline(Polyline {
485                points: polyline.points.iter().map(|p| rigid.point(*p)).collect(),
486                closed: polyline.closed,
487            }),
488            Reparam::Same,
489        ),
490        Curve3::BSpline(spline) => (
491            Curve3::BSpline(BSplineCurve {
492                control_points: spline
493                    .control_points
494                    .iter()
495                    .map(|p| rigid.point(*p))
496                    .collect(),
497                ..spline.clone()
498            }),
499            Reparam::Same,
500        ),
501        Curve3::Intrinsic(intrinsic) => {
502            // A reflection reverses the sense of the screw: the torsion law
503            // would have to change sign.
504            rigid.unsupported_mirrored("an intrinsic space curve under a reflection")?;
505            (
506                Curve3::Intrinsic(Intrinsic3 {
507                    start: rigid.planar_frame(&intrinsic.start),
508                    ..intrinsic.clone()
509                }),
510                Reparam::Same,
511            )
512        }
513        Curve3::RuledSection(section) => {
514            rigid.unsupported_mirrored("a ruled section under a reflection")?;
515            (
516                Curve3::RuledSection(RuledSection3 {
517                    carrier: RuledCarrier {
518                        frame: rigid.axial_frame(&section.carrier.frame),
519                        ..section.carrier
520                    },
521                    graph: section.graph,
522                }),
523                Reparam::Same,
524            )
525        }
526        Curve3::TorusSection(section) => {
527            rigid.unsupported_mirrored("a torus section under a reflection")?;
528            (
529                Curve3::TorusSection(TorusSection3 {
530                    torus: TorusCarrier {
531                        frame: rigid.axial_frame(&section.torus.frame),
532                        ..section.torus
533                    },
534                    graph: section.graph,
535                }),
536                Reparam::Same,
537            )
538        }
539        Curve3::ImplicitSection(section) => (
540            Curve3::ImplicitSection(ImplicitSection3 {
541                carrier: carrier(&section.carrier, rigid)?,
542                curve: section.curve.clone(),
543            }),
544            Reparam::Same,
545        ),
546        Curve3::PairSection(section) => (
547            Curve3::PairSection(PairSection3 {
548                first: carrier(&section.first, rigid)?,
549                second: carrier(&section.second, rigid)?,
550                nodes: section
551                    .nodes
552                    .iter()
553                    .map(|node| PairNode {
554                        point: rigid.point(node.point),
555                        ..*node
556                    })
557                    .collect(),
558            }),
559            Reparam::Same,
560        ),
561        Curve3::Elevated(_) => {
562            return Err(TransformError::Unsupported(
563                "an elevated alignment curve (plan and height law)",
564            ))
565        }
566        _ => return Err(TransformError::Unsupported("an unknown curve family")),
567    })
568}
569
570/// A parameter-plane point reflected in `u`: `(2 pi - u, v)`.
571fn reflect_point(p: Point2) -> Point2 {
572    Point2::new(TAU - p.x, p.y)
573}
574
575/// A parameter-plane direction reflected in `u`.
576fn reflect_vector(v: Vec2) -> Vec2 {
577    Vec2::new(-v.x, v.y)
578}
579
580/// A conic frame reflected in `u`, kept right-handed by reversing `y`; the
581/// point at `t` is then at `-t`.
582fn reflect_conic_frame(frame: &Frame2) -> Frame2 {
583    Frame2 {
584        origin: reflect_point(frame.origin),
585        x: reflect_vector(frame.x),
586        y: -reflect_vector(frame.y),
587    }
588}
589
590/// The pcurve under the motion. On a face whose angle now reads backwards
591/// (`reflect`), it is reflected in `u`; otherwise its points keep their
592/// parameters, and only a pcurve holding geometry in space moves.
593fn curve2(
594    curve: &Curve2,
595    rigid: &Rigid,
596    reflect: bool,
597) -> Result<(Curve2, Reparam), TransformError> {
598    if let Curve2::Lifted(lifted) = curve {
599        // The pcurve shares its space curve's parameter: it moves with it,
600        // and only where nothing is reparameterised.
601        rigid.unsupported_mirrored("a lifted pcurve under a reflection")?;
602        let (space, _) = curve3(&lifted.curve, rigid)?;
603        return Ok((
604            Curve2::Lifted(LiftedCurve2 {
605                curve: Box::new(space),
606                carrier: carrier(&lifted.carrier, rigid)?,
607                ..lifted.clone()
608            }),
609            Reparam::Same,
610        ));
611    }
612    if !reflect {
613        return Ok((curve.clone(), Reparam::Same));
614    }
615    Ok(match curve {
616        Curve2::Line(line) => (
617            Curve2::Line(Line2 {
618                origin: reflect_point(line.origin),
619                direction: reflect_vector(line.direction),
620            }),
621            Reparam::Same,
622        ),
623        Curve2::Circle(circle) => (
624            Curve2::Circle(Circle2 {
625                frame: reflect_conic_frame(&circle.frame),
626                ..*circle
627            }),
628            Reparam::Negated,
629        ),
630        Curve2::Ellipse(ellipse) => (
631            Curve2::Ellipse(Ellipse2 {
632                frame: reflect_conic_frame(&ellipse.frame),
633                ..*ellipse
634            }),
635            Reparam::Negated,
636        ),
637        Curve2::Polyline(polyline) => (
638            Curve2::Polyline(Polyline {
639                points: polyline.points.iter().map(|p| reflect_point(*p)).collect(),
640                closed: polyline.closed,
641            }),
642            Reparam::Same,
643        ),
644        Curve2::BSpline(spline) => (
645            Curve2::BSpline(BSplineCurve {
646                control_points: spline
647                    .control_points
648                    .iter()
649                    .map(|p| reflect_point(*p))
650                    .collect(),
651                ..spline.clone()
652            }),
653            Reparam::Same,
654        ),
655        // The point at `t` is `(t, v(t))`; reflected it is `(s, v)` with
656        // `s = 2 pi - t`, where `cos` keeps its sign and `sin` changes it.
657        Curve2::Sinusoid(wave) => (
658            Curve2::Sinusoid(Sinusoid2 {
659                sine: -wave.sine,
660                ..*wave
661            }),
662            Reparam::Reflected,
663        ),
664        _ => {
665            return Err(TransformError::Unsupported(
666                "a pcurve family on a curved face under a reflection",
667            ))
668        }
669    })
670}