1use 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
137fn 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
151fn 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#[derive(Debug, Clone, Copy, PartialEq)]
160pub enum Primitive {
161 Cuboid {
163 lo: [f64; 3],
165 hi: [f64; 3],
167 },
168 Cylinder {
172 base: [f64; 3],
174 axis: [f64; 3],
176 radius: f64,
178 height: f64,
180 },
181 HalfSpace {
185 normal: [f64; 3],
187 offset: f64,
189 },
190}
191
192#[derive(Debug, Clone, PartialEq)]
194pub enum Csg {
195 Primitive(Primitive),
197 Union(Vec<Csg>),
199 Intersection(Vec<Csg>),
201 Difference(Box<Csg>, Box<Csg>),
203}
204
205impl Csg {
206 pub fn cuboid(lo: [f64; 3], hi: [f64; 3]) -> Self {
208 Self::Primitive(Primitive::Cuboid { lo, hi })
209 }
210
211 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 pub fn half_space(normal: [f64; 3], offset: f64) -> Self {
224 Self::Primitive(Primitive::HalfSpace { normal, offset })
225 }
226
227 pub fn union(self, other: Csg) -> Self {
229 Self::Union(vec![self, other])
230 }
231
232 pub fn intersect(self, other: Csg) -> Self {
234 Self::Intersection(vec![self, other])
235 }
236
237 pub fn minus(self, other: Csg) -> Self {
239 Self::Difference(Box::new(self), Box::new(other))
240 }
241
242 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 *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 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 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 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 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 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 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#[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
519fn 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
543fn 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
558fn 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
575fn 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
602fn union(a: &[Span], b: &[Span]) -> Vec<Span> {
604 complement(&intersect(&complement(a), &complement(b)))
605}
606
607fn 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#[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 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 pub fn solid(&self, region: usize) -> &Csg {
686 &self.solids[region].0
687 }
688
689 pub fn tolerance_m(&self) -> f64 {
691 self.tol
692 }
693
694 pub fn bounds(&self) -> ([f64; 3], [f64; 3]) {
696 (self.lo, self.hi)
697 }
698
699 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 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 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 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 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 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 assert!(spans(&b, [-1.0, 2.0, 0.5], [1.0, 0.0, 0.0]).is_empty());
900 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 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 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 let cyl = Csg::cylinder([0.0; 3], [0.0, 0.0, 5.0], 1.0, 2.0);
917 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 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 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 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 assert!(spans(&cyl, [-3.0, 1.0, 1.0], [1.0, 0.0, 0.0]).is_empty());
939 assert!(spans(&cyl, [1.5, 0.0, -1.0], [0.0, 0.0, 1.0]).is_empty());
941 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}