Skip to main content

rscad_csg_bsp/
factory.rs

1use nalgebra::{Point, Point3, Vector3};
2use slotmap::SlotMap;
3
4use crate::metadata::*;
5use crate::*;
6
7/// Maps old `FaceKey`s to new `FaceKey`s after a factory merge or cast.
8#[derive(Clone, Debug, Default)]
9pub struct FaceKeyRemap(pub std::collections::HashMap<FaceKey, FaceKey>);
10
11/// A factory that creates CSG primitives while tracking face and edge metadata.
12///
13/// All metadata is owned by the factory. Polygons and CSGs store only
14/// [`FaceKey`] references that can be looked up here.
15///
16/// # Example
17///
18/// ```
19/// use rscad_csg_bsp::CsgFactory;
20///
21/// let mut factory = CsgFactory::<f64>::new();
22/// let csg = factory.cuboid(2.0, 1.0, 3.0);
23///
24/// // Pick a polygon and find all siblings sharing the same face.
25/// let face_key = csg.polygons()[0].face_key().unwrap();
26/// let siblings = factory.polygons_sharing_face(&csg, face_key);
27/// ```
28#[derive(Clone, Debug)]
29pub struct CsgFactory<T: Number> {
30    pub primitives: SlotMap<PrimitiveKey, PrimitiveMeta>,
31    pub faces: SlotMap<FaceKey, FaceMeta<T>>,
32    pub edges: SlotMap<EdgeKey, EdgeMeta<T>>,
33}
34
35impl<T: Number> Default for CsgFactory<T> {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl<T: Number> CsgFactory<T> {
42    pub fn new() -> Self {
43        Self {
44            primitives: SlotMap::with_key(),
45            faces: SlotMap::with_key(),
46            edges: SlotMap::with_key(),
47        }
48    }
49
50    // ── Merge ──────────────────────────────────────────────────
51
52    /// Absorb all entries from `other` into `self`, remapping keys.
53    ///
54    /// Returns a [`FaceKeyRemap`] so callers can update polygon `face_key`s.
55    pub fn absorb(&mut self, other: &Self) -> FaceKeyRemap {
56        let mut prim_remap: std::collections::HashMap<PrimitiveKey, PrimitiveKey> =
57            std::collections::HashMap::with_capacity(other.primitives.len());
58        let mut face_remap =
59            FaceKeyRemap(std::collections::HashMap::with_capacity(other.faces.len()));
60        let mut edge_remap: std::collections::HashMap<EdgeKey, EdgeKey> =
61            std::collections::HashMap::with_capacity(other.edges.len());
62
63        // 1. Primitives
64        for (old_pk, meta) in &other.primitives {
65            let new_pk = self.primitives.insert(meta.clone());
66            prim_remap.insert(old_pk, new_pk);
67        }
68
69        // 2. Faces (update primitive_key)
70        for (old_fk, meta) in &other.faces {
71            let mut meta = meta.clone();
72            if let Some(&new_pk) = prim_remap.get(&meta.primitive_key) {
73                meta.primitive_key = new_pk;
74            }
75            let new_fk = self.faces.insert(meta);
76            face_remap.0.insert(old_fk, new_fk);
77        }
78
79        // 3. Edges (update primitive_key + face_keys)
80        for (old_ek, meta) in &other.edges {
81            let mut meta = meta.clone();
82            if let Some(&new_pk) = prim_remap.get(&meta.primitive_key) {
83                meta.primitive_key = new_pk;
84            }
85            meta.face_keys = meta
86                .face_keys
87                .map(|fk| face_remap.0.get(&fk).copied().unwrap_or(fk));
88            let new_ek = self.edges.insert(meta);
89            edge_remap.insert(old_ek, new_ek);
90        }
91
92        // 4. Fix up face_keys/edge_keys inside the newly inserted PrimitiveMetas
93        for new_pk in prim_remap.values() {
94            if let Some(pm) = self.primitives.get_mut(*new_pk) {
95                pm.face_keys = pm
96                    .face_keys
97                    .iter()
98                    .map(|fk| face_remap.0.get(fk).copied().unwrap_or(*fk))
99                    .collect();
100                pm.edge_keys = pm
101                    .edge_keys
102                    .iter()
103                    .map(|ek| edge_remap.get(ek).copied().unwrap_or(*ek))
104                    .collect();
105            }
106        }
107
108        face_remap
109    }
110
111    /// Cast the factory's numeric type from `T` to `T2`.
112    ///
113    /// Returns the new factory and a face-key remap (needed to update polygons).
114    pub fn cast<T2: Number>(self) -> (CsgFactory<T2>, FaceKeyRemap)
115    where
116        T: num_traits::cast::AsPrimitive<T2>,
117    {
118        // PrimitiveMeta has no T — copy as-is (keys stay the same inside the same SlotMap)
119        let primitives = self.primitives;
120
121        let mut face_remap = FaceKeyRemap::default();
122        let mut faces: SlotMap<FaceKey, FaceMeta<T2>> = SlotMap::with_key();
123        for (old_fk, meta) in self.faces {
124            let new_fk = faces.insert(FaceMeta {
125                primitive_key: meta.primitive_key,
126                normal: meta.normal.map(|x| x.as_()),
127                label: meta.label,
128            });
129            face_remap.0.insert(old_fk, new_fk);
130        }
131
132        let mut edges: SlotMap<EdgeKey, EdgeMeta<T2>> = SlotMap::with_key();
133        for (_, meta) in self.edges {
134            edges.insert(EdgeMeta {
135                primitive_key: meta.primitive_key,
136                vertices: (
137                    meta.vertices.0.map(|x| x.as_()),
138                    meta.vertices.1.map(|x| x.as_()),
139                ),
140                face_keys: meta
141                    .face_keys
142                    .map(|fk| face_remap.0.get(&fk).copied().unwrap_or(fk)),
143            });
144        }
145
146        (
147            CsgFactory {
148                primitives,
149                faces,
150                edges,
151            },
152            face_remap,
153        )
154    }
155
156    // ── Queries ────────────────────────────────────────────────
157
158    pub fn face(&self, key: FaceKey) -> Option<&FaceMeta<T>> {
159        self.faces.get(key)
160    }
161
162    pub fn edge(&self, key: EdgeKey) -> Option<&EdgeMeta<T>> {
163        self.edges.get(key)
164    }
165
166    pub fn primitive(&self, key: PrimitiveKey) -> Option<&PrimitiveMeta> {
167        self.primitives.get(key)
168    }
169
170    /// Returns the indices of all polygons in `csg` that share the given face.
171    pub fn polygons_sharing_face(&self, csg: &Csg<T>, face_key: FaceKey) -> Vec<usize> {
172        csg.polygons()
173            .iter()
174            .enumerate()
175            .filter(|(_, p)| p.face_key() == Some(face_key))
176            .map(|(i, _)| i)
177            .collect()
178    }
179
180    // ── Primitive factories ────────────────────────────────────
181
182    /// Create a cube (uniform cuboid) centered at the origin.
183    pub fn cube(&mut self, size: T) -> Csg<T> {
184        self.cuboid(size, size, size)
185    }
186
187    /// Create a cuboid centered at the origin with the given dimensions.
188    pub fn cuboid(&mut self, x: T, y: T, z: T) -> Csg<T> {
189        let two = T::one() + T::one();
190        let hx = x / two;
191        let hy = y / two;
192        let hz = z / two;
193
194        let v = [
195            Point::from([-hx, -hy, -hz]), // 0: left  bottom back
196            Point::from([hx, -hy, -hz]),  // 1: right bottom back
197            Point::from([hx, hy, -hz]),   // 2: right top    back
198            Point::from([-hx, hy, -hz]),  // 3: left  top    back
199            Point::from([-hx, -hy, hz]),  // 4: left  bottom front
200            Point::from([hx, -hy, hz]),   // 5: right bottom front
201            Point::from([hx, hy, hz]),    // 6: right top    front
202            Point::from([-hx, hy, hz]),   // 7: left  top    front
203        ];
204
205        // (vertices, normal, distance, label, edge vertex-index pairs)
206        #[allow(clippy::type_complexity)]
207        let face_defs: [([Point<T, 3>; 4], Vector3<T>, T, &str, [[usize; 2]; 4]); 6] = [
208            (
209                [v[3], v[2], v[1], v[0]],
210                -Vector3::z(),
211                hz,
212                "back",
213                [[3, 2], [2, 1], [1, 0], [0, 3]],
214            ),
215            (
216                [v[4], v[5], v[6], v[7]],
217                Vector3::z(),
218                hz,
219                "front",
220                [[4, 5], [5, 6], [6, 7], [7, 4]],
221            ),
222            (
223                [v[3], v[0], v[4], v[7]],
224                -Vector3::x(),
225                hx,
226                "left",
227                [[3, 0], [0, 4], [4, 7], [7, 3]],
228            ),
229            (
230                [v[1], v[2], v[6], v[5]],
231                Vector3::x(),
232                hx,
233                "right",
234                [[1, 2], [2, 6], [6, 5], [5, 1]],
235            ),
236            (
237                [v[0], v[1], v[5], v[4]],
238                -Vector3::y(),
239                hy,
240                "bottom",
241                [[0, 1], [1, 5], [5, 4], [4, 0]],
242            ),
243            (
244                [v[2], v[3], v[7], v[6]],
245                Vector3::y(),
246                hy,
247                "top",
248                [[2, 3], [3, 7], [7, 6], [6, 2]],
249            ),
250        ];
251
252        // Reserve a primitive key first (we'll fill in face/edge keys after).
253        let prim_key = self.primitives.insert(PrimitiveMeta {
254            kind: PrimitiveKind::Cuboid,
255            face_keys: Vec::new(),
256            edge_keys: Vec::new(),
257            color: None,
258        });
259
260        let mut all_face_keys = Vec::with_capacity(6);
261        let mut all_edge_keys = Vec::new();
262
263        // Build face keys first so edges can reference them.
264        let face_keys_arr: Vec<FaceKey> = face_defs
265            .iter()
266            .map(|(_, normal, _, label, _)| {
267                self.faces.insert(FaceMeta {
268                    primitive_key: prim_key,
269                    normal: *normal,
270                    label: label.to_string(),
271                })
272            })
273            .collect();
274        all_face_keys.extend_from_slice(&face_keys_arr);
275
276        // Build edges — deduplicate by canonical (min, max) vertex index pair.
277        // Each edge belongs to exactly two faces.
278        let mut edge_map: std::collections::BTreeMap<(usize, usize), (EdgeKey, bool)> =
279            std::collections::BTreeMap::new();
280
281        for (face_idx, (_, _, _, _, edge_pairs)) in face_defs.iter().enumerate() {
282            for &[a, b] in edge_pairs {
283                let canonical = if a < b { (a, b) } else { (b, a) };
284                edge_map
285                    .entry(canonical)
286                    .and_modify(|(ek, _)| {
287                        // Second face referencing this edge — update the metadata.
288                        if let Some(em) = self.edges.get_mut(*ek) {
289                            em.face_keys[1] = face_keys_arr[face_idx];
290                        }
291                    })
292                    .or_insert_with(|| {
293                        let ek = self.edges.insert(EdgeMeta {
294                            primitive_key: prim_key,
295                            vertices: (v[a], v[b]),
296                            face_keys: [face_keys_arr[face_idx], face_keys_arr[face_idx]],
297                        });
298                        all_edge_keys.push(ek);
299                        (ek, true)
300                    });
301            }
302        }
303
304        // Update the primitive with collected keys.
305        if let Some(pm) = self.primitives.get_mut(prim_key) {
306            pm.face_keys = all_face_keys;
307            pm.edge_keys = all_edge_keys;
308        }
309
310        // Build polygons, stamping each with its face key.
311        let polygons = face_defs.iter().zip(face_keys_arr.iter()).flat_map(
312            |((verts, normal, dist, _, _), &fk)| {
313                Polygon::from_points_and_plane(verts.to_vec(), Plane::new(*normal, *dist))
314                    .ok()
315                    .map(|mut p| {
316                        p.set_face_key(Some(fk));
317                        p
318                    })
319            },
320        );
321
322        Csg::from_polygons(polygons)
323    }
324
325    /// Create a cylinder centered at the origin, axis along Y.
326    pub fn cylinder(&mut self, radius: T, height: T, segments: usize) -> Csg<T> {
327        let segments = segments.max(3);
328        let two = T::one() + T::one();
329        let half_h = height / two;
330        let pi = T::PI();
331        let two_pi = pi + pi;
332        let seg_t = T::from(segments).expect("BUG: f64 must represent segment count");
333
334        let circle_point = |j: usize, y: T| -> Point<T, 3> {
335            let angle = two_pi * T::from(j).expect("BUG: f64 must represent loop index") / seg_t;
336            Point::from([
337                radius * num_traits::real::Real::cos(angle),
338                y,
339                radius * num_traits::real::Real::sin(angle),
340            ])
341        };
342
343        let top_pts: Vec<Point<T, 3>> = (0..segments).map(|j| circle_point(j, half_h)).collect();
344        let bot_pts: Vec<Point<T, 3>> = (0..segments).map(|j| circle_point(j, -half_h)).collect();
345
346        let prim_key = self.primitives.insert(PrimitiveMeta {
347            kind: PrimitiveKind::Cylinder,
348            face_keys: Vec::new(),
349            edge_keys: Vec::new(),
350            color: None,
351        });
352
353        // Top cap face
354        let top_fk = self.faces.insert(FaceMeta {
355            primitive_key: prim_key,
356            normal: Vector3::y(),
357            label: "top".to_string(),
358        });
359
360        // Bottom cap face
361        let bot_fk = self.faces.insert(FaceMeta {
362            primitive_key: prim_key,
363            normal: -Vector3::y(),
364            label: "bottom".to_string(),
365        });
366
367        let mut all_face_keys = vec![top_fk, bot_fk];
368        let mut all_edge_keys = Vec::new();
369        let mut polygons = Vec::new();
370
371        // Top cap polygon
372        if let Ok(mut p) =
373            Polygon::from_points_and_plane(top_pts.clone(), Plane::new(Vector3::y(), half_h))
374        {
375            p.set_face_key(Some(top_fk));
376            polygons.push(p);
377        }
378
379        // Bottom cap polygon (reversed winding)
380        let mut bot_cap = bot_pts.clone();
381        bot_cap.reverse();
382        if let Ok(mut p) =
383            Polygon::from_points_and_plane(bot_cap, Plane::new(-Vector3::y(), half_h))
384        {
385            p.set_face_key(Some(bot_fk));
386            polygons.push(p);
387        }
388
389        // Side quads — each is its own face
390        for j in 0..segments {
391            let jn = (j + 1) % segments;
392
393            let mid_angle =
394                (two * T::from(j).expect("BUG: f64 must represent loop index") + T::one()) * pi
395                    / seg_t;
396            let normal = Vector3::new(
397                num_traits::real::Real::cos(mid_angle),
398                T::zero(),
399                num_traits::real::Real::sin(mid_angle),
400            );
401            let dist = radius * num_traits::real::Real::cos(pi / seg_t);
402
403            let side_fk = self.faces.insert(FaceMeta {
404                primitive_key: prim_key,
405                normal,
406                label: format!("side_{j}"),
407            });
408            all_face_keys.push(side_fk);
409
410            // Top edge of this side quad (shared with top cap)
411            let top_ek = self.edges.insert(EdgeMeta {
412                primitive_key: prim_key,
413                vertices: (top_pts[j], top_pts[jn]),
414                face_keys: [side_fk, top_fk],
415            });
416            all_edge_keys.push(top_ek);
417
418            // Bottom edge of this side quad (shared with bottom cap)
419            let bot_ek = self.edges.insert(EdgeMeta {
420                primitive_key: prim_key,
421                vertices: (bot_pts[j], bot_pts[jn]),
422                face_keys: [side_fk, bot_fk],
423            });
424            all_edge_keys.push(bot_ek);
425
426            let verts = [top_pts[j], top_pts[jn], bot_pts[jn], bot_pts[j]];
427            if let Ok(mut p) = Polygon::from_points_and_plane(verts, Plane::new(normal, dist)) {
428                p.set_face_key(Some(side_fk));
429                polygons.push(p);
430            }
431        }
432
433        // Vertical edges between adjacent side faces
434        for j in 0..segments {
435            let jn = (j + 1) % segments;
436            // Find the side face keys for face j and face jn
437            // side faces start at index 2 in all_face_keys
438            let side_fk_j = all_face_keys[2 + j];
439            let side_fk_jn = all_face_keys[2 + jn];
440            let ek = self.edges.insert(EdgeMeta {
441                primitive_key: prim_key,
442                vertices: (top_pts[jn], bot_pts[jn]),
443                face_keys: [side_fk_j, side_fk_jn],
444            });
445            all_edge_keys.push(ek);
446        }
447
448        if let Some(pm) = self.primitives.get_mut(prim_key) {
449            pm.face_keys = all_face_keys;
450            pm.edge_keys = all_edge_keys;
451        }
452
453        Csg::from_polygons(polygons)
454    }
455
456    /// Create a cone centered at the origin, axis along Y.
457    pub fn cone(&mut self, radius: T, height: T, segments: usize) -> Csg<T> {
458        let segments = segments.max(3);
459        let two = T::one() + T::one();
460        let half_h = height / two;
461        let two_pi = two * T::PI();
462        let seg_t = T::from(segments).expect("BUG: f64 must represent segment count");
463
464        let apex = Point::from([T::zero(), half_h, T::zero()]);
465        let base: Vec<Point<T, 3>> = (0..segments)
466            .map(|j| {
467                let angle =
468                    two_pi * T::from(j).expect("BUG: f64 must represent loop index") / seg_t;
469                Point::from([
470                    radius * num_traits::real::Real::cos(angle),
471                    -half_h,
472                    radius * num_traits::real::Real::sin(angle),
473                ])
474            })
475            .collect();
476
477        let prim_key = self.primitives.insert(PrimitiveMeta {
478            kind: PrimitiveKind::Cone,
479            face_keys: Vec::new(),
480            edge_keys: Vec::new(),
481            color: None,
482        });
483
484        let base_fk = self.faces.insert(FaceMeta {
485            primitive_key: prim_key,
486            normal: -Vector3::y(),
487            label: "base".to_string(),
488        });
489
490        let mut all_face_keys = vec![base_fk];
491        let mut all_edge_keys = Vec::new();
492        let mut polygons = Vec::new();
493
494        // Base cap (reversed winding)
495        let mut base_cap = base.clone();
496        base_cap.reverse();
497        if let Ok(mut p) =
498            Polygon::from_points_and_plane(base_cap, Plane::new(-Vector3::y(), half_h))
499        {
500            p.set_face_key(Some(base_fk));
501            polygons.push(p);
502        }
503
504        // Side triangles
505        for j in 0..segments {
506            let jn = (j + 1) % segments;
507
508            let side_fk = self.faces.insert(FaceMeta {
509                primitive_key: prim_key,
510                normal: {
511                    // Approximate outward normal for this side triangle
512                    let mid = (base[j].coords + base[jn].coords) / two
513                        - nalgebra::Vector3::new(T::zero(), -half_h, T::zero());
514                    mid.normalize()
515                },
516                label: format!("side_{j}"),
517            });
518            all_face_keys.push(side_fk);
519
520            // Base edge (shared with base cap)
521            let base_ek = self.edges.insert(EdgeMeta {
522                primitive_key: prim_key,
523                vertices: (base[j], base[jn]),
524                face_keys: [side_fk, base_fk],
525            });
526            all_edge_keys.push(base_ek);
527
528            if let Ok(mut p) = Polygon::from_points(vec![apex, base[jn], base[j]]) {
529                p.set_face_key(Some(side_fk));
530                polygons.push(p);
531            }
532        }
533
534        // Slant edges (apex to base vertex, shared between adjacent side faces)
535        for j in 0..segments {
536            let jp = if j == 0 { segments - 1 } else { j - 1 };
537            let side_fk_prev = all_face_keys[1 + jp];
538            let side_fk_curr = all_face_keys[1 + j];
539            let ek = self.edges.insert(EdgeMeta {
540                primitive_key: prim_key,
541                vertices: (apex, base[j]),
542                face_keys: [side_fk_prev, side_fk_curr],
543            });
544            all_edge_keys.push(ek);
545        }
546
547        if let Some(pm) = self.primitives.get_mut(prim_key) {
548            pm.face_keys = all_face_keys;
549            pm.edge_keys = all_edge_keys;
550        }
551
552        Csg::from_polygons(polygons)
553    }
554
555    /// Create a torus centered at the origin, lying in the XZ plane.
556    pub fn torus(
557        &mut self,
558        major_radius: T,
559        minor_radius: T,
560        major_segments: usize,
561        minor_segments: usize,
562    ) -> Csg<T> {
563        let major_segments = major_segments.max(3);
564        let minor_segments = minor_segments.max(3);
565        let two_pi = T::PI() + T::PI();
566        let maj_t = T::from(major_segments).expect("BUG: f64 must represent segment count");
567        let min_t = T::from(minor_segments).expect("BUG: f64 must represent segment count");
568
569        let vertex = |i: usize, j: usize| -> Point<T, 3> {
570            let theta = two_pi * T::from(i).expect("BUG: f64 must represent loop index") / maj_t;
571            let phi = two_pi * T::from(j).expect("BUG: f64 must represent loop index") / min_t;
572            let r = major_radius + minor_radius * num_traits::real::Real::cos(phi);
573            Point::from([
574                r * num_traits::real::Real::cos(theta),
575                minor_radius * num_traits::real::Real::sin(phi),
576                r * num_traits::real::Real::sin(theta),
577            ])
578        };
579
580        let prim_key = self.primitives.insert(PrimitiveMeta {
581            kind: PrimitiveKind::Torus,
582            face_keys: Vec::new(),
583            edge_keys: Vec::new(),
584            color: None,
585        });
586
587        let mut all_face_keys = Vec::new();
588        let all_edge_keys = Vec::new();
589        let mut polygons = Vec::new();
590
591        // Each quad on the torus surface is its own face.
592        for i in 0..major_segments {
593            let in_ = (i + 1) % major_segments;
594            for j in 0..minor_segments {
595                let jn = (j + 1) % minor_segments;
596
597                let v00 = vertex(i, j);
598                let v01 = vertex(i, jn);
599                let v10 = vertex(in_, j);
600                let v11 = vertex(in_, jn);
601
602                let face_fk = self.faces.insert(FaceMeta {
603                    primitive_key: prim_key,
604                    normal: {
605                        let e1 = v01 - v00;
606                        let e2 = v10 - v00;
607                        e1.cross(&e2).normalize()
608                    },
609                    label: format!("quad_{i}_{j}"),
610                });
611                all_face_keys.push(face_fk);
612
613                if let Ok(mut p) = Polygon::from_points(vec![v00, v01, v11, v10]) {
614                    p.set_face_key(Some(face_fk));
615                    polygons.push(p);
616                }
617            }
618        }
619
620        if let Some(pm) = self.primitives.get_mut(prim_key) {
621            pm.face_keys = all_face_keys;
622            pm.edge_keys = all_edge_keys;
623        }
624
625        Csg::from_polygons(polygons)
626    }
627
628    /// Create a parallelepiped (rhomboid) centered at the origin.
629    pub fn rhomboid(&mut self, a: Vector3<T>, b: Vector3<T>, c: Vector3<T>) -> Csg<T> {
630        let two = T::one() + T::one();
631        let offset = (a + b + c) / two;
632
633        let corners = [
634            Vector3::zeros(), // 0
635            a,                // 1: +a
636            a + b,            // 2: +a+b
637            b,                // 3: +b
638            c,                // 4: +c
639            a + c,            // 5: +a+c
640            a + b + c,        // 6: +a+b+c
641            b + c,            // 7: +b+c
642        ];
643        let v: [Point<T, 3>; 8] = corners.map(|corner| Point::from(corner - offset));
644
645        let face_defs: [([usize; 4], &str); 6] = [
646            ([0, 3, 2, 1], "-c"),
647            ([4, 5, 6, 7], "+c"),
648            ([0, 4, 7, 3], "-a"),
649            ([1, 2, 6, 5], "+a"),
650            ([0, 1, 5, 4], "-b"),
651            ([3, 7, 6, 2], "+b"),
652        ];
653
654        let four = T::from(4).expect("BUG: f64 must represent 4");
655
656        let prim_key = self.primitives.insert(PrimitiveMeta {
657            kind: PrimitiveKind::Rhomboid,
658            face_keys: Vec::new(),
659            edge_keys: Vec::new(),
660            color: None,
661        });
662
663        let mut all_face_keys = Vec::new();
664        let mut all_edge_keys = Vec::new();
665        let mut polygons = Vec::new();
666
667        let mut face_keys_arr = Vec::with_capacity(6);
668
669        for (idx, label) in face_defs.iter() {
670            let verts: Vec<Point<T, 3>> = idx.iter().map(|&i| v[i]).collect();
671            let face_center =
672                (verts[0].coords + verts[1].coords + verts[2].coords + verts[3].coords) / four;
673
674            let poly = match Polygon::from_points(verts) {
675                Ok(p) => p,
676                Err(_) => {
677                    face_keys_arr.push(None);
678                    continue;
679                }
680            };
681
682            let (poly, normal) = if poly.plane().normal().dot(&face_center) < T::zero() {
683                let flipped = poly.flip();
684                let n = flipped.plane().normal();
685                (flipped, n)
686            } else {
687                let n = poly.plane().normal();
688                (poly, n)
689            };
690
691            let fk = self.faces.insert(FaceMeta {
692                primitive_key: prim_key,
693                normal,
694                label: label.to_string(),
695            });
696            face_keys_arr.push(Some(fk));
697            all_face_keys.push(fk);
698
699            let mut poly = poly;
700            poly.set_face_key(Some(fk));
701            polygons.push(poly);
702        }
703
704        // Edges — deduplicate by canonical vertex index pair
705        let edge_indices: [[[usize; 2]; 4]; 6] = [
706            [[0, 3], [3, 2], [2, 1], [1, 0]],
707            [[4, 5], [5, 6], [6, 7], [7, 4]],
708            [[0, 4], [4, 7], [7, 3], [3, 0]],
709            [[1, 2], [2, 6], [6, 5], [5, 1]],
710            [[0, 1], [1, 5], [5, 4], [4, 0]],
711            [[3, 7], [7, 6], [6, 2], [2, 3]],
712        ];
713
714        let mut edge_map: std::collections::BTreeMap<(usize, usize), EdgeKey> =
715            std::collections::BTreeMap::new();
716
717        for (face_idx, edges) in edge_indices.iter().enumerate() {
718            let Some(fk) = face_keys_arr[face_idx] else {
719                continue;
720            };
721            for &[ea, eb] in edges {
722                let canonical = if ea < eb { (ea, eb) } else { (eb, ea) };
723                edge_map
724                    .entry(canonical)
725                    .and_modify(|ek| {
726                        if let Some(em) = self.edges.get_mut(*ek) {
727                            em.face_keys[1] = fk;
728                        }
729                    })
730                    .or_insert_with(|| {
731                        let ek = self.edges.insert(EdgeMeta {
732                            primitive_key: prim_key,
733                            vertices: (v[ea], v[eb]),
734                            face_keys: [fk, fk],
735                        });
736                        all_edge_keys.push(ek);
737                        ek
738                    });
739            }
740        }
741
742        if let Some(pm) = self.primitives.get_mut(prim_key) {
743            pm.face_keys = all_face_keys;
744            pm.edge_keys = all_edge_keys;
745        }
746
747        Csg::from_polygons(polygons)
748    }
749
750    /// Create a UV sphere centered at the origin.
751    pub fn sphere(&mut self, radius: T, segments: usize, stacks: usize) -> Csg<T> {
752        use itertools::*;
753
754        let segments = segments.max(3);
755        let stacks = stacks.max(2);
756
757        let pi = T::PI();
758        let two = T::one() + T::one();
759        let two_pi = two * pi;
760
761        let stacks_t = T::from(stacks).expect("BUG: failed to convert stacks to T");
762        let segments_t = T::from(segments).expect("BUG: failed to convert segments to T");
763
764        let point_on_surface = |frac_seg: T, frac_stack: T| {
765            let lat = pi * frac_stack;
766            let stack_radius = radius * num_traits::real::Real::sin(lat);
767            let x = stack_radius * num_traits::real::Real::sin(two_pi * frac_seg);
768            let y = radius * num_traits::real::Real::cos(lat);
769            let z = stack_radius * num_traits::real::Real::cos(two_pi * frac_seg);
770            Point3::new(x, y, z)
771        };
772
773        let seg_fracs: Vec<T> = (0..segments)
774            .flat_map(|v| T::from(v))
775            .map(|v| v / segments_t)
776            .collect();
777
778        let stack_pairs: Vec<(T, T)> = (1..stacks)
779            .flat_map(|v| T::from(v))
780            .map(|v| v / stacks_t)
781            .tuple_windows()
782            .collect();
783
784        let first_ring = T::one() / stacks_t;
785        let last_ring = T::from(stacks - 1).expect("BUG: stacks > 1") / stacks_t;
786
787        let prim_key = self.primitives.insert(PrimitiveMeta {
788            kind: PrimitiveKind::Sphere,
789            face_keys: Vec::new(),
790            edge_keys: Vec::new(),
791            color: None,
792        });
793
794        let mut all_face_keys = Vec::new();
795        let mut polygons = Vec::new();
796
797        // Top cap
798        let top_fk = self.faces.insert(FaceMeta {
799            primitive_key: prim_key,
800            normal: Vector3::y(),
801            label: "top_cap".to_string(),
802        });
803        all_face_keys.push(top_fk);
804
805        if let Ok(mut p) =
806            Polygon::from_points(seg_fracs.iter().map(|&s| point_on_surface(s, first_ring)))
807        {
808            p.set_face_key(Some(top_fk));
809            polygons.push(p);
810        }
811
812        // Bottom cap
813        let bot_fk = self.faces.insert(FaceMeta {
814            primitive_key: prim_key,
815            normal: -Vector3::y(),
816            label: "bottom_cap".to_string(),
817        });
818        all_face_keys.push(bot_fk);
819
820        if let Ok(mut p) = Polygon::from_points(
821            seg_fracs
822                .iter()
823                .rev()
824                .map(|&s| point_on_surface(s, last_ring)),
825        ) {
826            p.set_face_key(Some(bot_fk));
827            polygons.push(p);
828        }
829
830        // Body quads — each quad is its own face
831        for (left, right) in seg_fracs.iter().copied().circular_tuple_windows::<(_, _)>() {
832            for &(top, bottom) in &stack_pairs {
833                let face_fk = self.faces.insert(FaceMeta {
834                    primitive_key: prim_key,
835                    normal: {
836                        let center = point_on_surface((left + right) / two, (top + bottom) / two);
837                        center.coords.normalize()
838                    },
839                    label: "body".to_string(),
840                });
841                all_face_keys.push(face_fk);
842
843                if let Ok(mut p) = Polygon::from_points([
844                    point_on_surface(left, top),
845                    point_on_surface(left, bottom),
846                    point_on_surface(right, bottom),
847                    point_on_surface(right, top),
848                ]) {
849                    p.set_face_key(Some(face_fk));
850                    polygons.push(p);
851                }
852            }
853        }
854
855        if let Some(pm) = self.primitives.get_mut(prim_key) {
856            pm.face_keys = all_face_keys;
857            // Edge tracking for sphere is omitted — too many edges for little value.
858            pm.edge_keys = Vec::new();
859        }
860
861        Csg::from_polygons(polygons)
862    }
863}