Skip to main content

brep_kernel/brep/
transform_topology.rs

1use crate::topology::BrepSolid;
2use crate::{NurbsCurve, NurbsSurface, Vec3, Vec4};
3
4#[derive(Clone, Copy, Debug)]
5pub struct AffineTransform {
6    pub elements: [f64; 16],
7}
8
9impl AffineTransform {
10    pub fn new(elements: [f64; 16]) -> Result<Self, String> {
11        if elements.iter().any(|value| !value.is_finite()) {
12            return Err("AffineTransform: matrix must be finite".into());
13        }
14        if elements[12].abs() > 1e-12
15            || elements[13].abs() > 1e-12
16            || elements[14].abs() > 1e-12
17            || (elements[15] - 1.0).abs() > 1e-12
18        {
19            return Err("AffineTransform: projective matrices are unsupported".into());
20        }
21        if (Self { elements }).determinant3().abs() <= 1e-14 {
22            return Err("AffineTransform: singular matrix".into());
23        }
24        Ok(Self { elements })
25    }
26
27    pub fn determinant3(self) -> f64 {
28        let m = self.elements;
29        m[0] * (m[5] * m[10] - m[6] * m[9]) - m[1] * (m[4] * m[10] - m[6] * m[8])
30            + m[2] * (m[4] * m[9] - m[5] * m[8])
31    }
32
33    pub fn point(self, point: Vec3) -> Vec3 {
34        let m = self.elements;
35        Vec3::new(
36            m[0] * point.x + m[1] * point.y + m[2] * point.z + m[3],
37            m[4] * point.x + m[5] * point.y + m[6] * point.z + m[7],
38            m[8] * point.x + m[9] * point.y + m[10] * point.z + m[11],
39        )
40    }
41
42    fn homogeneous(self, point: Vec4) -> Result<Vec4, String> {
43        Ok(Vec4::from_point(self.point(point.point()?), point.w))
44    }
45}
46
47pub(crate) fn transform_curve(
48    curve: &NurbsCurve,
49    transform: AffineTransform,
50) -> Result<NurbsCurve, String> {
51    NurbsCurve::new(
52        curve.degree,
53        curve.knots.clone(),
54        curve
55            .control_points
56            .iter()
57            .map(|point| transform.homogeneous(*point))
58            .collect::<Result<_, _>>()?,
59    )
60}
61
62pub(crate) fn transform_surface(
63    surface: &NurbsSurface,
64    transform: AffineTransform,
65) -> Result<NurbsSurface, String> {
66    NurbsSurface::new(
67        surface.degree_u,
68        surface.degree_v,
69        surface.knots_u.clone(),
70        surface.knots_v.clone(),
71        surface
72            .control_points
73            .iter()
74            .map(|row| {
75                row.iter()
76                    .map(|point| transform.homogeneous(*point))
77                    .collect::<Result<Vec<_>, _>>()
78            })
79            .collect::<Result<_, _>>()?,
80    )
81}
82
83/// Transform exact BREP geometry without changing entity IDs or parameter
84/// ranges. `reverse_orientation` is required for negative-determinant maps.
85pub fn transform_brep(
86    solid: &BrepSolid,
87    transform: AffineTransform,
88    reverse_orientation: bool,
89) -> Result<BrepSolid, String> {
90    let determinant = transform.determinant3();
91    if determinant < 0.0 && !reverse_orientation {
92        return Err("transformSolid: reflection requires orientation reversal".into());
93    }
94    if determinant > 0.0 && reverse_orientation {
95        return Err("transformSolid: orientation reversal requires a reflection".into());
96    }
97    let mut result = solid.clone();
98    for vertex in &mut result.vertices {
99        vertex.point = transform.point(vertex.point);
100    }
101    for edge in &mut result.edges {
102        edge.curve = transform_curve(&edge.curve, transform)?;
103    }
104    for shell in &mut result.shells {
105        for face in &mut shell.faces {
106            face.surface = transform_surface(&face.surface, transform)?;
107            if reverse_orientation {
108                face.same_sense = !face.same_sense;
109                for loop_record in &mut face.loops {
110                    loop_record.coedges.reverse();
111                    for coedge in &mut loop_record.coedges {
112                        coedge.forward = !coedge.forward;
113                        coedge.pcurve = coedge.pcurve.reversed()?;
114                    }
115                }
116            }
117        }
118    }
119    let issues = result.validate();
120    if issues.is_empty() {
121        Ok(result)
122    } else {
123        Err(format!("transformed BREP is invalid: {issues:?}"))
124    }
125}
126
127/// Reflect an exact BREP across the plane through `plane_point` with unit
128/// normal `n = plane_normal.normalized()`. The reflection is the affine map
129/// `p' = R p + t` with linear part `R = I - 2 n nᵀ` and translation
130/// `t = 2 (plane_point·n) n`, so points on the plane map to themselves. A
131/// reflection has negative determinant (it flips handedness), so faces are
132/// re-oriented via the existing transform with `reverse_orientation = true`,
133/// keeping outward normals.
134pub fn mirror_brep(
135    solid: &BrepSolid,
136    plane_point: Vec3,
137    plane_normal: Vec3,
138) -> Result<BrepSolid, String> {
139    let n = plane_normal.normalized()?;
140    let (nx, ny, nz) = (n.x, n.y, n.z);
141    let d = plane_point.dot(n);
142    let (tx, ty, tz) = (2.0 * d * nx, 2.0 * d * ny, 2.0 * d * nz);
143    let matrix = [
144        1.0 - 2.0 * nx * nx,
145        -2.0 * nx * ny,
146        -2.0 * nx * nz,
147        tx,
148        -2.0 * ny * nx,
149        1.0 - 2.0 * ny * ny,
150        -2.0 * ny * nz,
151        ty,
152        -2.0 * nz * nx,
153        -2.0 * nz * ny,
154        1.0 - 2.0 * nz * nz,
155        tz,
156        0.0,
157        0.0,
158        0.0,
159        1.0,
160    ];
161    let transform = AffineTransform::new(matrix)?;
162    transform_brep(solid, transform, true)
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::make_box_brep;
169    use crate::mass_properties::solid_mass_properties;
170
171    #[test]
172    fn affine_transform_preserves_exact_box_topology() {
173        let box_solid = make_box_brep(Vec3::default(), 2.0, 3.0, 4.0).unwrap();
174        let transform = AffineTransform::new([
175            2.0, 0.2, 0.0, 5.0, 0.0, 3.0, 0.0, -2.0, 0.0, 0.0, 0.5, 7.0, 0.0, 0.0, 0.0, 1.0,
176        ])
177        .unwrap();
178        let result = transform_brep(&box_solid, transform, false).unwrap();
179        assert!(result.validate().is_empty());
180        assert_eq!(result.vertices.len(), 8);
181        assert_eq!(result.edges.len(), 12);
182        assert_eq!(result.shells[0].faces.len(), 6);
183        assert!(
184            result.vertices[0]
185                .point
186                .sub(Vec3::new(5.0, -2.0, 7.0))
187                .length()
188                < 1e-12
189        );
190    }
191
192    #[test]
193    fn reflected_box_reverses_face_and_loop_orientation() {
194        let box_solid = make_box_brep(Vec3::default(), 2.0, 3.0, 4.0).unwrap();
195        let transform = AffineTransform::new([
196            -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
197        ])
198        .unwrap();
199        let result = transform_brep(&box_solid, transform, true).unwrap();
200        assert!(result.validate().is_empty());
201    }
202
203    fn aabb(solid: &BrepSolid) -> (Vec3, Vec3) {
204        let mut min = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
205        let mut max = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
206        for vertex in &solid.vertices {
207            min = Vec3::new(
208                min.x.min(vertex.point.x),
209                min.y.min(vertex.point.y),
210                min.z.min(vertex.point.z),
211            );
212            max = Vec3::new(
213                max.x.max(vertex.point.x),
214                max.y.max(vertex.point.y),
215                max.z.max(vertex.point.z),
216            );
217        }
218        (min, max)
219    }
220
221    #[test]
222    fn mirror_brep_reflects_a_box() {
223        let box_solid = make_box_brep(Vec3::default(), 10.0, 10.0, 10.0).unwrap();
224        let (orig_min, orig_max) = aabb(&box_solid);
225        let original = solid_mass_properties(&box_solid).unwrap();
226
227        let mirrored = mirror_brep(&box_solid, Vec3::default(), Vec3::new(1.0, 0.0, 0.0)).unwrap();
228
229        // (1) valid solid.
230        assert!(mirrored.validate().is_empty());
231        assert_eq!(mirrored.vertices.len(), 8);
232        assert_eq!(mirrored.edges.len(), 12);
233        assert_eq!(mirrored.shells[0].faces.len(), 6);
234
235        // Volume unchanged within 1e-6 * V, and outward-oriented (positive).
236        let reflected = solid_mass_properties(&mirrored).unwrap();
237        assert!(reflected.volume > 0.0);
238        assert!((reflected.volume - original.volume).abs() <= 1e-6 * original.volume);
239
240        // AABB is the box reflected across x = 0: min.x/max.x negated & swapped,
241        // y and z ranges unchanged.
242        let (min, max) = aabb(&mirrored);
243        assert!((min.x - (-orig_max.x)).abs() < 1e-9);
244        assert!((max.x - (-orig_min.x)).abs() < 1e-9);
245        assert!((min.y - orig_min.y).abs() < 1e-9);
246        assert!((max.y - orig_max.y).abs() < 1e-9);
247        assert!((min.z - orig_min.z).abs() < 1e-9);
248        assert!((max.z - orig_max.z).abs() < 1e-9);
249    }
250
251    #[test]
252    fn mirror_brep_stays_valid_and_outward_oriented() {
253        let box_solid = make_box_brep(Vec3::new(1.0, 2.0, 3.0), 4.0, 5.0, 6.0).unwrap();
254        let mirrored = mirror_brep(
255            &box_solid,
256            Vec3::new(0.0, 0.0, 0.0),
257            Vec3::new(0.0, 1.0, 0.0),
258        )
259        .unwrap();
260        assert!(mirrored.validate().is_empty());
261        assert!(solid_mass_properties(&mirrored).unwrap().volume > 0.0);
262    }
263}