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`), a banked one (`Curve3::Banked`), or an
234    ///   unknown curve or surface family.
235    ///
236    /// Additionally under a reflection, [`TransformError::Unsupported`] for:
237    /// - an intrinsic space curve (its torsion would change sign);
238    /// - a ruled or torus section curve, and a traced or paired section on a
239    ///   ruled, spherical or toroidal carrier;
240    /// - a lifted pcurve on any face;
241    /// - on a curved (non-planar, non-B-spline) face, any pcurve other than
242    ///   a line, circle, ellipse, polyline, B-spline or sinusoid;
243    /// - a pcurve shared by faces that would need different reflections.
244    pub fn transformed(&self, transform: &Transform3) -> Result<ExactBRep, TransformError> {
245        let rigid = Rigid::new(transform)?;
246        let source = &self.topology;
247
248        let mut curves3 = Vec::with_capacity(self.curves3.len());
249        let mut curve_reparam = Vec::with_capacity(self.curves3.len());
250        for curve in &self.curves3 {
251            let (curve, reparam) = curve3(curve, &rigid)?;
252            curves3.push(curve);
253            curve_reparam.push(reparam);
254        }
255        let mut surfaces = Vec::with_capacity(self.surfaces.len());
256        let mut axial = Vec::with_capacity(self.surfaces.len());
257        for surface in &self.surfaces {
258            let (surface, reflected) = surface_of(surface, &rigid)?;
259            surfaces.push(surface);
260            axial.push(reflected);
261        }
262
263        // A pcurve is reflected in `u` when its face's surface reads its
264        // angle backwards; a pcurve shared by faces that disagree cannot
265        // be both, and is refused.
266        let mut pcurve_reflect: Vec<Option<bool>> = vec![None; self.curves2.len()];
267        for face in source.faces() {
268            let Some(surface) = face.surface else {
269                continue;
270            };
271            let reflect = axial.get(surface.index()).copied().unwrap_or(false);
272            for bound in &face.bounds {
273                let Some(wire) = source.loops().get(bound.loop_id.index()) else {
274                    continue;
275                };
276                for use_ in &wire.edges {
277                    let Some(pcurve) = use_.pcurve else {
278                        continue;
279                    };
280                    let Some(slot) = pcurve_reflect.get_mut(pcurve.index()) else {
281                        continue;
282                    };
283                    match slot {
284                        Some(existing) if *existing != reflect => {
285                            return Err(TransformError::Unsupported(
286                                "a pcurve shared by faces of different families under a reflection",
287                            ));
288                        }
289                        _ => *slot = Some(reflect),
290                    }
291                }
292            }
293        }
294        let mut curves2 = Vec::with_capacity(self.curves2.len());
295        let mut pcurve_reparam = Vec::with_capacity(self.curves2.len());
296        for (index, curve) in self.curves2.iter().enumerate() {
297            let (curve, reparam) = curve2(curve, &rigid, pcurve_reflect[index].unwrap_or(false))?;
298            curves2.push(curve);
299            pcurve_reparam.push(reparam);
300        }
301
302        let mut topology = ExactTopology::default();
303        for vertex in source.vertices() {
304            topology.add_vertex(Vertex {
305                position: rigid.point(vertex.position),
306            });
307        }
308        let mut edge_intervals = self.edge_intervals.clone();
309        for (index, edge) in source.edges().iter().enumerate() {
310            let id = topology.add_edge(Edge {
311                start: edge.start,
312                end: edge.end,
313                curve: edge.curve,
314            });
315            let reparam = edge
316                .curve
317                .and_then(|curve| curve_reparam.get(curve.index()).copied())
318                .unwrap_or(Reparam::Same);
319            debug_assert_eq!(source.edge_id_at(index), Some(id));
320            if let Some(interval) = edge_intervals.get_mut(&id) {
321                *interval = reparam.interval(*interval);
322            }
323        }
324        let mut pcurve_intervals = self.pcurve_intervals.clone();
325        for (index, wire) in source.loops().iter().enumerate() {
326            let id = topology.add_loop(Loop {
327                edges: wire.edges.clone(),
328            });
329            debug_assert_eq!(source.loop_id_at(index), Some(id));
330            for (use_index, use_) in wire.edges.iter().enumerate() {
331                let reparam = use_
332                    .pcurve
333                    .and_then(|curve| pcurve_reparam.get(curve.index()).copied())
334                    .unwrap_or(Reparam::Same);
335                if let Some(interval) = pcurve_intervals.get_mut(&(id, use_index)) {
336                    *interval = reparam.interval(*interval);
337                }
338            }
339        }
340        for face in source.faces() {
341            let orientation = match (face.orientation, rigid.mirror) {
342                (orientation, false) => orientation,
343                (Orientation::Forward, true) => Orientation::Reversed,
344                (Orientation::Reversed, true) => Orientation::Forward,
345            };
346            topology.add_face(Face {
347                surface: face.surface,
348                bounds: face.bounds.clone(),
349                orientation,
350            });
351        }
352        for shell in source.shells() {
353            topology.add_shell(shell.clone());
354        }
355        for solid in source.solids() {
356            topology.add_solid(solid.clone());
357        }
358
359        Ok(ExactBRep {
360            topology,
361            curves3,
362            curves2,
363            surfaces,
364            edge_intervals,
365            pcurve_intervals,
366            face_names: self.face_names.clone(),
367            edge_names: self.edge_names.clone(),
368        })
369    }
370}
371
372/// The surface moved, and whether its angle now reads backwards.
373fn surface_of(surface: &Surface, rigid: &Rigid) -> Result<(Surface, bool), TransformError> {
374    let axial = rigid.mirror;
375    Ok(match surface {
376        Surface::Plane(p) => (
377            Surface::Plane(Plane {
378                frame: rigid.planar_frame(&p.frame),
379            }),
380            false,
381        ),
382        Surface::Cylinder(c) => (
383            Surface::Cylinder(Cylinder {
384                frame: rigid.axial_frame(&c.frame),
385                ..*c
386            }),
387            axial,
388        ),
389        Surface::EllipticalCylinder(c) => (
390            Surface::EllipticalCylinder(EllipticalCylinder {
391                frame: rigid.axial_frame(&c.frame),
392                ..*c
393            }),
394            axial,
395        ),
396        Surface::Cone(c) => (
397            Surface::Cone(Cone {
398                frame: rigid.axial_frame(&c.frame),
399                ..*c
400            }),
401            axial,
402        ),
403        Surface::Sphere(s) => (
404            Surface::Sphere(Sphere {
405                frame: rigid.axial_frame(&s.frame),
406                ..*s
407            }),
408            axial,
409        ),
410        Surface::Torus(t) => (
411            Surface::Torus(Torus {
412                frame: rigid.axial_frame(&t.frame),
413                ..*t
414            }),
415            axial,
416        ),
417        Surface::BSpline(b) => (Surface::BSpline(spline_surface(b, rigid)), false),
418        _ => return Err(TransformError::Unsupported("an unknown surface family")),
419    })
420}
421
422fn spline_surface(surface: &BSplineSurface, rigid: &Rigid) -> BSplineSurface {
423    BSplineSurface {
424        control_points: surface
425            .control_points
426            .iter()
427            .map(|row| row.iter().map(|p| rigid.point(*p)).collect())
428            .collect(),
429        ..surface.clone()
430    }
431}
432
433fn carrier(value: &Carrier, rigid: &Rigid) -> Result<Carrier, TransformError> {
434    Ok(match value {
435        Carrier::Plane(frame) => Carrier::Plane(rigid.planar_frame(frame)),
436        Carrier::Ruled(ruled) => {
437            rigid.unsupported_mirrored("a curve on a ruled carrier under a reflection")?;
438            Carrier::Ruled(RuledCarrier {
439                frame: rigid.axial_frame(&ruled.frame),
440                ..*ruled
441            })
442        }
443        Carrier::Sphere { frame, radius } => {
444            rigid.unsupported_mirrored("a curve on a spherical carrier under a reflection")?;
445            Carrier::Sphere {
446                frame: rigid.axial_frame(frame),
447                radius: *radius,
448            }
449        }
450        Carrier::Torus(torus) => {
451            rigid.unsupported_mirrored("a curve on a toroidal carrier under a reflection")?;
452            Carrier::Torus(TorusCarrier {
453                frame: rigid.axial_frame(&torus.frame),
454                ..*torus
455            })
456        }
457        Carrier::Spline(surface) => Carrier::Spline(Box::new(spline_surface(surface, rigid))),
458    })
459}
460
461fn curve3(curve: &Curve3, rigid: &Rigid) -> Result<(Curve3, Reparam), TransformError> {
462    Ok(match curve {
463        Curve3::Line(line) => (
464            Curve3::Line(Line3 {
465                origin: rigid.point(line.origin),
466                direction: rigid.vector(line.direction),
467            }),
468            Reparam::Same,
469        ),
470        Curve3::Circle(circle) => (
471            Curve3::Circle(Circle3 {
472                frame: rigid.planar_frame(&circle.frame),
473                ..*circle
474            }),
475            Reparam::Same,
476        ),
477        Curve3::Ellipse(ellipse) => (
478            Curve3::Ellipse(Ellipse3 {
479                frame: rigid.planar_frame(&ellipse.frame),
480                ..*ellipse
481            }),
482            Reparam::Same,
483        ),
484        Curve3::Polyline(polyline) => (
485            Curve3::Polyline(Polyline {
486                points: polyline.points.iter().map(|p| rigid.point(*p)).collect(),
487                closed: polyline.closed,
488            }),
489            Reparam::Same,
490        ),
491        Curve3::BSpline(spline) => (
492            Curve3::BSpline(BSplineCurve {
493                control_points: spline
494                    .control_points
495                    .iter()
496                    .map(|p| rigid.point(*p))
497                    .collect(),
498                ..spline.clone()
499            }),
500            Reparam::Same,
501        ),
502        Curve3::Intrinsic(intrinsic) => {
503            // A reflection reverses the sense of the screw: the torsion law
504            // would have to change sign.
505            rigid.unsupported_mirrored("an intrinsic space curve under a reflection")?;
506            (
507                Curve3::Intrinsic(Intrinsic3 {
508                    start: rigid.planar_frame(&intrinsic.start),
509                    ..intrinsic.clone()
510                }),
511                Reparam::Same,
512            )
513        }
514        Curve3::RuledSection(section) => {
515            rigid.unsupported_mirrored("a ruled section under a reflection")?;
516            (
517                Curve3::RuledSection(RuledSection3 {
518                    carrier: RuledCarrier {
519                        frame: rigid.axial_frame(&section.carrier.frame),
520                        ..section.carrier
521                    },
522                    graph: section.graph,
523                }),
524                Reparam::Same,
525            )
526        }
527        Curve3::TorusSection(section) => {
528            rigid.unsupported_mirrored("a torus section under a reflection")?;
529            (
530                Curve3::TorusSection(TorusSection3 {
531                    torus: TorusCarrier {
532                        frame: rigid.axial_frame(&section.torus.frame),
533                        ..section.torus
534                    },
535                    graph: section.graph,
536                }),
537                Reparam::Same,
538            )
539        }
540        Curve3::ImplicitSection(section) => (
541            Curve3::ImplicitSection(ImplicitSection3 {
542                carrier: carrier(&section.carrier, rigid)?,
543                curve: section.curve.clone(),
544            }),
545            Reparam::Same,
546        ),
547        Curve3::PairSection(section) => (
548            Curve3::PairSection(PairSection3 {
549                first: carrier(&section.first, rigid)?,
550                second: carrier(&section.second, rigid)?,
551                nodes: section
552                    .nodes
553                    .iter()
554                    .map(|node| PairNode {
555                        point: rigid.point(node.point),
556                        ..*node
557                    })
558                    .collect(),
559            }),
560            Reparam::Same,
561        ),
562        Curve3::Elevated(_) => {
563            return Err(TransformError::Unsupported(
564                "an elevated alignment curve (plan and height law)",
565            ))
566        }
567        // Its cant is measured against +Z, which a motion would tilt.
568        Curve3::Banked(_) => {
569            return Err(TransformError::Unsupported(
570                "a banked alignment curve (centreline and cant law)",
571            ))
572        }
573        _ => return Err(TransformError::Unsupported("an unknown curve family")),
574    })
575}
576
577/// A parameter-plane point reflected in `u`: `(2 pi - u, v)`.
578fn reflect_point(p: Point2) -> Point2 {
579    Point2::new(TAU - p.x, p.y)
580}
581
582/// A parameter-plane direction reflected in `u`.
583fn reflect_vector(v: Vec2) -> Vec2 {
584    Vec2::new(-v.x, v.y)
585}
586
587/// A conic frame reflected in `u`, kept right-handed by reversing `y`; the
588/// point at `t` is then at `-t`.
589fn reflect_conic_frame(frame: &Frame2) -> Frame2 {
590    Frame2 {
591        origin: reflect_point(frame.origin),
592        x: reflect_vector(frame.x),
593        y: -reflect_vector(frame.y),
594    }
595}
596
597/// The pcurve under the motion. On a face whose angle now reads backwards
598/// (`reflect`), it is reflected in `u`; otherwise its points keep their
599/// parameters, and only a pcurve holding geometry in space moves.
600fn curve2(
601    curve: &Curve2,
602    rigid: &Rigid,
603    reflect: bool,
604) -> Result<(Curve2, Reparam), TransformError> {
605    if let Curve2::Lifted(lifted) = curve {
606        // The pcurve shares its space curve's parameter: it moves with it,
607        // and only where nothing is reparameterised.
608        rigid.unsupported_mirrored("a lifted pcurve under a reflection")?;
609        let (space, _) = curve3(&lifted.curve, rigid)?;
610        return Ok((
611            Curve2::Lifted(LiftedCurve2 {
612                curve: Box::new(space),
613                carrier: carrier(&lifted.carrier, rigid)?,
614                ..lifted.clone()
615            }),
616            Reparam::Same,
617        ));
618    }
619    if !reflect {
620        return Ok((curve.clone(), Reparam::Same));
621    }
622    Ok(match curve {
623        Curve2::Line(line) => (
624            Curve2::Line(Line2 {
625                origin: reflect_point(line.origin),
626                direction: reflect_vector(line.direction),
627            }),
628            Reparam::Same,
629        ),
630        Curve2::Circle(circle) => (
631            Curve2::Circle(Circle2 {
632                frame: reflect_conic_frame(&circle.frame),
633                ..*circle
634            }),
635            Reparam::Negated,
636        ),
637        Curve2::Ellipse(ellipse) => (
638            Curve2::Ellipse(Ellipse2 {
639                frame: reflect_conic_frame(&ellipse.frame),
640                ..*ellipse
641            }),
642            Reparam::Negated,
643        ),
644        Curve2::Polyline(polyline) => (
645            Curve2::Polyline(Polyline {
646                points: polyline.points.iter().map(|p| reflect_point(*p)).collect(),
647                closed: polyline.closed,
648            }),
649            Reparam::Same,
650        ),
651        Curve2::BSpline(spline) => (
652            Curve2::BSpline(BSplineCurve {
653                control_points: spline
654                    .control_points
655                    .iter()
656                    .map(|p| reflect_point(*p))
657                    .collect(),
658                ..spline.clone()
659            }),
660            Reparam::Same,
661        ),
662        // The point at `t` is `(t, v(t))`; reflected it is `(s, v)` with
663        // `s = 2 pi - t`, where `cos` keeps its sign and `sin` changes it.
664        Curve2::Sinusoid(wave) => (
665            Curve2::Sinusoid(Sinusoid2 {
666                sine: -wave.sine,
667                ..*wave
668            }),
669            Reparam::Reflected,
670        ),
671        _ => {
672            return Err(TransformError::Unsupported(
673                "a pcurve family on a curved face under a reflection",
674            ))
675        }
676    })
677}