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