Skip to main content

lindhard/geometry/
csg.rs

1//! Constructive solid geometry: box, cylinder and half-space primitives,
2//! combined by union, intersection and difference, and the [`CsgGeometry`]
3//! target built from them.
4//!
5//! # Solids and regions
6//!
7//! A [`Csg`] is a tree whose leaves are [`Primitive`]s and whose inner nodes
8//! are [`Csg::Union`], [`Csg::Intersection`] and [`Csg::Difference`]; the
9//! operators nest freely. A [`CsgGeometry`] is an ordered list of top-level
10//! solids, each tagged with an index into a shared material list. **One region
11//! per top-level solid**, in the order listed. Space outside every solid is
12//! vacuum ([`Geometry::locate`] returns `None`).
13//!
14//! # Ownership rules
15//!
16//! These are the rules of the triangle-mesh target, adopted verbatim (see the
17//! [`mesh`](super::mesh) module docs), so that a CSG solid, the same solid as
18//! a mesh and the same solid as a [`VoxelGrid`](super::VoxelGrid) agree on
19//! every shared face.
20//!
21//! 1. **Overlap.** Where solids overlap, the first listed solid owns the
22//!    space, so a mask listed before a substrate cuts into it. No overlap
23//!    search is needed.
24//! 2. **Surface points, directional.** A flight is owned, at each point, by
25//!    the solid it is heading into: the state of a solid at the start of a
26//!    flight is its state at `pos + tol * dir`. [`Geometry::locate`] has no
27//!    direction; it uses the fixed offset `tol * (1, 1, 1) / sqrt(3)`, so the
28//!    lower faces (and edges, vertices) of an axis-aligned box belong to it and
29//!    the upper ones do not, exactly as voxel faces do. Where that offset does
30//!    not move a point off a surface (a plane containing the diagonal
31//!    `(1, 1, 1)`), and for a flight lying exactly in a surface, the point
32//!    belongs to the side that `(1, 1, 1)` points into, and for a plane
33//!    containing the diagonal to the side its normal's first non-zero
34//!    component points away from. Each plane thus gives its points to exactly
35//!    one side.
36//! 3. **No re-hit.** Crossings at `t <= tol` along the flight are ignored, so
37//!    a particle that has just crossed a surface and sits on it (to rounding)
38//!    does not cross it again.
39//! 4. **Coincident and tangent surfaces.** Along a flight every primitive and
40//!    every boolean node is a sorted list of inside spans (below). At each
41//!    node, spans no longer than `tol` are removed and gaps no longer than
42//!    `tol` are closed. So a ray that grazes a cylinder or clips a box corner
43//!    within `tol` does not enter it; a hole cut flush with a face leaves no
44//!    sliver; and two boxes united along a shared face are one span with no
45//!    crossing at the face. Crossings of different top-level solids within
46//!    `tol` of each other are one event, so a flight through a surface shared
47//!    by two solids passes directly from one to the other and never through
48//!    vacuum between them. This is the CSG form of the mesh rule that two
49//!    crossings of one solid closer than `tol` cancel.
50//!
51//! The tolerance is `tol = 1e-10 x` the diagonal of the bounding box of all
52//! solids, the same constant as the mesh target (see
53//! [`CsgGeometry::tolerance_m`]).
54//!
55//! # Bounded solids
56//!
57//! A half-space is unbounded, so it may only appear where a combination
58//! bounds it: every top-level solid must have a finite axis-aligned bounding
59//! box, otherwise [`CsgGeometry::new`] returns
60//! [`GeometryError::CsgUnbounded`]. Bounds are taken conservatively: a box's
61//! own; a cylinder's exact box (its end discs, each of which extends
62//! `r sqrt(1 - a_k^2)` along axis `k` for a unit axis `a`); a half-space whose
63//! normal is a coordinate axis is bounded on that one side, any other is
64//! unbounded; a union takes the hull of its operands, an intersection their
65//! overlap, a difference the bounds of what it cuts from. So a box cut by an
66//! oblique half-space, or six axis-aligned half-spaces intersected, are
67//! bounded, but a lone half-space or a union containing one is not. A
68//! top-level bounding box that is empty on some axis is rejected as
69//! [`GeometryError::CsgInvalid`]. The scene bounding box (for `tol` and
70//! [`Geometry::entry_point`]) is the hull of the top-level boxes.
71//!
72//! # Events
73//!
74//! As for meshes: [`Geometry::exit`] reports `Enter { region, pos }` when the
75//! owner changes to a solid of a *different material* (a change between
76//! solids of the same material is not an event), and `Escape` when the flight
77//! leaves every solid. [`Geometry::flight`] is overridden to report the solid
78//! a clear flight ends in. The `Face` of an escape comes from the outward
79//! unit normal `n` of the surface crossed (for the subtracted operand of a
80//! difference, its inward normal): [`Face::Front`](super::Face::Front) if
81//! `n_x` is negative and the largest component in magnitude,
82//! [`Face::Back`](super::Face::Back) if positive and largest,
83//! [`Face::Side`](super::Face::Side) otherwise.
84//!
85//! # Ray classification
86//!
87//! The ray-casting method of S. D. Roth, "Ray casting for modeling solids",
88//! Computer Graphics and Image Processing 18 (1982) 109-144: each primitive
89//! is classified along the (whole, unbounded) line of the flight into the
90//! parameter spans where the line is inside it, and the span lists are
91//! combined up the tree by the set operation of each node. Every span end
92//! carries the outward normal of the surface it lies on; a difference
93//! negates the normals of the subtracted operand. Here union and difference
94//! are built from intersection and complement (`A u B` is the complement of
95//! `A' n B'`, `A - B` is `A n B'`), and the rule 4 clean-up is applied at
96//! every node. Each primitive is convex, so its classification is a single
97//! span:
98//!
99//! * **Box**, axis aligned: the intersection of the three slab intervals of
100//!   T. L. Kay and J. T. Kajiya, "Ray tracing complex scenes", Computer
101//!   Graphics (Proc. SIGGRAPH '86) 20 (4) (1986) 269-278.
102//! * **Cylinder**, finite and capped: the slab interval of its two end planes
103//!   (as for the box) intersected with the interval inside the infinite
104//!   cylinder, whose ends are the roots of `|w + t v|^2 = r^2` (`w`, `v` the
105//!   components of the offset from the base and of the direction normal to
106//!   the axis). The roots use the cancellation-free form of the quadratic
107//!   formula, `q = -(b + sgn(b) sqrt(b^2 - a c))`, `t = q / a` and `t = c / q`
108//!   (W. H. Press, S. A. Teukolsky, W. T. Vetterling and B. P. Flannery,
109//!   *Numerical Recipes*, 3rd ed., Cambridge University Press 2007, section
110//!   5.6). A tangent line (zero discriminant) does not enter.
111//! * **Half-space** `{p : n . p <= offset}`: the half-line on the inside of
112//!   the plane crossing.
113//!
114//! [`Geometry::locate`] classifies the offset point directly: a union holds it
115//! if any operand does, an intersection if all do, a difference if the first
116//! operand does and the second does not.
117//!
118//! A flight classifies every primitive of every solid once, so its cost is
119//! linear in the total number of primitives. Nothing is cached between
120//! queries and no query depends on thread count or evaluation order.
121
122use super::mesh::{face_of, TOL_REL};
123use super::{Exit, ExitOutcome, Flight, Geometry, GeometryError};
124use crate::material::Material;
125use crate::vec3::{dot, norm, scale, sub, V3};
126
127fn neg(a: V3) -> V3 {
128    [-a[0], -a[1], -a[2]]
129}
130
131fn axis(k: usize) -> V3 {
132    let mut e = [0.0; 3];
133    e[k] = 1.0;
134    e
135}
136
137/// Whether a point exactly on a surface with outward normal `n` belongs to
138/// the solid (rule 2 of the module docs): yes if the diagonal `(1, 1, 1)`
139/// points inward; for a surface containing the diagonal, yes if the first
140/// non-zero component of `n` is negative. `owns(n)` and `owns(-n)` always
141/// differ for a non-zero `n` (negation is exact, so the sum flips sign
142/// exactly).
143fn owns(n: V3) -> bool {
144    let e = n[0] + n[1] + n[2];
145    if e != 0.0 {
146        return e < 0.0;
147    }
148    n.iter().find(|c| **c != 0.0).is_some_and(|c| *c < 0.0)
149}
150
151/// Whether the coordinate `x` lies in the slab `[lo, hi]` measured along the
152/// unit vector `u` (outward normals `-u` at `lo`, `u` at `hi`), with rule-2
153/// ownership of the two planes.
154fn in_slab(x: f64, lo: f64, hi: f64, u: V3) -> bool {
155    (x > lo || (x == lo && owns(neg(u)))) && (x < hi || (x == hi && owns(u)))
156}
157
158/// One axis-aligned primitive solid. Lengths in metres.
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub enum Primitive {
161    /// The axis-aligned box `[lo, hi]` (each `lo[k] < hi[k]`).
162    Cuboid {
163        /// Lower corner, m.
164        lo: [f64; 3],
165        /// Upper corner, m.
166        hi: [f64; 3],
167    },
168    /// A finite cylinder with flat end caps: the points `base + s a + w` with
169    /// `0 <= s <= height` and `w` normal to `a`, `|w| <= radius`, where `a` is
170    /// `axis` normalised.
171    Cylinder {
172        /// Centre of the first end cap, m.
173        base: [f64; 3],
174        /// Direction of the axis (any non-zero length; normalised on build).
175        axis: [f64; 3],
176        /// Radius, m.
177        radius: f64,
178        /// Length along the axis, m.
179        height: f64,
180    },
181    /// The half-space `{p : normal . p <= offset}`, with `normal` its outward
182    /// normal. Scaling `normal` and `offset` together describes the same
183    /// half-space (normalised on build). Unbounded: see the module docs.
184    HalfSpace {
185        /// Outward normal (any non-zero length).
186        normal: [f64; 3],
187        /// Offset, in units of `|normal|` m.
188        offset: f64,
189    },
190}
191
192/// A constructive-solid-geometry tree. See the module docs.
193#[derive(Debug, Clone, PartialEq)]
194pub enum Csg {
195    /// A single primitive.
196    Primitive(Primitive),
197    /// Points inside any operand (at least one operand).
198    Union(Vec<Csg>),
199    /// Points inside every operand (at least one operand).
200    Intersection(Vec<Csg>),
201    /// Points inside the first operand and not inside the second.
202    Difference(Box<Csg>, Box<Csg>),
203}
204
205impl Csg {
206    /// The axis-aligned box `[lo, hi]`, m.
207    pub fn cuboid(lo: [f64; 3], hi: [f64; 3]) -> Self {
208        Self::Primitive(Primitive::Cuboid { lo, hi })
209    }
210
211    /// A capped cylinder from the centre of its first end cap along `axis`,
212    /// m.
213    pub fn cylinder(base: [f64; 3], axis: [f64; 3], radius: f64, height: f64) -> Self {
214        Self::Primitive(Primitive::Cylinder {
215            base,
216            axis,
217            radius,
218            height,
219        })
220    }
221
222    /// The half-space `{p : normal . p <= offset}`.
223    pub fn half_space(normal: [f64; 3], offset: f64) -> Self {
224        Self::Primitive(Primitive::HalfSpace { normal, offset })
225    }
226
227    /// `self` united with `other`.
228    pub fn union(self, other: Csg) -> Self {
229        Self::Union(vec![self, other])
230    }
231
232    /// `self` intersected with `other`.
233    pub fn intersect(self, other: Csg) -> Self {
234        Self::Intersection(vec![self, other])
235    }
236
237    /// `self` with `other` removed.
238    pub fn minus(self, other: Csg) -> Self {
239        Self::Difference(Box::new(self), Box::new(other))
240    }
241
242    /// Check every primitive and normalise cylinder axes and half-space
243    /// normals in place.
244    fn validate(&mut self) -> Result<(), &'static str> {
245        let finite = |v: &V3| v.iter().all(|c| c.is_finite());
246        match self {
247            Self::Primitive(Primitive::Cuboid { lo, hi }) => {
248                if !finite(lo) || !finite(hi) {
249                    return Err("a box corner is not finite");
250                }
251                if (0..3).any(|k| lo[k] >= hi[k]) {
252                    return Err("a box needs lo < hi on every axis");
253                }
254            }
255            Self::Primitive(Primitive::Cylinder {
256                base,
257                axis,
258                radius,
259                height,
260            }) => {
261                if !finite(base) {
262                    return Err("a cylinder base is not finite");
263                }
264                let l = norm(*axis);
265                if !finite(axis) || !(l > 0.0 && l.is_finite()) {
266                    return Err("a cylinder axis must be finite and non-zero");
267                }
268                if !(radius.is_finite() && *radius > 0.0) {
269                    return Err("a cylinder radius must be finite and positive");
270                }
271                if !(height.is_finite() && *height > 0.0) {
272                    return Err("a cylinder height must be finite and positive");
273                }
274                *axis = scale(*axis, 1.0 / l);
275            }
276            Self::Primitive(Primitive::HalfSpace { normal, offset }) => {
277                let l = norm(*normal);
278                if !finite(normal) || !(l > 0.0 && l.is_finite()) {
279                    return Err("a half-space normal must be finite and non-zero");
280                }
281                if !offset.is_finite() {
282                    return Err("a half-space offset is not finite");
283                }
284                // Divide, not multiply by 1/l: x / sqrt(x*x) is exactly ±1 for an axis normal.
285                *normal = [normal[0] / l, normal[1] / l, normal[2] / l];
286                *offset /= l;
287            }
288            Self::Union(v) | Self::Intersection(v) => {
289                if v.is_empty() {
290                    return Err("a union or intersection needs at least one operand");
291                }
292                for c in v {
293                    c.validate()?;
294                }
295            }
296            Self::Difference(a, b) => {
297                a.validate()?;
298                b.validate()?;
299            }
300        }
301        Ok(())
302    }
303
304    /// Conservative axis-aligned bounds `(lo, hi)` (module docs), possibly
305    /// infinite. Requires a validated tree.
306    fn bounds(&self) -> (V3, V3) {
307        match self {
308            Self::Primitive(Primitive::Cuboid { lo, hi }) => (*lo, *hi),
309            Self::Primitive(Primitive::Cylinder {
310                base,
311                axis,
312                radius,
313                height,
314            }) => {
315                let top = [
316                    base[0] + height * axis[0],
317                    base[1] + height * axis[1],
318                    base[2] + height * axis[2],
319                ];
320                let mut lo = [0.0; 3];
321                let mut hi = [0.0; 3];
322                for k in 0..3 {
323                    let e = radius * (1.0 - axis[k] * axis[k]).max(0.0).sqrt();
324                    lo[k] = base[k].min(top[k]) - e;
325                    hi[k] = base[k].max(top[k]) + e;
326                }
327                (lo, hi)
328            }
329            Self::Primitive(Primitive::HalfSpace { normal, offset }) => {
330                let mut lo = [f64::NEG_INFINITY; 3];
331                let mut hi = [f64::INFINITY; 3];
332                for k in 0..3 {
333                    if normal[k] == 1.0 {
334                        hi[k] = *offset;
335                    } else if normal[k] == -1.0 {
336                        lo[k] = -*offset;
337                    }
338                }
339                (lo, hi)
340            }
341            Self::Union(v) => v.iter().map(Csg::bounds).fold(
342                ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]),
343                |(lo, hi), (l, h)| {
344                    (
345                        [lo[0].min(l[0]), lo[1].min(l[1]), lo[2].min(l[2])],
346                        [hi[0].max(h[0]), hi[1].max(h[1]), hi[2].max(h[2])],
347                    )
348                },
349            ),
350            Self::Intersection(v) => v.iter().map(Csg::bounds).fold(
351                ([f64::NEG_INFINITY; 3], [f64::INFINITY; 3]),
352                |(lo, hi), (l, h)| {
353                    (
354                        [lo[0].max(l[0]), lo[1].max(l[1]), lo[2].max(l[2])],
355                        [hi[0].min(h[0]), hi[1].min(h[1]), hi[2].min(h[2])],
356                    )
357                },
358            ),
359            Self::Difference(a, _) => a.bounds(),
360        }
361    }
362
363    /// Whether `p` is inside, with rule-2 ownership of points exactly on a
364    /// surface. Requires a validated tree.
365    pub fn contains(&self, p: [f64; 3]) -> bool {
366        match self {
367            Self::Primitive(prim) => prim.contains(p),
368            Self::Union(v) => v.iter().any(|c| c.contains(p)),
369            Self::Intersection(v) => v.iter().all(|c| c.contains(p)),
370            Self::Difference(a, b) => a.contains(p) && !b.contains(p),
371        }
372    }
373
374    /// The inside spans of the line `o + t d` (`t` over all reals), sorted,
375    /// with the rule-4 clean-up at tolerance `tol`. Requires a validated tree.
376    fn spans(&self, o: V3, d: V3, tol: f64) -> Vec<Span> {
377        let out = match self {
378            Self::Primitive(prim) => prim.span(o, d).into_iter().collect(),
379            Self::Union(v) => {
380                let mut it = v.iter().map(|c| c.spans(o, d, tol));
381                let first = it.next().unwrap_or_default();
382                it.fold(first, |a, b| union(&a, &b))
383            }
384            Self::Intersection(v) => {
385                let mut it = v.iter().map(|c| c.spans(o, d, tol));
386                let first = it.next().unwrap_or_default();
387                it.fold(first, |a, b| intersect(&a, &b))
388            }
389            Self::Difference(a, b) => {
390                intersect(&a.spans(o, d, tol), &complement(&b.spans(o, d, tol)))
391            }
392        };
393        clean(out, tol)
394    }
395}
396
397impl Primitive {
398    fn contains(&self, p: V3) -> bool {
399        match *self {
400            Self::Cuboid { lo, hi } => (0..3).all(|k| in_slab(p[k], lo[k], hi[k], axis(k))),
401            Self::Cylinder {
402                base,
403                axis: a,
404                radius,
405                height,
406            } => {
407                let rel = sub(p, base);
408                let z = dot(rel, a);
409                if !in_slab(z, 0.0, height, a) {
410                    return false;
411                }
412                let w = sub(rel, scale(a, z));
413                let c = dot(w, w) - radius * radius;
414                c < 0.0 || (c == 0.0 && owns(w))
415            }
416            Self::HalfSpace { normal, offset } => {
417                let s = dot(normal, p) - offset;
418                s < 0.0 || (s == 0.0 && owns(normal))
419            }
420        }
421    }
422
423    /// The single inside span of the line `o + t d`, if any.
424    fn span(&self, o: V3, d: V3) -> Option<Span> {
425        match *self {
426            Self::Cuboid { lo, hi } => (0..3).try_fold(Span::ALL, |s, k| {
427                clip(s, slab(o[k], d[k], lo[k], hi[k], axis(k))?)
428            }),
429            Self::Cylinder {
430                base,
431                axis: a,
432                radius,
433                height,
434            } => {
435                let rel = sub(o, base);
436                let (z0, dz) = (dot(rel, a), dot(d, a));
437                let caps = slab(z0, dz, 0.0, height, a)?;
438                let w = sub(rel, scale(a, z0));
439                let v = sub(d, scale(a, dz));
440                let qa = dot(v, v);
441                let qb = dot(w, v);
442                let qc = dot(w, w) - radius * radius;
443                let side = if qa == 0.0 {
444                    // Parallel to the axis: inside the tube or not, all along.
445                    if qc < 0.0 || (qc == 0.0 && owns(w)) {
446                        Span::ALL
447                    } else {
448                        return None;
449                    }
450                } else {
451                    let disc = qb * qb - qa * qc;
452                    if disc.is_nan() || disc <= 0.0 {
453                        return None;
454                    }
455                    // Numerical Recipes (3rd ed.) section 5.6.
456                    let q = -(qb + disc.sqrt().copysign(qb));
457                    let (r1, r2) = (q / qa, qc / q);
458                    let (t0, t1) = if r1 <= r2 { (r1, r2) } else { (r2, r1) };
459                    let radial = |t: f64| {
460                        let n = [w[0] + t * v[0], w[1] + t * v[1], w[2] + t * v[2]];
461                        scale(n, 1.0 / norm(n))
462                    };
463                    Span {
464                        t0,
465                        n0: radial(t0),
466                        t1,
467                        n1: radial(t1),
468                    }
469                };
470                clip(caps, side)
471            }
472            Self::HalfSpace { normal, offset } => {
473                let s0 = dot(normal, o) - offset;
474                let ds = dot(normal, d);
475                if ds == 0.0 {
476                    (s0 < 0.0 || (s0 == 0.0 && owns(normal))).then_some(Span::ALL)
477                } else {
478                    let t = -s0 / ds;
479                    Some(if ds > 0.0 {
480                        Span {
481                            t0: f64::NEG_INFINITY,
482                            n0: [0.0; 3],
483                            t1: t,
484                            n1: normal,
485                        }
486                    } else {
487                        Span {
488                            t0: t,
489                            n0: normal,
490                            t1: f64::INFINITY,
491                            n1: [0.0; 3],
492                        }
493                    })
494                }
495            }
496        }
497    }
498}
499
500/// An inside span `[t0, t1]` of a line, with the outward unit normals of the
501/// surfaces at its ends (unused at an infinite end).
502#[derive(Debug, Clone, Copy, PartialEq)]
503struct Span {
504    t0: f64,
505    n0: V3,
506    t1: f64,
507    n1: V3,
508}
509
510impl Span {
511    const ALL: Span = Span {
512        t0: f64::NEG_INFINITY,
513        n0: [0.0; 3],
514        t1: f64::INFINITY,
515        n1: [0.0; 3],
516    };
517}
518
519/// The span of the slab `[lo, hi]` along the unit vector `u`, for a line at
520/// coordinate `x0` moving at rate `dx` (Kay and Kajiya 1986).
521fn slab(x0: f64, dx: f64, lo: f64, hi: f64, u: V3) -> Option<Span> {
522    if dx == 0.0 {
523        return in_slab(x0, lo, hi, u).then_some(Span::ALL);
524    }
525    let (ta, tb) = ((lo - x0) / dx, (hi - x0) / dx);
526    Some(if dx > 0.0 {
527        Span {
528            t0: ta,
529            n0: neg(u),
530            t1: tb,
531            n1: u,
532        }
533    } else {
534        Span {
535            t0: tb,
536            n0: u,
537            t1: ta,
538            n1: neg(u),
539        }
540    })
541}
542
543/// The overlap of two spans, if not empty. Ties in either end go to `a`.
544fn clip(a: Span, b: Span) -> Option<Span> {
545    let (t0, n0) = if b.t0 > a.t0 {
546        (b.t0, b.n0)
547    } else {
548        (a.t0, a.n0)
549    };
550    let (t1, n1) = if b.t1 < a.t1 {
551        (b.t1, b.n1)
552    } else {
553        (a.t1, a.n1)
554    };
555    (t0 < t1).then_some(Span { t0, n0, t1, n1 })
556}
557
558/// The intersection of two sorted, disjoint span lists.
559fn intersect(a: &[Span], b: &[Span]) -> Vec<Span> {
560    let mut out = Vec::new();
561    let (mut i, mut j) = (0, 0);
562    while i < a.len() && j < b.len() {
563        if let Some(s) = clip(a[i], b[j]) {
564            out.push(s);
565        }
566        if a[i].t1 < b[j].t1 {
567            i += 1;
568        } else {
569            j += 1;
570        }
571    }
572    out
573}
574
575/// The complement of a sorted, disjoint span list; the normals are negated so
576/// they stay outward.
577fn complement(a: &[Span]) -> Vec<Span> {
578    let mut out = Vec::with_capacity(a.len() + 1);
579    let (mut t, mut n) = (f64::NEG_INFINITY, [0.0; 3]);
580    for s in a {
581        if s.t0 > t {
582            out.push(Span {
583                t0: t,
584                n0: n,
585                t1: s.t0,
586                n1: neg(s.n0),
587            });
588        }
589        (t, n) = (s.t1, neg(s.n1));
590    }
591    if t < f64::INFINITY {
592        out.push(Span {
593            t0: t,
594            n0: n,
595            t1: f64::INFINITY,
596            n1: [0.0; 3],
597        });
598    }
599    out
600}
601
602/// The union of two sorted, disjoint span lists (De Morgan).
603fn union(a: &[Span], b: &[Span]) -> Vec<Span> {
604    complement(&intersect(&complement(a), &complement(b)))
605}
606
607/// Rule 4: drop spans no longer than `tol`, then close gaps no longer than
608/// `tol`.
609fn clean(spans: Vec<Span>, tol: f64) -> Vec<Span> {
610    let mut out: Vec<Span> = Vec::with_capacity(spans.len());
611    for s in spans.into_iter().filter(|s| s.t1 - s.t0 > tol) {
612        match out.last_mut() {
613            Some(last) if s.t0 - last.t1 <= tol => {
614                last.t1 = s.t1;
615                last.n1 = s.n1;
616            }
617            _ => out.push(s),
618        }
619    }
620    out
621}
622
623/// A target of CSG solids, each tagged with a material. See the module docs
624/// for the ownership rules.
625#[derive(Debug, Clone, PartialEq)]
626pub struct CsgGeometry {
627    materials: Vec<Material>,
628    solids: Vec<(Csg, u32)>,
629    tol: f64,
630    lo: V3,
631    hi: V3,
632}
633
634impl CsgGeometry {
635    /// Build from the material list and `(solid, material index)` pairs, in
636    /// priority order (the first listed owns overlaps). Lengths in metres.
637    /// Cylinder axes and half-space normals are normalised.
638    pub fn new(materials: Vec<Material>, solids: Vec<(Csg, u32)>) -> Result<Self, GeometryError> {
639        if materials.is_empty() {
640            return Err(GeometryError::NoMaterials);
641        }
642        if solids.is_empty() {
643            return Err(GeometryError::NoSolids);
644        }
645        let mut solids = solids;
646        let mut lo = [f64::INFINITY; 3];
647        let mut hi = [f64::NEG_INFINITY; 3];
648        for (i, (csg, m)) in solids.iter_mut().enumerate() {
649            if *m as usize >= materials.len() {
650                return Err(GeometryError::SolidMaterialOutOfRange {
651                    solid: i,
652                    index: *m,
653                    n_materials: materials.len(),
654                });
655            }
656            csg.validate()
657                .map_err(|reason| GeometryError::CsgInvalid { solid: i, reason })?;
658            let (l, h) = csg.bounds();
659            if !l.iter().chain(&h).all(|c| c.is_finite()) {
660                return Err(GeometryError::CsgUnbounded { solid: i });
661            }
662            if (0..3).any(|k| l[k] > h[k]) {
663                return Err(GeometryError::CsgInvalid {
664                    solid: i,
665                    reason: "the solid's bounding box is empty",
666                });
667            }
668            for k in 0..3 {
669                lo[k] = lo[k].min(l[k]);
670                hi[k] = hi[k].max(h[k]);
671            }
672        }
673        let tol = TOL_REL * norm(sub(hi, lo));
674        Ok(Self {
675            materials,
676            solids,
677            tol,
678            lo,
679            hi,
680        })
681    }
682
683    /// The solid of `region` (validated: unit cylinder axes and half-space
684    /// normals).
685    pub fn solid(&self, region: usize) -> &Csg {
686        &self.solids[region].0
687    }
688
689    /// The geometric tolerance of the ownership rules, m.
690    pub fn tolerance_m(&self) -> f64 {
691        self.tol
692    }
693
694    /// Axis-aligned bounds of all solids, `(min, max)`, m.
695    pub fn bounds(&self) -> ([f64; 3], [f64; 3]) {
696        (self.lo, self.hi)
697    }
698
699    /// [`Geometry::locate`] with the offset of rule 2 applied with the given
700    /// sign on each axis (all +1 for `locate`).
701    fn locate_shifted(&self, pos: V3, sign: V3) -> Option<usize> {
702        if !pos.iter().all(|c| c.is_finite()) {
703            return None;
704        }
705        let s = self.tol / 3f64.sqrt();
706        let p = [
707            pos[0] + sign[0] * s,
708            pos[1] + sign[1] * s,
709            pos[2] + sign[2] * s,
710        ];
711        self.solids.iter().position(|(c, _)| c.contains(p))
712    }
713}
714
715impl Geometry for CsgGeometry {
716    fn n_materials(&self) -> usize {
717        self.materials.len()
718    }
719
720    fn material(&self, index: usize) -> &Material {
721        &self.materials[index]
722    }
723
724    fn n_regions(&self) -> usize {
725        self.solids.len()
726    }
727
728    fn material_index(&self, region: usize) -> usize {
729        self.solids[region].1 as usize
730    }
731
732    fn locate(&self, pos: [f64; 3]) -> Option<usize> {
733        self.locate_shifted(pos, [1.0; 3])
734    }
735
736    fn entry_point(&self) -> [f64; 3] {
737        [
738            self.lo[0],
739            0.5 * (self.lo[1] + self.hi[1]),
740            0.5 * (self.lo[2] + self.hi[2]),
741        ]
742    }
743
744    fn exit(&self, region: usize, pos: [f64; 3], dir: [f64; 3], limit: f64) -> Option<Exit> {
745        match self.flight(region, pos, dir, limit) {
746            Flight::Event(e) => Some(e),
747            Flight::Clear { .. } => None,
748        }
749    }
750
751    fn flight(&self, region: usize, pos: [f64; 3], dir: [f64; 3], limit: f64) -> Flight {
752        let clear = Flight::Clear { region };
753        if !dir.iter().all(|d| d.is_finite())
754            || dir == [0.0; 3]
755            || !pos.iter().all(|c| c.is_finite())
756            || limit.is_nan()
757        {
758            return clear;
759        }
760        let tol = self.tol;
761        let n = self.solids.len();
762
763        // Span ends (t, solid, entering, normal) in (tol, limit + tol], and
764        // the state of every solid at t = tol (rules 2 and 3). The flight's
765        // own region is taken to be inside, as for meshes.
766        let mut events: Vec<(f64, u32, bool, V3)> = Vec::new();
767        let mut inside = vec![false; n];
768        for (j, (csg, _)) in self.solids.iter().enumerate() {
769            for s in csg.spans(pos, dir, tol) {
770                if s.t0 <= tol && s.t1 > tol {
771                    inside[j] = true;
772                }
773                for (t, entering, normal) in [(s.t0, true, s.n0), (s.t1, false, s.n1)] {
774                    if t > tol && t <= limit + tol {
775                        events.push((t, j as u32, entering, normal));
776                    }
777                }
778            }
779        }
780        inside[region] = true;
781        events.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
782
783        let owner_of = |inside: &[bool]| inside.iter().position(|&b| b);
784        let mut current = region;
785        // Start-state mismatch (a surface start heading into a solid that has
786        // priority or is not `region`): an event at distance 0.
787        let mut pending_start = true;
788        let mut i = 0usize;
789        loop {
790            let (t0, cluster_end) = if pending_start {
791                (0.0, i)
792            } else {
793                if i >= events.len() || events[i].0 > limit {
794                    return Flight::Clear { region: current };
795                }
796                let t0 = events[i].0;
797                let mut e = i;
798                while e < events.len() && events[e].0 - t0 <= tol {
799                    inside[events[e].1 as usize] = events[e].2;
800                    e += 1;
801                }
802                (t0, e)
803            };
804            let cluster = &events[i..cluster_end];
805            pending_start = false;
806            i = cluster_end;
807            let at = [
808                pos[0] + t0 * dir[0],
809                pos[1] + t0 * dir[1],
810                pos[2] + t0 * dir[2],
811            ];
812            match owner_of(&inside) {
813                Some(k) if k == current => {}
814                Some(k) => {
815                    if self.solids[k].1 == self.solids[current].1 {
816                        current = k;
817                    } else {
818                        return Flight::Event(Exit {
819                            distance: t0,
820                            at,
821                            outcome: ExitOutcome::Enter { region: k, pos: at },
822                        });
823                    }
824                }
825                None => {
826                    // The surface of the solid being left, else any.
827                    let normal = cluster
828                        .iter()
829                        .find(|e| e.1 as usize == current && !e.2)
830                        .or_else(|| cluster.first())
831                        .map_or(neg(dir), |e| e.3);
832                    return Flight::Event(Exit {
833                        distance: t0,
834                        at,
835                        outcome: ExitOutcome::Escape {
836                            face: face_of(normal),
837                            normal,
838                        },
839                    });
840                }
841            }
842        }
843    }
844
845    /// A point on the surface of a solid is not in vacuum, whichever of the
846    /// eight diagonal offsets (the one of [`Geometry::locate`] and its mirror
847    /// images) would put it outside.
848    fn in_vacuum(&self, pos: [f64; 3]) -> bool {
849        (0..8).all(|m| {
850            let sign = |k: usize| if m >> k & 1 == 0 { 1.0 } else { -1.0 };
851            self.locate_shifted(pos, [sign(0), sign(1), sign(2)])
852                .is_none()
853        })
854    }
855}
856
857#[cfg(test)]
858mod tests {
859    use super::super::test_util::{c, enter, si, unit};
860    use super::*;
861
862    const TOL: f64 = 1e-12;
863
864    fn spans(c: &Csg, o: V3, d: V3) -> Vec<(f64, f64)> {
865        let mut c = c.clone();
866        c.validate().unwrap();
867        c.spans(o, d, TOL).iter().map(|s| (s.t0, s.t1)).collect()
868    }
869
870    fn close(a: f64, b: f64) -> bool {
871        a == b || (a - b).abs() <= 1e-12 * (1.0 + a.abs().max(b.abs()))
872    }
873
874    fn same(got: &[(f64, f64)], want: &[(f64, f64)]) -> bool {
875        got.len() == want.len()
876            && got
877                .iter()
878                .zip(want)
879                .all(|(g, w)| close(g.0, w.0) && close(g.1, w.1))
880    }
881
882    const INF: f64 = f64::INFINITY;
883
884    #[test]
885    fn box_span_and_containment() {
886        let b = Csg::cuboid([0.0; 3], [2.0, 1.0, 1.0]);
887        let s = spans(&b, [-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
888        assert!(same(&s, &[(1.0, 3.0)]), "{s:?}");
889        let mut b2 = b.clone();
890        b2.validate().unwrap();
891        let sp = b2.spans([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0], TOL);
892        assert_eq!(sp[0].n0, [-1.0, 0.0, 0.0]);
893        assert_eq!(sp[0].n1, [1.0, 0.0, 0.0]);
894        // Oblique.
895        let d = unit([1.0, 1.0, 0.0]);
896        let s = spans(&b, [0.0, 0.0, 0.5], d);
897        assert!(same(&s, &[(0.0, 2f64.sqrt())]), "{s:?}");
898        // Misses, and parallel outside a slab.
899        assert!(spans(&b, [-1.0, 2.0, 0.5], [1.0, 0.0, 0.0]).is_empty());
900        // Parallel inside: unbounded in that direction only via other axes.
901        let s = spans(&b, [1.0, 0.5, 0.5], [0.0, 0.0, 1.0]);
902        assert!(same(&s, &[(-0.5, 0.5)]), "{s:?}");
903        // A ray lying in the lower face is inside, in the upper face outside.
904        assert_eq!(spans(&b, [-1.0, 0.0, 0.5], [1.0, 0.0, 0.0]).len(), 1);
905        assert!(spans(&b, [-1.0, 1.0, 0.5], [1.0, 0.0, 0.0]).is_empty());
906        // Containment: half-open, lower faces own.
907        assert!(b.contains([0.0, 0.0, 0.0]));
908        assert!(b.contains([1.0, 0.5, 0.5]));
909        assert!(!b.contains([2.0, 0.5, 0.5]));
910        assert!(!b.contains([1.0, 1.0, 0.5]));
911    }
912
913    #[test]
914    fn cylinder_span_caps_and_tangent() {
915        // Axis z, radius 1, from z = 0 to 2.
916        let cyl = Csg::cylinder([0.0; 3], [0.0, 0.0, 5.0], 1.0, 2.0);
917        // Across the axis.
918        let s = spans(&cyl, [-3.0, 0.0, 1.0], [1.0, 0.0, 0.0]);
919        assert!(same(&s, &[(2.0, 4.0)]), "{s:?}");
920        // Off-centre chord 2 sqrt(1 - b^2).
921        let b = 0.6;
922        let s = spans(&cyl, [-3.0, b, 1.0], [1.0, 0.0, 0.0]);
923        let h = (1.0 - b * b).sqrt();
924        assert!(same(&s, &[(3.0 - h, 3.0 + h)]), "{s:?}");
925        // Along the axis: through both caps.
926        let s = spans(&cyl, [0.5, 0.0, -1.0], [0.0, 0.0, 1.0]);
927        assert!(same(&s, &[(1.0, 3.0)]), "{s:?}");
928        let mut v = cyl.clone();
929        v.validate().unwrap();
930        let sp = v.spans([0.5, 0.0, -1.0], [0.0, 0.0, 1.0], TOL);
931        assert_eq!((sp[0].n0, sp[0].n1), ([0.0, 0.0, -1.0], [0.0, 0.0, 1.0]));
932        // Oblique, through a cap and the side: enters the bottom cap at
933        // (0, 0, 0), leaves the side at x = 1, z = 1.
934        let d = unit([1.0, 0.0, 1.0]);
935        let s = spans(&cyl, [-1.0, 0.0, -1.0], d);
936        assert!(same(&s, &[(2f64.sqrt(), 2.0 * 2f64.sqrt())]), "{s:?}");
937        // Tangent: no span.
938        assert!(spans(&cyl, [-3.0, 1.0, 1.0], [1.0, 0.0, 0.0]).is_empty());
939        // Parallel to the axis outside: none.
940        assert!(spans(&cyl, [1.5, 0.0, -1.0], [0.0, 0.0, 1.0]).is_empty());
941        // Containment (on the validated, unit-axis tree) and a tilted axis.
942        let mut cyl = cyl;
943        cyl.validate().unwrap();
944        assert!(cyl.contains([0.0, 0.0, 0.0]));
945        assert!(cyl.contains([0.5, 0.5, 1.0]));
946        assert!(!cyl.contains([0.0, 0.0, 2.0]));
947        assert!(!cyl.contains([0.8, 0.8, 1.0]));
948        let tilted = Csg::cylinder([0.0; 3], [1.0, 1.0, 0.0], 0.5, 3.0);
949        let s = spans(&tilted, [0.0, 0.0, 0.0], unit([1.0, 1.0, 0.0]));
950        assert!(same(&s, &[(0.0, 3.0)]), "{s:?}");
951    }
952
953    #[test]
954    fn half_space_span_and_parallel() {
955        // x <= 2.
956        let h = Csg::half_space([2.0, 0.0, 0.0], 4.0);
957        assert!(same(&spans(&h, [0.0; 3], [1.0, 0.0, 0.0]), &[(-INF, 2.0)]));
958        assert!(same(&spans(&h, [0.0; 3], [-1.0, 0.0, 0.0]), &[(-2.0, INF)]));
959        // Parallel to the plane: all or nothing.
960        assert!(same(
961            &spans(&h, [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
962            &[(-INF, INF)]
963        ));
964        assert!(spans(&h, [3.0, 0.0, 0.0], [0.0, 1.0, 0.0]).is_empty());
965        // In the plane: its outward normal +x points along the diagonal, so
966        // the plane is outside.
967        assert!(spans(&h, [2.0, 0.0, 0.0], [0.0, 1.0, 0.0]).is_empty());
968        assert!(!h.contains([2.0, 0.0, 0.0]));
969        assert!(h.contains([1.999, 5.0, 5.0]));
970        // A plane containing the diagonal: exactly one side owns it.
971        let a = Csg::half_space([1.0, -1.0, 0.0], 0.0);
972        let b = Csg::half_space([-1.0, 1.0, 0.0], 0.0);
973        let p = [1.0, 1.0, 7.0];
974        assert_ne!(a.contains(p), b.contains(p));
975    }
976
977    #[test]
978    fn boolean_spans() {
979        let x = [1.0, 0.0, 0.0];
980        let o = [-10.0, 0.5, 0.5];
981        let a = Csg::cuboid([0.0; 3], [2.0, 1.0, 1.0]);
982        let b = Csg::cuboid([1.0, 0.0, 0.0], [3.0, 1.0, 1.0]);
983        let far = Csg::cuboid([5.0, 0.0, 0.0], [6.0, 1.0, 1.0]);
984        // Overlapping union: one span; disjoint union: two.
985        let s = spans(&a.clone().union(b.clone()), o, x);
986        assert!(same(&s, &[(10.0, 13.0)]), "{s:?}");
987        let s = spans(&a.clone().union(far.clone()), o, x);
988        assert!(same(&s, &[(10.0, 12.0), (15.0, 16.0)]), "{s:?}");
989        // Abutting: merged (rule 4).
990        let ab = Csg::cuboid([2.0, 0.0, 0.0], [3.0, 1.0, 1.0]);
991        let s = spans(&a.clone().union(ab), o, x);
992        assert!(same(&s, &[(10.0, 13.0)]), "{s:?}");
993        // Intersection and difference.
994        let s = spans(&a.clone().intersect(b.clone()), o, x);
995        assert!(same(&s, &[(11.0, 12.0)]), "{s:?}");
996        let s = spans(&a.clone().minus(b.clone()), o, x);
997        assert!(same(&s, &[(10.0, 11.0)]), "{s:?}");
998        let s = spans(&b.clone().minus(a.clone()), o, x);
999        assert!(same(&s, &[(12.0, 13.0)]), "{s:?}");
1000        // A hole in the middle splits the span; its normals are inward to
1001        // the hole.
1002        let hole = Csg::cuboid([0.5, 0.0, 0.0], [1.0, 1.0, 1.0]);
1003        let mut d = a.clone().minus(hole);
1004        d.validate().unwrap();
1005        let sp = d.spans(o, x, TOL);
1006        assert_eq!(sp.len(), 2);
1007        assert!(close(sp[0].t1, 10.5) && close(sp[1].t0, 11.0));
1008        assert_eq!(sp[0].n1, [1.0, 0.0, 0.0]);
1009        assert_eq!(sp[1].n0, [-1.0, 0.0, 0.0]);
1010        // Nested: (A u B) - C with C in the middle of the union.
1011        let cc = Csg::cuboid([1.5, 0.0, 0.0], [2.5, 1.0, 1.0]);
1012        let s = spans(&a.clone().union(b.clone()).minus(cc), o, x);
1013        assert!(same(&s, &[(10.0, 11.5), (12.5, 13.0)]), "{s:?}");
1014        // A flush cut leaves no sliver (rule 4).
1015        let flush = Csg::cuboid([1.0 - 1e-14, 0.0, 0.0], [5.0, 1.0, 1.0]);
1016        let s = spans(&b.clone().minus(flush), o, x);
1017        assert!(s.is_empty(), "{s:?}");
1018        // Six axis-aligned half-spaces equal the box.
1019        let six = Csg::Intersection(vec![
1020            Csg::half_space([-1.0, 0.0, 0.0], 0.0),
1021            Csg::half_space([1.0, 0.0, 0.0], 2.0),
1022            Csg::half_space([0.0, -1.0, 0.0], 0.0),
1023            Csg::half_space([0.0, 1.0, 0.0], 1.0),
1024            Csg::half_space([0.0, 0.0, -1.0], 0.0),
1025            Csg::half_space([0.0, 0.0, 1.0], 1.0),
1026        ]);
1027        let d = unit([0.3, 0.2, 0.1]);
1028        assert!(same(
1029            &spans(&six, [0.1, 0.1, 0.1], d),
1030            &spans(&a, [0.1, 0.1, 0.1], d)
1031        ));
1032        let mut v = six.clone();
1033        v.validate().unwrap();
1034        assert_eq!(v.bounds(), ([0.0; 3], [2.0, 1.0, 1.0]));
1035    }
1036
1037    #[test]
1038    fn non_unit_axis_normals_stay_axis_aligned() {
1039        // 49 * (1/49) != 1 in f64, so normalising must divide, not scale.
1040        for k in [49.0, 98.0, 103.0, 107.0, 161.0, 187.0] {
1041            let six = Csg::Intersection(vec![
1042                Csg::half_space([-k, 0.0, 0.0], 0.0),
1043                Csg::half_space([k, 0.0, 0.0], 2.0 * k),
1044                Csg::half_space([0.0, -k, 0.0], 0.0),
1045                Csg::half_space([0.0, k, 0.0], k),
1046                Csg::half_space([0.0, 0.0, -k], 0.0),
1047                Csg::half_space([0.0, 0.0, k], k),
1048            ]);
1049            let mut v = six.clone();
1050            v.validate().unwrap();
1051            assert_eq!(v.bounds(), ([0.0; 3], [2.0, 1.0, 1.0]), "k = {k}");
1052        }
1053    }
1054
1055    #[test]
1056    fn validation_and_bounds() {
1057        let ok = |c: Csg| CsgGeometry::new(vec![si()], vec![(c, 0)]);
1058        assert!(matches!(
1059            ok(Csg::cuboid([0.0; 3], [0.0, 1.0, 1.0])),
1060            Err(GeometryError::CsgInvalid { solid: 0, .. })
1061        ));
1062        assert!(matches!(
1063            ok(Csg::cylinder([0.0; 3], [0.0; 3], 1.0, 1.0)),
1064            Err(GeometryError::CsgInvalid { .. })
1065        ));
1066        assert!(matches!(
1067            ok(Csg::cylinder([0.0; 3], [0.0, 0.0, 1.0], -1.0, 1.0)),
1068            Err(GeometryError::CsgInvalid { .. })
1069        ));
1070        assert!(matches!(
1071            ok(Csg::Union(vec![])),
1072            Err(GeometryError::CsgInvalid { .. })
1073        ));
1074        assert_eq!(
1075            ok(Csg::half_space([1.0, 0.0, 0.0], 1.0)),
1076            Err(GeometryError::CsgUnbounded { solid: 0 })
1077        );
1078        assert_eq!(
1079            ok(Csg::cuboid([0.0; 3], [1.0; 3]).union(Csg::half_space([1.0, 1.0, 0.0], 1.0))),
1080            Err(GeometryError::CsgUnbounded { solid: 0 })
1081        );
1082        assert!(matches!(
1083            ok(Csg::cuboid([0.0; 3], [1.0; 3]).intersect(Csg::cuboid([2.0; 3], [3.0; 3]))),
1084            Err(GeometryError::CsgInvalid { .. })
1085        ));
1086        // A box cut by an oblique plane is bounded by the box.
1087        let g = ok(Csg::cuboid([0.0; 3], [1.0; 3]).minus(Csg::half_space([1.0, 1.0, 1.0], 0.5)))
1088            .unwrap();
1089        assert_eq!(g.bounds(), ([0.0; 3], [1.0; 3]));
1090        // A tilted cylinder's exact box.
1091        let s = 0.5f64.sqrt();
1092        let g = ok(Csg::cylinder([0.0; 3], [1.0, 1.0, 0.0], 1.0, 2.0)).unwrap();
1093        let (lo, hi) = g.bounds();
1094        let want_lo = [-s, -s, -1.0];
1095        let want_hi = [2.0 * s + s, 2.0 * s + s, 1.0];
1096        for k in 0..3 {
1097            assert!(
1098                close(lo[k], want_lo[k]) && close(hi[k], want_hi[k]),
1099                "{lo:?} {hi:?}"
1100            );
1101        }
1102        // Validation normalises the half-space.
1103        let g =
1104            ok(Csg::cuboid([0.0; 3], [4.0; 3]).intersect(Csg::half_space([2.0, 0.0, 0.0], 6.0)))
1105                .unwrap();
1106        assert_eq!(g.bounds(), ([0.0; 3], [3.0, 4.0, 4.0]));
1107        assert!(CsgGeometry::new(vec![], vec![(Csg::cuboid([0.0; 3], [1.0; 3]), 0)]).is_err());
1108        assert_eq!(
1109            CsgGeometry::new(vec![si()], vec![]),
1110            Err(GeometryError::NoSolids)
1111        );
1112        assert_eq!(
1113            CsgGeometry::new(vec![si()], vec![(Csg::cuboid([0.0; 3], [1.0; 3]), 1)]),
1114            Err(GeometryError::SolidMaterialOutOfRange {
1115                solid: 0,
1116                index: 1,
1117                n_materials: 1
1118            })
1119        );
1120    }
1121
1122    #[test]
1123    fn mask_listed_first_cuts_into_substrate() {
1124        // A mask block over part of a substrate, listed first.
1125        let g = CsgGeometry::new(
1126            vec![si(), c()],
1127            vec![
1128                (Csg::cuboid([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]), 1),
1129                (Csg::cuboid([0.0, 0.0, 0.0], [4.0, 4.0, 4.0]), 0),
1130            ],
1131        )
1132        .unwrap();
1133        assert_eq!(g.locate([0.5, 0.5, 0.5]), Some(0));
1134        assert_eq!(g.locate([2.0, 0.5, 0.5]), Some(1));
1135        let e = g.exit(0, [0.5, 0.5, 0.5], [1.0, 0.0, 0.0], 1e9).unwrap();
1136        assert!(close(e.distance, 0.5));
1137        assert_eq!(enter(&e).0, 1);
1138    }
1139}