Skip to main content

rscad_csg_bsp/
csg.rs

1use nalgebra::{Affine3, Vector3};
2
3use crate::*;
4use core::iter::IntoIterator;
5
6#[derive(Clone)]
7pub struct Csg<T: Number> {
8    polygons: Vec<Polygon<T, 3>>,
9    /// Embedded metadata factory. Polygons reference it via `face_key`.
10    factory: CsgFactory<T>,
11}
12
13impl<T: Number> Csg<T> {
14    pub fn new(polygons: Vec<Polygon<T, 3>>) -> Self {
15        Self {
16            polygons,
17            factory: CsgFactory::new(),
18        }
19    }
20
21    pub fn from_polygons(polygons: impl IntoIterator<Item = Polygon<T, 3>>) -> Self {
22        Self {
23            polygons: polygons.into_iter().collect(),
24            factory: CsgFactory::new(),
25        }
26    }
27
28    pub fn factory(&self) -> &CsgFactory<T> {
29        &self.factory
30    }
31
32    pub fn factory_mut(&mut self) -> &mut CsgFactory<T> {
33        &mut self.factory
34    }
35
36    /// Replace the embedded factory (e.g. after building via an external [`CsgFactory`]).
37    pub fn with_factory(mut self, factory: CsgFactory<T>) -> Self {
38        self.factory = factory;
39        self
40    }
41
42    pub fn union(mut self, other: Csg<T>) -> Self {
43        let remap = self.factory.absorb(&other.factory);
44        let other_polygons = remap_polygons(other.polygons, &remap);
45        let mut a = BspNode::build(self.polygons);
46        let mut b = BspNode::build(other_polygons);
47        a.clip_to(&b);
48        b.clip_to(&a);
49        b.invert();
50        b.clip_to(&a);
51        b.invert();
52        a.build_into(b.polygons());
53        Self {
54            polygons: a.polygons(),
55            factory: self.factory,
56        }
57    }
58
59    pub fn intersection(mut self, other: Csg<T>) -> Self {
60        let remap = self.factory.absorb(&other.factory);
61        let other_polygons = remap_polygons(other.polygons, &remap);
62        let mut a = BspNode::build(self.polygons);
63        let mut b = BspNode::build(other_polygons);
64        a.invert();
65        b.clip_to(&a);
66        b.invert();
67        a.clip_to(&b);
68        b.clip_to(&a);
69        a.build_into(b.polygons());
70        a.invert();
71        Self {
72            polygons: a.polygons(),
73            factory: self.factory,
74        }
75    }
76
77    pub fn polygons(&self) -> &[Polygon<T, 3>] {
78        self.polygons.as_slice()
79    }
80
81    /// Run a cleanup pass: remove degenerate polygons, collapse collinear
82    /// vertices, and merge coplanar adjacent polygons. Call once before
83    /// mesh generation rather than after every CSG operation.
84    pub fn cleanup(mut self) -> Self {
85        self.polygons = crate::cleanup::cleanup_polygons(self.polygons);
86        self
87    }
88
89    /// Apply a function to every polygon in place.
90    pub fn mapv_inplace<F>(&mut self, f: F)
91    where
92        F: Fn(Polygon<T, 3>) -> Polygon<T, 3>,
93    {
94        let polys = core::mem::take(&mut self.polygons);
95        self.polygons = polys.into_iter().map(f).collect();
96    }
97
98    /// Apply a function to every polygon in place, in parallel.
99    #[cfg(feature = "rayon")]
100    pub fn par_mapv_inplace<F>(&mut self, f: F)
101    where
102        F: Fn(Polygon<T, 3>) -> Polygon<T, 3> + Send + Sync,
103    {
104        use rayon::prelude::*;
105        let polys = core::mem::take(&mut self.polygons);
106        self.polygons = polys.into_par_iter().map(f).collect();
107    }
108
109    /// Apply a function to every vertex of every polygon in place.
110    pub fn mapv_inplace_points<F>(&mut self, f: F)
111    where
112        F: Fn(nalgebra::Point3<T>) -> nalgebra::Point3<T>,
113    {
114        self.mapv_inplace(|p| p.map_vertices(&f));
115    }
116
117    /// Apply a function to every vertex of every polygon in place, in parallel.
118    #[cfg(feature = "rayon")]
119    pub fn par_mapv_inplace_points<F>(&mut self, f: F)
120    where
121        F: Fn(nalgebra::Point3<T>) -> nalgebra::Point3<T> + Send + Sync,
122    {
123        self.par_mapv_inplace(|p| p.map_vertices(&f));
124    }
125
126    /// Translate all polygons by an offset.
127    pub fn translate(mut self, offset: nalgebra::Vector3<T>) -> Self {
128        self.mapv_inplace(|p| p.translate(offset));
129        self
130    }
131
132    /// Scale all polygon vertices by per-axis factors.
133    ///
134    /// Planes are recomputed from the scaled vertices: a non-uniform scale
135    /// changes plane normals (inverse-transpose transform), so the cached
136    /// plane can't just be carried over.
137    pub fn scale(mut self, factors: nalgebra::Vector3<T>) -> Self {
138        self.mapv_inplace(|mut p| {
139            p.vertices
140                .mapv_inplace(|v| v.coords.component_mul(&factors).into());
141            p.recompute_plane();
142            p
143        });
144        self
145    }
146
147    /// Rotate all polygons by Euler angles (degrees), applied as Z × Y × X.
148    pub fn rotate_euler(mut self, angles_deg: nalgebra::Vector3<T>) -> Self {
149        let rotation = crate::euler_rotation_matrix(angles_deg);
150        self.mapv_inplace(|p| p.rotate(rotation));
151        self
152    }
153
154    /// Mirror across axes where the corresponding component is non-zero.
155    ///
156    /// Reverses polygon winding to maintain correct normals.
157    pub fn mirror(mut self, axes: nalgebra::Vector3<T>) -> Self {
158        let signs = axes.map(|a| if a != T::zero() { -T::one() } else { T::one() });
159        self.mapv_inplace(|mut p| {
160            p.vertices
161                .mapv_inplace(|v| v.coords.component_mul(&signs).into());
162            p.vertices.invert_axis(ndarray::Axis(0));
163            let normal = p.plane.normal().component_mul(&signs);
164            let d = normal.dot(&p.vertices[0].coords);
165            p.plane = Plane::new(normal, d);
166            p
167        });
168        self
169    }
170
171    /// Symmetric difference: `(a - b) ∪ (b - a)`.
172    pub fn xor(self, other: Csg<T>) -> Self {
173        let a2 = self.clone();
174        let b2 = other.clone();
175        self.difference(b2).union(other.difference(a2))
176    }
177
178    /// Different (a - (a ^ b))
179    pub fn difference(mut self, other: Csg<T>) -> Self {
180        let remap = self.factory.absorb(&other.factory);
181        let other_polygons = remap_polygons(other.polygons, &remap);
182        let mut a = BspNode::build(self.polygons);
183        let mut b = BspNode::build(other_polygons);
184        a.invert();
185        a.clip_to(&b);
186        b.clip_to(&a);
187        b.invert();
188        b.clip_to(&a);
189        b.invert();
190        a.build_into(b.polygons());
191        a.invert();
192        Self {
193            polygons: a.polygons(),
194            factory: self.factory,
195        }
196    }
197
198    /// Extrudes a planar shape along a vector.
199    ///
200    /// The extrusion is subdivided into `slices` levels. At each level the
201    /// rotation angle and scale factor are linearly interpolated from identity
202    /// (at the bottom) to the full `transform` (at the top).
203    ///
204    /// The `transform` is decomposed into a rotation (axis-angle) and uniform
205    /// scale. Non-uniform scale is not supported.
206    ///
207    /// Side walls are triangulated to guarantee planarity when twist is used.
208    pub fn extrude(
209        self,
210        along: Vector3<T>,
211        transform: Affine3<T>,
212        slices: usize,
213    ) -> Result<Self, Error> {
214        use nalgebra::Rotation3;
215        use num_traits::Float;
216
217        if !self.coplanar() {
218            return Err(Error::NonPlanarExtrude);
219        }
220
221        let slices = slices.max(1);
222        let slices_t = T::from(slices).expect("BUG: slices must convert to T");
223
224        // Decompose transform into rotation (axis-angle) + uniform scale.
225        let m = transform.matrix().fixed_view::<3, 3>(0, 0).into_owned();
226        let scale_factor = Float::cbrt(m.determinant());
227        let rot_matrix = m / scale_factor;
228        let rotation = Rotation3::from_matrix_unchecked(rot_matrix);
229        let axis_angle = rotation.scaled_axis();
230
231        let vertex_at = |v: nalgebra::Point3<T>, t: T| {
232            let s = T::one() + (scale_factor - T::one()) * t;
233            let rot = Rotation3::from_scaled_axis(axis_angle * t);
234            let offset = along * t;
235            nalgebra::Point3::from((rot * (v.coords * s)) + offset)
236        };
237
238        use itertools::Itertools;
239
240        let polygons: Vec<Polygon<T, 3>> =
241            self.polygons
242                .iter()
243                .flat_map(|base_poly| {
244                    let aligned = base_poly.plane().normal().dot(&along) > T::zero();
245
246                    let base_verts: Vec<nalgebra::Point3<T>> =
247                        base_poly.vertices().iter().copied().collect();
248
249                    let levels: Vec<Vec<nalgebra::Point3<T>>> = (0..=slices)
250                        .map(|i| {
251                            let t = T::from(i).expect("BUG: slice index") / slices_t;
252                            base_verts.iter().map(|&v| vertex_at(v, t)).collect()
253                        })
254                        .collect();
255
256                    let sides = levels
257                        .iter()
258                        .map(|level| {
259                            level
260                                .iter()
261                                .copied()
262                                .circular_tuple_windows::<(_, _)>()
263                                .collect::<Vec<_>>()
264                        })
265                        .tuple_windows()
266                        .flat_map(move |(bot_edges, top_edges)| {
267                            bot_edges.into_iter().zip(top_edges).flat_map(
268                                move |((a, b), (d, c))| {
269                                    if aligned {
270                                        [
271                                            Polygon::from_points([a, b, c]).ok(),
272                                            Polygon::from_points([a, c, d]).ok(),
273                                        ]
274                                    } else {
275                                        [
276                                            Polygon::from_points([a, c, b]).ok(),
277                                            Polygon::from_points([a, d, c]).ok(),
278                                        ]
279                                    }
280                                    .into_iter()
281                                    .flatten()
282                                },
283                            )
284                        });
285
286                    let first_level = levels.first().expect("BUG: levels is non-empty");
287                    let last_level = levels.last().expect("BUG: levels is non-empty");
288                    let (bottom_cap, top_cap) = if aligned {
289                        (
290                            Polygon::from_points(first_level.iter().rev().copied()),
291                            Polygon::from_points(last_level.iter().copied()),
292                        )
293                    } else {
294                        (
295                            Polygon::from_points(first_level.iter().copied()),
296                            Polygon::from_points(last_level.iter().rev().copied()),
297                        )
298                    };
299
300                    sides.chain(bottom_cap).chain(top_cap).collect::<Vec<_>>()
301                })
302                .collect();
303
304        Ok(Csg::from_polygons(polygons))
305    }
306
307    /// Revolves a planar XZ-plane profile about the +Y axis (lathe).
308    ///
309    /// Profile vertices `(x, 0, z)` sweep to `(x·cosθ, z, x·sinθ)`; profile-x
310    /// is the radius (x ≥ 0 expected), profile-z the height along the axis.
311    /// Full revolutions weld the seam; partial sweeps are closed with start
312    /// and end caps. Side walls are triangulated (swept quads are non-planar),
313    /// and triangles degenerated by on-axis vertices are dropped.
314    pub fn lathe(self, angle_degrees: T, segments: usize) -> Result<Self, Error> {
315        use itertools::Itertools;
316        use num_traits::Float;
317
318        if !self.coplanar() {
319            return Err(Error::NonPlanarExtrude);
320        }
321
322        let segments = segments.max(3);
323        let full_turn = T::from(360.0).expect("BUG: const");
324        let full_circle = angle_degrees >= full_turn;
325        let sweep = if full_circle {
326            full_turn
327        } else {
328            angle_degrees
329        };
330        let step =
331            Float::to_radians(sweep) / T::from(segments).expect("BUG: segments must convert to T");
332
333        let vertex_at = |v: nalgebra::Point3<T>, i: usize| {
334            let theta = step * T::from(i).expect("BUG: segment index");
335            nalgebra::Point3::new(v.x * Float::cos(theta), v.z, v.x * Float::sin(theta))
336        };
337
338        // Full circles reuse level 0 as the final level so the seam welds exactly.
339        let level_count = if full_circle { segments } else { segments + 1 };
340
341        let polygons: Vec<Polygon<T, 3>> = self
342            .polygons
343            .iter()
344            .flat_map(|base_poly| {
345                // Profile polygons wind with normal −Y in the standard case
346                // (see `rect`); a +Y normal flips every winding below.
347                let aligned = base_poly.plane().normal().y < T::zero();
348
349                let base_verts: Vec<nalgebra::Point3<T>> =
350                    base_poly.vertices().iter().copied().collect();
351
352                let levels: Vec<Vec<nalgebra::Point3<T>>> = (0..level_count)
353                    .map(|i| base_verts.iter().map(|&v| vertex_at(v, i)).collect())
354                    .collect();
355
356                let edges_of = |level: &Vec<nalgebra::Point3<T>>| {
357                    level
358                        .iter()
359                        .copied()
360                        .circular_tuple_windows::<(_, _)>()
361                        .collect::<Vec<_>>()
362                };
363
364                let sides =
365                    (0..segments)
366                        .map(|i| (i, (i + 1) % level_count))
367                        .flat_map(|(bot, top)| {
368                            edges_of(&levels[bot])
369                                .into_iter()
370                                .zip(edges_of(&levels[top]))
371                                .flat_map(move |((a, b), (d, c))| {
372                                    if aligned {
373                                        [
374                                            Polygon::from_points([a, b, c]).ok(),
375                                            Polygon::from_points([a, c, d]).ok(),
376                                        ]
377                                    } else {
378                                        [
379                                            Polygon::from_points([a, c, b]).ok(),
380                                            Polygon::from_points([a, d, c]).ok(),
381                                        ]
382                                    }
383                                    .into_iter()
384                                    .flatten()
385                                })
386                                .collect::<Vec<_>>()
387                        });
388
389                let caps = if full_circle {
390                    [None, None]
391                } else {
392                    let first_level = levels.first().expect("BUG: levels is non-empty");
393                    let last_level = levels.last().expect("BUG: levels is non-empty");
394                    if aligned {
395                        [
396                            Polygon::from_points(first_level.iter().rev().copied()).ok(),
397                            Polygon::from_points(last_level.iter().copied()).ok(),
398                        ]
399                    } else {
400                        [
401                            Polygon::from_points(first_level.iter().copied()).ok(),
402                            Polygon::from_points(last_level.iter().rev().copied()).ok(),
403                        ]
404                    }
405                };
406
407                sides.chain(caps.into_iter().flatten()).collect::<Vec<_>>()
408            })
409            .collect();
410
411        Ok(Csg::from_polygons(polygons))
412    }
413
414    pub fn coplanar(&self) -> bool {
415        use itertools::Itertools;
416        self.polygons()
417            .iter()
418            .tuple_windows()
419            .all(|(last, next)| last.plane().roughly_eq(&next.plane()))
420    }
421
422    /// Cast the CSG solid's numeric type from `T` to `T2`, e.g. `f64` → `f32`.
423    ///
424    /// Face keys are preserved unchanged; only vertex coordinates and plane
425    /// equations are converted via [`num_traits::cast::AsPrimitive`].
426    pub fn cast<T2: Number>(self) -> Csg<T2>
427    where
428        T: num_traits::cast::AsPrimitive<T2>,
429    {
430        let (new_factory, face_remap) = self.factory.cast::<T2>();
431        let polygons = self
432            .polygons
433            .into_iter()
434            .map(|p| {
435                let mut p2 = p.cast::<T2>();
436                if let Some(fk) = p2.face_key()
437                    && let Some(&new_fk) = face_remap.0.get(&fk)
438                {
439                    p2.set_face_key(Some(new_fk));
440                }
441                p2
442            })
443            .collect();
444        Csg {
445            polygons,
446            factory: new_factory,
447        }
448    }
449}
450
451/// Update polygon `face_key`s using a remap from a factory merge.
452fn remap_polygons<T: Number>(
453    mut polygons: Vec<Polygon<T, 3>>,
454    remap: &FaceKeyRemap,
455) -> Vec<Polygon<T, 3>> {
456    if remap.0.is_empty() {
457        return polygons;
458    }
459    for p in &mut polygons {
460        if let Some(fk) = p.face_key()
461            && let Some(&new_fk) = remap.0.get(&fk)
462        {
463            p.set_face_key(Some(new_fk));
464        }
465    }
466    polygons
467}
468
469#[cfg(feature = "bevy")]
470impl Csg<f32> {
471    /// Look up the color for a polygon via face_key → FaceMeta → PrimitiveMeta → color.
472    fn polygon_color(&self, polygon: &Polygon<f32, 3>) -> Option<[f32; 4]> {
473        let fk = polygon.face_key()?;
474        let fm = self.factory.face(fk)?;
475        let pm = self.factory.primitive(fm.primitive_key)?;
476        pm.color
477    }
478
479    pub fn to_bevy_mesh(&self) -> bevy_mesh::Mesh {
480        use bevy_asset::RenderAssetUsages;
481        use bevy_mesh::{Indices, Mesh};
482        use wgpu::PrimitiveTopology;
483
484        let has_colors = self
485            .polygons
486            .iter()
487            .any(|p| self.polygon_color(p).is_some());
488        let color_fn = |p: &Polygon<f32, 3>| self.polygon_color(p);
489
490        let (vertices, normals, indices, colors) =
491            build_mesh_buffers(&self.polygons, has_colors, &color_fn);
492
493        let mut mesh = Mesh::new(
494            PrimitiveTopology::TriangleList,
495            RenderAssetUsages::default(),
496        );
497        mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertices);
498        mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
499        if let Some(colors) = colors {
500            mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
501        }
502        mesh.insert_indices(Indices::U32(indices));
503        mesh
504    }
505}
506
507#[cfg(all(feature = "bevy", feature = "rayon"))]
508#[allow(clippy::type_complexity)]
509fn build_mesh_buffers(
510    polygons: &[Polygon<f32, 3>],
511    emit_colors: bool,
512    color_fn: &dyn Fn(&Polygon<f32, 3>) -> Option<[f32; 4]>,
513) -> (
514    Vec<[f32; 3]>,
515    Vec<[f32; 3]>,
516    Vec<u32>,
517    Option<Vec<[f32; 4]>>,
518) {
519    use rayon::prelude::*;
520
521    #[allow(clippy::type_complexity)]
522    let chunks: Vec<(Vec<[f32; 3]>, [f32; 3], Vec<u32>, Option<[f32; 4]>)> = polygons
523        .par_iter()
524        .map(|polygon| {
525            let normal: [f32; 3] = bytemuck::cast(polygon.plane().normal());
526            let verts: Vec<[f32; 3]> = polygon
527                .vertices()
528                .iter()
529                .map(|v| bytemuck::cast(*v))
530                .collect();
531            let tris: Vec<u32> = if polygon.vertices().len() == 3 {
532                vec![0, 1, 2]
533            } else {
534                polygon
535                    .tessellate_earcut::<u32>()
536                    .triangles()
537                    .iter()
538                    .copied()
539                    .collect()
540            };
541            // color_fn can't be called from par_iter (not Sync), so we use face_key as a proxy
542            // and look up in the sequential pass below
543            (verts, normal, tris, None)
544        })
545        .collect();
546
547    let mut offset = 0u32;
548    let offsets: Vec<u32> = chunks
549        .iter()
550        .map(|(verts, _, _, _)| {
551            let o = offset;
552            offset += verts.len() as u32;
553            o
554        })
555        .collect();
556
557    let total_verts: usize = chunks.iter().map(|(v, _, _, _)| v.len()).sum();
558    let total_tris: usize = chunks.iter().map(|(_, _, t, _)| t.len()).sum();
559
560    let mut vertices = Vec::with_capacity(total_verts);
561    let mut normals = Vec::with_capacity(total_verts);
562    let mut indices = Vec::with_capacity(total_tris);
563    let mut colors = if emit_colors {
564        Some(Vec::with_capacity(total_verts))
565    } else {
566        None
567    };
568
569    for (((verts, normal, tris, _), offset), polygon) in
570        chunks.into_iter().zip(offsets).zip(polygons)
571    {
572        let n = verts.len();
573        normals.extend(std::iter::repeat_n(normal, n));
574        vertices.extend(verts);
575        indices.extend(tris.into_iter().map(|i| i + offset));
576        if let Some(ref mut color_buf) = colors {
577            let c = color_fn(polygon).unwrap_or([1.0, 1.0, 1.0, 1.0]);
578            color_buf.extend(std::iter::repeat_n(c, n));
579        }
580    }
581
582    (vertices, normals, indices, colors)
583}
584
585#[cfg(all(feature = "bevy", not(feature = "rayon")))]
586#[allow(clippy::type_complexity)]
587fn build_mesh_buffers(
588    polygons: &[Polygon<f32, 3>],
589    emit_colors: bool,
590    color_fn: &dyn Fn(&Polygon<f32, 3>) -> Option<[f32; 4]>,
591) -> (
592    Vec<[f32; 3]>,
593    Vec<[f32; 3]>,
594    Vec<u32>,
595    Option<Vec<[f32; 4]>>,
596) {
597    let mut vertices: Vec<[f32; 3]> = Vec::new();
598    let mut normals: Vec<[f32; 3]> = Vec::new();
599    let mut indices: Vec<u32> = Vec::new();
600    let mut colors: Option<Vec<[f32; 4]>> = if emit_colors { Some(Vec::new()) } else { None };
601    let mut vertex_offset: u32 = 0;
602
603    for polygon in polygons {
604        let normal: [f32; 3] = bytemuck::cast(polygon.plane().normal());
605        let n_verts = polygon.vertices().len();
606
607        for v in polygon.vertices().iter() {
608            vertices.push(bytemuck::cast(*v));
609            normals.push(normal);
610        }
611        if let Some(ref mut color_buf) = colors {
612            let c = color_fn(polygon).unwrap_or([1.0, 1.0, 1.0, 1.0]);
613            color_buf.extend(std::iter::repeat_n(c, n_verts));
614        }
615        if n_verts == 3 {
616            indices.extend_from_slice(&[vertex_offset, vertex_offset + 1, vertex_offset + 2]);
617        } else {
618            let tessellated = polygon.tessellate_earcut::<u32>();
619            for idx in tessellated.triangles().iter() {
620                indices.push(*idx + vertex_offset);
621            }
622        }
623        vertex_offset += n_verts as u32;
624    }
625
626    (vertices, normals, indices, colors)
627}
628
629#[derive(thiserror::Error, Debug)]
630pub enum Error {
631    #[error("The shape you tried to extrude is non planar")]
632    NonPlanarExtrude,
633}
634
635#[cfg(test)]
636mod tests {
637    /// Non-uniform scale changes plane normals (inverse-transpose transform).
638    /// A stale cached plane leaves vertices off their own plane, which makes
639    /// BSP classification inconsistent and cascades into unbounded polygon
640    /// splitting (stack overflow on export).
641    #[test]
642    fn non_uniform_scale_keeps_polygon_planes_consistent() {
643        let sphere = crate::CsgFactory::<f64>::new().sphere(0.5, 32, 32);
644        let scaled = sphere.scale(nalgebra::Vector3::new(3.6, 3.98, 1.0));
645        for polygon in scaled.polygons() {
646            let plane = polygon.plane();
647            for v in polygon.vertices().iter() {
648                assert!(
649                    plane.has_point(v),
650                    "vertex {v:?} off its polygon plane {plane:?}"
651                );
652            }
653        }
654    }
655}