1use crate::feature::{FaceTag, LeafPath};
26use crate::prelude_::*;
27
28const T_EPS: f64 = 1.0e-9;
30const OUTLINE_SEGMENTS: usize = 48;
33
34pub type AabbHint<'a> = &'a dyn Fn(&DynamicObject) -> Option<(DVec3, DVec3)>;
38
39#[derive(Clone, Copy, Debug, PartialEq)]
41pub enum LeafPrim {
42 Cube { size: DVec3 },
44 Cylinder {
47 radius: f64,
48 height: f64,
49 segments: usize,
50 },
51 Cone {
53 radius: f64,
54 height: f64,
55 segments: usize,
56 },
57 Sphere { radius: f64 },
59 Aabb { min: DVec3, max: DVec3 },
61}
62
63#[derive(Clone, Copy, Debug)]
65pub struct GeomLeaf {
66 pub prim: LeafPrim,
67 pub world: DAffine3,
69 pub world_inv: DAffine3,
71}
72
73#[derive(Clone, Debug, PartialEq)]
75pub enum CsgExpr {
76 Leaf(usize),
77 Union(Vec<CsgExpr>),
78 Inter(Vec<CsgExpr>),
79 Diff(Vec<CsgExpr>),
81}
82
83#[derive(Clone, Debug)]
85pub struct CsgLeaves {
86 pub leaves: Vec<GeomLeaf>,
87 pub expr: CsgExpr,
88 pub paths: Vec<LeafPath>,
91}
92
93#[derive(Clone, Copy, Debug)]
95pub struct HitBoundary {
96 pub t: f64,
98 pub leaf: usize,
100 pub face: FaceTag,
101 pub flipped: bool,
104}
105
106#[derive(Clone, Copy, Debug)]
108pub struct HitSpan {
109 pub enter: HitBoundary,
110 pub exit: HitBoundary,
111}
112
113impl CsgLeaves {
114 pub fn from_dynamic(obj: &DynamicObject, world: DAffine3, aabb_hint: Option<AabbHint>) -> Self {
118 Self::build(obj, world, aabb_hint, false)
119 }
120
121 pub fn from_dynamic_unfeatured(
129 obj: &DynamicObject,
130 world: DAffine3,
131 aabb_hint: Option<AabbHint>,
132 ) -> Self {
133 Self::build(obj, world, aabb_hint, true)
134 }
135
136 fn build(
137 obj: &DynamicObject,
138 world: DAffine3,
139 aabb_hint: Option<AabbHint>,
140 strip_features: bool,
141 ) -> Self {
142 let mut csg = CsgLeaves {
143 leaves: Vec::new(),
144 expr: CsgExpr::Union(Vec::new()),
145 paths: Vec::new(),
146 };
147 let mut path = Vec::new();
148 csg.expr = csg.walk(obj, world, &mut path, aabb_hint, strip_features);
149 csg
150 }
151
152 pub fn is_empty(&self) -> bool {
154 self.leaves.is_empty()
155 }
156
157 fn push_leaf(&mut self, prim: LeafPrim, world: DAffine3, path: &[u16]) -> CsgExpr {
158 let idx = self.leaves.len();
159 self.leaves.push(GeomLeaf {
160 prim,
161 world,
162 world_inv: world.inverse(),
163 });
164 self.paths.push(LeafPath(path.to_vec()));
165 CsgExpr::Leaf(idx)
166 }
167
168 fn walk_children(
169 &mut self,
170 children: &[DynamicObject],
171 world: DAffine3,
172 path: &mut Vec<u16>,
173 hint: Option<AabbHint>,
174 strip: bool,
175 ) -> Vec<CsgExpr> {
176 let branching = children.len() > 1;
177 children
178 .iter()
179 .enumerate()
180 .map(|(i, child)| {
181 if branching {
182 path.push(i as u16);
183 }
184 let expr = self.walk(child, world, path, hint, strip);
185 if branching {
186 path.pop();
187 }
188 expr
189 })
190 .collect()
191 }
192
193 fn walk(
194 &mut self,
195 obj: &DynamicObject,
196 world: DAffine3,
197 path: &mut Vec<u16>,
198 hint: Option<AabbHint>,
199 strip: bool,
200 ) -> CsgExpr {
201 let empty = || CsgExpr::Union(Vec::new());
202 match obj {
203 DynamicObject::Cube { size } => self.push_leaf(
204 LeafPrim::Cube {
205 size: DVec3::from_array(*size),
206 },
207 world,
208 path,
209 ),
210 DynamicObject::Sphere { radius, .. } => {
211 self.push_leaf(LeafPrim::Sphere { radius: *radius }, world, path)
212 }
213 DynamicObject::Cylinder {
214 radius,
215 height,
216 segments,
217 } => self.push_leaf(
218 LeafPrim::Cylinder {
219 radius: *radius,
220 height: *height,
221 segments: *segments,
222 },
223 world,
224 path,
225 ),
226 DynamicObject::Cone {
227 radius,
228 height,
229 segments,
230 } => self.push_leaf(
231 LeafPrim::Cone {
232 radius: *radius,
233 height: *height,
234 segments: *segments,
235 },
236 world,
237 path,
238 ),
239
240 DynamicObject::Circle { .. }
242 | DynamicObject::Square { .. }
243 | DynamicObject::Polygon { .. }
244 | DynamicObject::Sketch { .. }
245 | DynamicObject::Empty => empty(),
246
247 DynamicObject::LinearExtrude { .. }
250 | DynamicObject::Revolve { .. }
251 | DynamicObject::Offset { .. } => match hint.and_then(|f| f(obj)) {
252 Some((min, max)) => self.push_leaf(LeafPrim::Aabb { min, max }, world, path),
253 None => empty(),
254 },
255
256 DynamicObject::EdgeFeature { data, child } => {
266 if strip {
267 path.push(0);
268 let child_expr = self.walk(child, world, path, hint, strip);
269 path.pop();
270 return child_expr;
271 }
272 let (masks, _) = crate::feature::lower::lower_masks(data, child);
273 path.push(0);
274 let child_expr = self.walk(child, world, path, hint, strip);
275 path.pop();
276 path.push(1);
277 let sub_exprs = self.walk_children(&masks.sub, world, path, None, strip);
278 path.pop();
279 path.push(2);
280 let add_exprs = self.walk_children(&masks.add, world, path, None, strip);
281 path.pop();
282 let cut = CsgExpr::Diff(vec![child_expr, CsgExpr::Union(sub_exprs)]);
283 CsgExpr::Union(core::iter::once(cut).chain(add_exprs).collect())
284 }
285
286 DynamicObject::Union { children } | DynamicObject::Group { children } => {
287 CsgExpr::Union(self.walk_children(children, world, path, hint, strip))
288 }
289 DynamicObject::Intersection { children } => {
290 CsgExpr::Inter(self.walk_children(children, world, path, hint, strip))
291 }
292 DynamicObject::Difference { children } => {
293 CsgExpr::Diff(self.walk_children(children, world, path, hint, strip))
294 }
295
296 DynamicObject::Translate { offset, child } => {
297 let world = world * DAffine3::from_translation(DVec3::from_array(*offset));
298 self.walk(child, world, path, hint, strip)
299 }
300 DynamicObject::Rotate { angles, child } => {
301 let q = DQuat::from_euler(
303 EulerRot::ZYX,
304 angles[2].to_radians(),
305 angles[1].to_radians(),
306 angles[0].to_radians(),
307 );
308 self.walk(child, world * DAffine3::from_quat(q), path, hint, strip)
309 }
310 DynamicObject::Scale { factors, child } => {
311 let world = world * DAffine3::from_scale(DVec3::from_array(*factors));
312 self.walk(child, world, path, hint, strip)
313 }
314 DynamicObject::Mirror { axis, child } => {
315 let axis = DVec3::from_array(*axis);
316 let len_sq = axis.length_squared();
317 if len_sq < 1.0e-24 {
318 return empty(); }
320 let n = axis / len_sq.sqrt();
321 let m = DMat3::IDENTITY - 2.0 * DMat3::from_cols(n * n.x, n * n.y, n * n.z);
322 self.walk(child, world * DAffine3::from_mat3(m), path, hint, strip)
323 }
324 DynamicObject::Color { child, .. } => self.walk(child, world, path, hint, strip),
325 DynamicObject::Material { child, .. } => self.walk(child, world, path, hint, strip),
326 }
327 }
328
329 pub fn spans(&self, ro: DVec3, rd: DVec3) -> Vec<HitSpan> {
332 self.expr_spans(&self.expr, ro, rd)
333 }
334
335 pub fn first_hit(&self, ro: DVec3, rd: DVec3, t_min: f64) -> Option<HitBoundary> {
337 self.spans(ro, rd)
338 .into_iter()
339 .flat_map(|s| [s.enter, s.exit])
340 .find(|b| b.t > t_min)
341 }
342
343 fn expr_spans(&self, expr: &CsgExpr, ro: DVec3, rd: DVec3) -> Vec<HitSpan> {
344 match expr {
345 CsgExpr::Leaf(idx) => leaf_spans(*idx, &self.leaves[*idx], ro, rd),
346 CsgExpr::Union(children) => children
347 .iter()
348 .map(|c| self.expr_spans(c, ro, rd))
349 .reduce(|a, b| merge_spans(a, b, BoolOp::Union))
350 .unwrap_or_default(),
351 CsgExpr::Inter(children) => {
352 let mut iter = children.iter().map(|c| self.expr_spans(c, ro, rd));
353 let Some(first) = iter.next() else {
354 return Vec::new();
355 };
356 iter.fold(first, |a, b| merge_spans(a, b, BoolOp::Inter))
357 }
358 CsgExpr::Diff(children) => {
359 let mut iter = children.iter().map(|c| self.expr_spans(c, ro, rd));
360 let Some(first) = iter.next() else {
361 return Vec::new();
362 };
363 iter.fold(first, |a, b| merge_spans(a, b, BoolOp::Diff))
364 }
365 }
366 }
367
368 pub fn contains(&self, p: DVec3) -> bool {
370 self.expr_contains(&self.expr, p)
371 }
372
373 fn expr_contains(&self, expr: &CsgExpr, p: DVec3) -> bool {
374 match expr {
375 CsgExpr::Leaf(idx) => leaf_contains(&self.leaves[*idx], p),
376 CsgExpr::Union(children) => children.iter().any(|c| self.expr_contains(c, p)),
377 CsgExpr::Inter(children) => {
378 !children.is_empty() && children.iter().all(|c| self.expr_contains(c, p))
379 }
380 CsgExpr::Diff(children) => match children.split_first() {
381 None => false,
382 Some((first, rest)) => {
383 self.expr_contains(first, p) && !rest.iter().any(|c| self.expr_contains(c, p))
384 }
385 },
386 }
387 }
388
389 pub fn boundary_normal(&self, boundary: &HitBoundary, ro: DVec3, rd: DVec3) -> DVec3 {
392 let leaf = &self.leaves[boundary.leaf];
393 let p_local = leaf.world_inv.transform_point3(ro + boundary.t * rd);
394 let n_local = local_face_normal(&leaf.prim, boundary.face, p_local);
395 let n_world = (leaf.world_inv.matrix3.transpose() * n_local).normalize_or_zero();
397 if boundary.flipped { -n_world } else { n_world }
398 }
399}
400
401fn leaf_spans(idx: usize, leaf: &GeomLeaf, ro: DVec3, rd: DVec3) -> Vec<HitSpan> {
406 let ro_l = leaf.world_inv.transform_point3(ro);
407 let rd_l = leaf.world_inv.matrix3 * rd;
408 let boundary = |t: f64, face: FaceTag| HitBoundary {
409 t,
410 leaf: idx,
411 face,
412 flipped: false,
413 };
414 let interval = match leaf.prim {
415 LeafPrim::Cube { size } => slab_interval(ro_l, rd_l, DVec3::ZERO, size, false),
416 LeafPrim::Aabb { min, max } => slab_interval(ro_l, rd_l, min, max, true),
417 LeafPrim::Sphere { radius } => sphere_interval(ro_l, rd_l, radius),
418 LeafPrim::Cylinder { radius, height, .. } => cylinder_interval(ro_l, rd_l, radius, height),
419 LeafPrim::Cone { radius, height, .. } => cone_interval(ro_l, rd_l, radius, height),
420 };
421 match interval {
422 Some(((t0, f0), (t1, f1))) if t1 - t0 > T_EPS => vec![HitSpan {
423 enter: boundary(t0, f0),
424 exit: boundary(t1, f1),
425 }],
426 _ => Vec::new(),
427 }
428}
429
430type FaceInterval = Option<((f64, FaceTag), (f64, FaceTag))>;
431
432fn slab_interval(ro: DVec3, rd: DVec3, min: DVec3, max: DVec3, proxy: bool) -> FaceInterval {
435 let mut t_in = (f64::NEG_INFINITY, FaceTag::Unknown);
436 let mut t_out = (f64::INFINITY, FaceTag::Unknown);
437 for axis in 0..3 {
438 let (o, d) = (ro[axis], rd[axis]);
439 if d == 0.0 {
440 if o < min[axis] || o > max[axis] {
441 return None;
442 }
443 continue;
444 }
445 let t_min_face = (min[axis] - o) / d;
446 let t_max_face = (max[axis] - o) / d;
447 let (near, far) = if d > 0.0 {
450 (
451 (t_min_face, tag_for(axis, false, proxy)),
452 (t_max_face, tag_for(axis, true, proxy)),
453 )
454 } else {
455 (
456 (t_max_face, tag_for(axis, true, proxy)),
457 (t_min_face, tag_for(axis, false, proxy)),
458 )
459 };
460 if near.0 > t_in.0 {
461 t_in = near;
462 }
463 if far.0 < t_out.0 {
464 t_out = far;
465 }
466 }
467 (t_in.0 < t_out.0).then_some((t_in, t_out))
468}
469
470fn tag_for(axis: usize, positive: bool, proxy: bool) -> FaceTag {
471 if proxy {
472 FaceTag::Unknown
473 } else {
474 FaceTag::cube(axis, positive)
475 }
476}
477
478fn sphere_interval(ro: DVec3, rd: DVec3, radius: f64) -> FaceInterval {
479 let a = rd.dot(rd);
480 if a == 0.0 {
481 return None;
482 }
483 let b = 2.0 * ro.dot(rd);
484 let c = ro.dot(ro) - radius * radius;
485 let disc = b * b - 4.0 * a * c;
486 if disc <= 0.0 {
487 return None;
488 }
489 let sq = disc.sqrt();
490 let (t0, t1) = ((-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a));
491 Some(((t0.min(t1), FaceTag::Sphere), (t0.max(t1), FaceTag::Sphere)))
492}
493
494fn cylinder_interval(ro: DVec3, rd: DVec3, radius: f64, height: f64) -> FaceInterval {
495 let a = rd.x * rd.x + rd.z * rd.z;
497 let radial: FaceInterval = if a == 0.0 {
498 (ro.x * ro.x + ro.z * ro.z <= radius * radius).then_some((
499 (f64::NEG_INFINITY, FaceTag::CylSide),
500 (f64::INFINITY, FaceTag::CylSide),
501 ))
502 } else {
503 let b = 2.0 * (ro.x * rd.x + ro.z * rd.z);
504 let c = ro.x * ro.x + ro.z * ro.z - radius * radius;
505 let disc = b * b - 4.0 * a * c;
506 (disc > 0.0).then(|| {
507 let sq = disc.sqrt();
508 let (t0, t1) = ((-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a));
509 (
510 (t0.min(t1), FaceTag::CylSide),
511 (t0.max(t1), FaceTag::CylSide),
512 )
513 })
514 };
515 let radial = radial?;
516 let axial: FaceInterval = if rd.y == 0.0 {
518 (0.0..=height).contains(&ro.y).then_some((
519 (f64::NEG_INFINITY, FaceTag::CylBottom),
520 (f64::INFINITY, FaceTag::CylTop),
521 ))
522 } else {
523 let ta = (0.0 - ro.y) / rd.y;
524 let tb = (height - ro.y) / rd.y;
525 Some(if rd.y > 0.0 {
526 ((ta, FaceTag::CylBottom), (tb, FaceTag::CylTop))
527 } else {
528 ((tb, FaceTag::CylTop), (ta, FaceTag::CylBottom))
529 })
530 };
531 let axial = axial?;
532 let t_in = if radial.0.0 > axial.0.0 {
533 radial.0
534 } else {
535 axial.0
536 };
537 let t_out = if radial.1.0 < axial.1.0 {
538 radial.1
539 } else {
540 axial.1
541 };
542 (t_in.0 < t_out.0).then_some((t_in, t_out))
543}
544
545fn cone_interval(ro: DVec3, rd: DVec3, radius: f64, height: f64) -> FaceInterval {
549 if height <= 0.0 || radius <= 0.0 {
550 return None;
551 }
552 let k = radius / height;
553 let inside = |t: f64| -> bool {
554 let p = ro + t * rd;
555 if p.y < 0.0 || p.y > height {
556 return false;
557 }
558 (p.x * p.x + p.z * p.z).sqrt() <= k * (height - p.y)
559 };
560
561 let mut candidates: Vec<(f64, FaceTag)> = Vec::with_capacity(4);
562 let a = rd.x * rd.x + rd.z * rd.z - k * k * rd.y * rd.y;
564 let b = 2.0 * (ro.x * rd.x + ro.z * rd.z + k * k * rd.y * (height - ro.y));
565 let c = ro.x * ro.x + ro.z * ro.z - k * k * (height - ro.y) * (height - ro.y);
566 let mut sheet = |t: f64| {
567 let y = ro.y + t * rd.y;
568 if (-T_EPS..=height + T_EPS).contains(&y) {
569 candidates.push((t, FaceTag::ConeSide));
570 }
571 };
572 if a.abs() > 1.0e-14 {
573 let disc = b * b - 4.0 * a * c;
574 if disc > 0.0 {
575 let sq = disc.sqrt();
576 sheet((-b - sq) / (2.0 * a));
577 sheet((-b + sq) / (2.0 * a));
578 }
579 } else if b.abs() > 1.0e-14 {
580 sheet(-c / b);
581 }
582 if rd.y != 0.0 {
584 let t = -ro.y / rd.y;
585 let p = ro + t * rd;
586 if p.x * p.x + p.z * p.z <= radius * radius + T_EPS {
587 candidates.push((t, FaceTag::ConeBottom));
588 }
589 }
590 candidates.sort_by(|x, y| x.0.total_cmp(&y.0));
591 candidates.dedup_by(|x, y| (x.0 - y.0).abs() < T_EPS);
592 candidates
594 .windows(2)
595 .find(|w| inside(0.5 * (w[0].0 + w[1].0)))
596 .map(|w| (w[0], w[1]))
597}
598
599fn leaf_contains(leaf: &GeomLeaf, p: DVec3) -> bool {
602 let p = leaf.world_inv.transform_point3(p);
603 match leaf.prim {
604 LeafPrim::Cube { size } => {
605 (0.0..=size.x).contains(&p.x)
606 && (0.0..=size.y).contains(&p.y)
607 && (0.0..=size.z).contains(&p.z)
608 }
609 LeafPrim::Aabb { min, max } => {
610 (min.x..=max.x).contains(&p.x)
611 && (min.y..=max.y).contains(&p.y)
612 && (min.z..=max.z).contains(&p.z)
613 }
614 LeafPrim::Sphere { radius } => p.length_squared() <= radius * radius,
615 LeafPrim::Cylinder { radius, height, .. } => {
616 (0.0..=height).contains(&p.y) && p.x * p.x + p.z * p.z <= radius * radius
617 }
618 LeafPrim::Cone { radius, height, .. } => {
619 if height <= 0.0 {
620 return false;
621 }
622 let k = radius / height;
623 (0.0..=height).contains(&p.y) && (p.x * p.x + p.z * p.z).sqrt() <= k * (height - p.y)
624 }
625 }
626}
627
628fn local_face_normal(prim: &LeafPrim, face: FaceTag, p: DVec3) -> DVec3 {
632 match face {
633 FaceTag::CubePosX => DVec3::X,
634 FaceTag::CubeNegX => -DVec3::X,
635 FaceTag::CubePosY | FaceTag::CylTop => DVec3::Y,
636 FaceTag::CubeNegY | FaceTag::CylBottom | FaceTag::ConeBottom => -DVec3::Y,
637 FaceTag::CubePosZ => DVec3::Z,
638 FaceTag::CubeNegZ => -DVec3::Z,
639 FaceTag::CylSide => DVec3::new(p.x, 0.0, p.z).normalize_or_zero(),
640 FaceTag::ConeSide => {
641 let LeafPrim::Cone { radius, height, .. } = prim else {
642 return DVec3::Y;
643 };
644 let rho = (p.x * p.x + p.z * p.z).sqrt();
645 if rho < 1.0e-12 {
646 return DVec3::Y; }
648 DVec3::new(p.x / rho, radius / height, p.z / rho).normalize()
650 }
651 FaceTag::Sphere => p.normalize_or_zero(),
652 FaceTag::Unknown => aabb_proxy_normal(prim, p),
653 }
654}
655
656fn aabb_proxy_normal(prim: &LeafPrim, p: DVec3) -> DVec3 {
659 let LeafPrim::Aabb { min, max } = prim else {
660 return DVec3::Y;
661 };
662 let mut best = (f64::INFINITY, DVec3::Y);
663 for axis in 0..3 {
664 let mut n = DVec3::ZERO;
665 n[axis] = -1.0;
666 if (p[axis] - min[axis]).abs() < best.0 {
667 best = ((p[axis] - min[axis]).abs(), n);
668 }
669 if (max[axis] - p[axis]).abs() < best.0 {
670 best = ((max[axis] - p[axis]).abs(), -n);
671 }
672 }
673 best.1
674}
675
676#[derive(Clone, Copy, PartialEq)]
679enum BoolOp {
680 Union,
681 Inter,
682 Diff,
683}
684
685fn merge_spans(a: Vec<HitSpan>, b: Vec<HitSpan>, op: BoolOp) -> Vec<HitSpan> {
688 match op {
690 BoolOp::Union if b.is_empty() => return a,
691 BoolOp::Union if a.is_empty() => return b,
692 BoolOp::Inter if a.is_empty() || b.is_empty() => return Vec::new(),
693 BoolOp::Diff if a.is_empty() || b.is_empty() => return a,
694 _ => {}
695 }
696
697 struct Event {
698 t: f64,
699 from_b: bool,
700 is_enter: bool,
701 boundary: HitBoundary,
702 }
703 let mut events: Vec<Event> = Vec::with_capacity(2 * (a.len() + b.len()));
704 for (spans, from_b) in [(&a, false), (&b, true)] {
705 for s in spans.iter() {
706 events.push(Event {
707 t: s.enter.t,
708 from_b,
709 is_enter: true,
710 boundary: s.enter,
711 });
712 events.push(Event {
713 t: s.exit.t,
714 from_b,
715 is_enter: false,
716 boundary: s.exit,
717 });
718 }
719 }
720 events.sort_by(|x, y| x.t.total_cmp(&y.t));
721
722 let combined = |ia: bool, ib: bool| match op {
723 BoolOp::Union => ia || ib,
724 BoolOp::Inter => ia && ib,
725 BoolOp::Diff => ia && !ib,
726 };
727 let mut inside_a = false;
728 let mut inside_b = false;
729 let mut inside = false;
730 let mut open: Option<HitBoundary> = None;
731 let mut out: Vec<HitSpan> = Vec::new();
732 for ev in events {
733 if ev.from_b {
734 inside_b = ev.is_enter;
735 } else {
736 inside_a = ev.is_enter;
737 }
738 let now = combined(inside_a, inside_b);
739 if now == inside {
740 continue;
741 }
742 inside = now;
743 let mut boundary = ev.boundary;
744 if op == BoolOp::Diff && ev.from_b {
745 boundary.flipped = !boundary.flipped;
746 }
747 if now {
748 open = Some(boundary);
749 } else if let Some(enter) = open.take() {
750 out.push(HitSpan {
751 enter,
752 exit: boundary,
753 });
754 }
755 }
756 let mut norm: Vec<HitSpan> = Vec::with_capacity(out.len());
759 for span in out {
760 if span.exit.t - span.enter.t <= T_EPS {
761 continue;
762 }
763 match norm.last_mut() {
764 Some(prev) if span.enter.t - prev.exit.t <= T_EPS => prev.exit = span.exit,
765 _ => norm.push(span),
766 }
767 }
768 norm
769}
770
771impl GeomLeaf {
774 pub fn face_outline(&self, face: FaceTag) -> Vec<Vec<DVec3>> {
778 let circle = |y: f64, r: f64| -> Vec<DVec3> {
779 (0..OUTLINE_SEGMENTS)
780 .map(|i| {
781 let a = core::f64::consts::TAU * i as f64 / OUTLINE_SEGMENTS as f64;
782 self.world
783 .transform_point3(DVec3::new(r * a.cos(), y, r * a.sin()))
784 })
785 .collect()
786 };
787 match (&self.prim, face) {
788 (LeafPrim::Cube { size }, tag) => {
789 let (axis, positive) = match tag {
790 FaceTag::CubePosX => (0, true),
791 FaceTag::CubeNegX => (0, false),
792 FaceTag::CubePosY => (1, true),
793 FaceTag::CubeNegY => (1, false),
794 FaceTag::CubePosZ => (2, true),
795 FaceTag::CubeNegZ => (2, false),
796 _ => return Vec::new(),
797 };
798 let (u, v) = ((axis + 1) % 3, (axis + 2) % 3);
799 let corners = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
800 let quad = corners
801 .iter()
802 .map(|&(cu, cv)| {
803 let mut p = DVec3::ZERO;
804 p[axis] = if positive { size[axis] } else { 0.0 };
805 p[u] = cu * size[u];
806 p[v] = cv * size[v];
807 self.world.transform_point3(p)
808 })
809 .collect();
810 vec![quad]
811 }
812 (LeafPrim::Cylinder { radius, height, .. }, FaceTag::CylTop) => {
813 vec![circle(*height, *radius)]
814 }
815 (LeafPrim::Cylinder { radius, .. }, FaceTag::CylBottom) => vec![circle(0.0, *radius)],
816 (LeafPrim::Cylinder { radius, height, .. }, FaceTag::CylSide) => {
817 vec![circle(0.0, *radius), circle(*height, *radius)]
818 }
819 (LeafPrim::Cone { radius, .. }, FaceTag::ConeBottom | FaceTag::ConeSide) => {
820 vec![circle(0.0, *radius)]
821 }
822 _ => Vec::new(),
823 }
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830
831 fn cube(size: [f64; 3]) -> DynamicObject {
832 DynamicObject::Cube { size }
833 }
834
835 fn unit_ray_x() -> (DVec3, DVec3) {
836 (DVec3::new(-10.0, 0.5, 0.5), DVec3::X)
837 }
838
839 fn leaves(obj: &DynamicObject) -> CsgLeaves {
840 CsgLeaves::from_dynamic(obj, DAffine3::IDENTITY, None)
841 }
842
843 #[test]
844 fn cube_axis_ray_hits_neg_then_pos_x() {
845 let csg = leaves(&cube([1.0, 1.0, 1.0]));
846 let spans = csg.spans(unit_ray_x().0, unit_ray_x().1);
847 assert_eq!(spans.len(), 1);
848 let s = spans[0];
849 assert!((s.enter.t - 10.0).abs() < 1e-12);
850 assert!((s.exit.t - 11.0).abs() < 1e-12);
851 assert_eq!(s.enter.face, FaceTag::CubeNegX);
852 assert_eq!(s.exit.face, FaceTag::CubePosX);
853 let n = csg.boundary_normal(&s.enter, unit_ray_x().0, unit_ray_x().1);
854 assert!((n - (-DVec3::X)).length() < 1e-12);
855 }
856
857 #[test]
858 fn cube_miss_and_axis_parallel_guard() {
859 let csg = leaves(&cube([1.0, 1.0, 1.0]));
860 assert!(csg.spans(DVec3::new(-10.0, 2.0, 0.5), DVec3::X).is_empty());
862 assert_eq!(csg.spans(DVec3::new(0.5, 0.5, -5.0), DVec3::Z).len(), 1);
864 }
865
866 #[test]
867 fn translated_rotated_cube_matches_renderer_convention() {
868 let obj = DynamicObject::Translate {
870 offset: [2.0, 0.0, 0.0],
871 child: Box::new(DynamicObject::Rotate {
872 angles: [0.0, 0.0, 90.0],
873 child: Box::new(cube([1.0, 2.0, 3.0])),
874 }),
875 };
876 let csg = leaves(&obj);
877 let ro = DVec3::new(1.0, 10.0, 1.5);
881 let spans = csg.spans(ro, -DVec3::Y);
882 assert_eq!(spans.len(), 1);
883 assert_eq!(spans[0].enter.face, FaceTag::CubePosX);
886 let n = csg.boundary_normal(&spans[0].enter, ro, -DVec3::Y);
887 assert!((n - DVec3::Y).length() < 1e-9);
888 }
889
890 #[test]
891 fn non_uniform_scale_keeps_world_t() {
892 let obj = DynamicObject::Scale {
893 factors: [2.0, 3.0, 4.0],
894 child: Box::new(cube([1.0, 1.0, 1.0])),
895 };
896 let csg = leaves(&obj);
897 let (ro, rd) = (DVec3::new(-10.0, 1.0, 1.0), DVec3::X);
898 let spans = csg.spans(ro, rd);
899 assert_eq!(spans.len(), 1);
900 assert!((spans[0].enter.t - 10.0).abs() < 1e-12);
901 assert!(
902 (spans[0].exit.t - 12.0).abs() < 1e-12,
903 "scaled span is 2 long"
904 );
905 }
906
907 #[test]
908 fn mirror_flips_geometry_not_normals_outwardness() {
909 let obj = DynamicObject::Mirror {
910 axis: [1.0, 0.0, 0.0],
911 child: Box::new(cube([1.0, 1.0, 1.0])),
912 };
913 let csg = leaves(&obj);
914 let (ro, rd) = (DVec3::new(-10.0, 0.5, 0.5), DVec3::X);
916 let spans = csg.spans(ro, rd);
917 assert_eq!(spans.len(), 1);
918 assert!((spans[0].enter.t - 9.0).abs() < 1e-12);
919 let n = csg.boundary_normal(&spans[0].enter, ro, rd);
920 assert!(
921 (n - (-DVec3::X)).length() < 1e-12,
922 "outward normal faces the ray"
923 );
924 }
925
926 #[test]
927 fn zero_mirror_axis_is_empty() {
928 let obj = DynamicObject::Mirror {
929 axis: [0.0, 0.0, 0.0],
930 child: Box::new(cube([1.0, 1.0, 1.0])),
931 };
932 assert!(leaves(&obj).is_empty());
933 }
934
935 #[test]
936 fn sphere_and_cylinder_intervals() {
937 let csg = leaves(&DynamicObject::Sphere {
938 radius: 2.0,
939 segments: 8,
940 stacks: 8,
941 });
942 let spans = csg.spans(DVec3::new(-10.0, 0.0, 0.0), DVec3::X);
943 assert_eq!(spans.len(), 1);
944 assert!((spans[0].enter.t - 8.0).abs() < 1e-12);
945 assert!((spans[0].exit.t - 12.0).abs() < 1e-12);
946
947 let cyl = leaves(&DynamicObject::Cylinder {
948 radius: 1.0,
949 height: 4.0,
950 segments: 8,
951 });
952 let spans = cyl.spans(DVec3::new(0.0, 10.0, 0.0), -DVec3::Y);
954 assert_eq!(spans.len(), 1);
955 assert_eq!(spans[0].enter.face, FaceTag::CylTop);
956 assert_eq!(spans[0].exit.face, FaceTag::CylBottom);
957 assert!((spans[0].enter.t - 6.0).abs() < 1e-12);
958 let spans = cyl.spans(DVec3::new(-5.0, 2.0, 0.0), DVec3::X);
960 assert_eq!(spans[0].enter.face, FaceTag::CylSide);
961 assert!((spans[0].enter.t - 4.0).abs() < 1e-12);
962 let n = cyl.boundary_normal(&spans[0].enter, DVec3::new(-5.0, 2.0, 0.0), DVec3::X);
963 assert!((n - (-DVec3::X)).length() < 1e-12);
964 }
965
966 #[test]
967 fn cone_side_and_base() {
968 let cone = leaves(&DynamicObject::Cone {
969 radius: 2.0,
970 height: 4.0,
971 segments: 8,
972 });
973 let (ro, rd) = (DVec3::new(-5.0, 2.0, 0.0), DVec3::X);
975 let spans = cone.spans(ro, rd);
976 assert_eq!(spans.len(), 1);
977 assert_eq!(spans[0].enter.face, FaceTag::ConeSide);
978 assert!((spans[0].enter.t - 4.0).abs() < 1e-9);
979 assert!((spans[0].exit.t - 6.0).abs() < 1e-9);
980 let n = cone.boundary_normal(&spans[0].enter, ro, rd);
981 let expect = DVec3::new(-1.0, 0.5, 0.0).normalize();
983 assert!((n - expect).length() < 1e-9);
984 let spans = cone.spans(DVec3::new(0.5, -5.0, 0.0), DVec3::Y);
986 assert_eq!(spans[0].enter.face, FaceTag::ConeBottom);
987 assert_eq!(spans[0].exit.face, FaceTag::ConeSide);
988 }
989
990 #[test]
991 fn union_merges_touching_and_overlapping() {
992 let obj = DynamicObject::Union {
993 children: vec![
994 cube([1.0, 1.0, 1.0]),
995 DynamicObject::Translate {
996 offset: [1.0, 0.0, 0.0],
997 child: Box::new(cube([1.0, 1.0, 1.0])),
998 },
999 ],
1000 };
1001 let csg = leaves(&obj);
1002 let spans = csg.spans(unit_ray_x().0, unit_ray_x().1);
1003 assert_eq!(spans.len(), 1, "touching cubes merge into one span");
1004 assert!((spans[0].exit.t - 12.0).abs() < 1e-12);
1005 }
1006
1007 #[test]
1008 fn difference_flips_subtrahend_normal() {
1009 let obj = DynamicObject::Difference {
1012 children: vec![
1013 cube([3.0, 1.0, 1.0]),
1014 DynamicObject::Translate {
1015 offset: [1.0, -0.5, -0.5],
1016 child: Box::new(cube([1.0, 2.0, 2.0])),
1017 },
1018 ],
1019 };
1020 let csg = leaves(&obj);
1021 let (ro, rd) = unit_ray_x();
1022 let spans = csg.spans(ro, rd);
1023 assert_eq!(spans.len(), 2, "plate is split by the through-slot");
1024 let exit = spans[0].exit;
1027 assert_eq!(exit.face, FaceTag::CubeNegX);
1028 assert!(exit.flipped);
1029 let n = csg.boundary_normal(&exit, ro, rd);
1030 assert!(
1031 (n - DVec3::X).length() < 1e-12,
1032 "flipped normal points into the slot (out of the material)"
1033 );
1034 assert!(csg.contains(DVec3::new(0.5, 0.5, 0.5)));
1036 assert!(!csg.contains(DVec3::new(1.5, 0.5, 0.5)));
1037 }
1038
1039 #[test]
1040 fn intersection_binds_to_tighter_solid() {
1041 let obj = DynamicObject::Intersection {
1042 children: vec![
1043 cube([2.0, 2.0, 2.0]),
1044 DynamicObject::Translate {
1045 offset: [1.0, 0.0, 0.0],
1046 child: Box::new(cube([2.0, 2.0, 2.0])),
1047 },
1048 ],
1049 };
1050 let csg = leaves(&obj);
1051 let (ro, rd) = (DVec3::new(-10.0, 1.0, 1.0), DVec3::X);
1052 let spans = csg.spans(ro, rd);
1053 assert_eq!(spans.len(), 1);
1054 assert!(
1055 (spans[0].enter.t - 11.0).abs() < 1e-12,
1056 "second cube binds entry"
1057 );
1058 assert!(
1059 (spans[0].exit.t - 12.0).abs() < 1e-12,
1060 "first cube binds exit"
1061 );
1062 }
1063
1064 #[test]
1065 fn empty_leaves_and_2d_are_invisible() {
1066 assert!(leaves(&DynamicObject::Empty).is_empty());
1067 assert!(leaves(&DynamicObject::Square { size: [1.0, 1.0] }).is_empty());
1068 let diff = DynamicObject::Difference { children: vec![] };
1069 assert!(
1070 leaves(&diff)
1071 .spans(unit_ray_x().0, unit_ray_x().1)
1072 .is_empty()
1073 );
1074 }
1075
1076 #[test]
1077 fn unsupported_subtree_uses_aabb_hint() {
1078 let extrude = DynamicObject::LinearExtrude {
1079 height: 2.0,
1080 twist: 0.0,
1081 scale: 1.0,
1082 slices: 1,
1083 child: Box::new(DynamicObject::Square { size: [1.0, 1.0] }),
1084 };
1085 assert!(leaves(&extrude).is_empty());
1087 let hint = |obj: &DynamicObject| match obj {
1089 DynamicObject::LinearExtrude { height, .. } => {
1090 Some((DVec3::ZERO, DVec3::new(1.0, *height, 1.0)))
1091 }
1092 _ => None,
1093 };
1094 let csg = CsgLeaves::from_dynamic(&extrude, DAffine3::IDENTITY, Some(&hint));
1095 let spans = csg.spans(DVec3::new(-5.0, 0.5, 0.5), DVec3::X);
1096 assert_eq!(spans.len(), 1);
1097 assert_eq!(spans[0].enter.face, FaceTag::Unknown);
1098 assert!(!spans[0].enter.face.is_selectable());
1099 }
1100
1101 #[test]
1102 fn branch_paths_skip_single_child_wrappers() {
1103 let obj = DynamicObject::Union {
1105 children: vec![
1106 cube([1.0; 3]),
1107 DynamicObject::Translate {
1108 offset: [5.0, 0.0, 0.0],
1109 child: Box::new(DynamicObject::Rotate {
1110 angles: [0.0, 45.0, 0.0],
1111 child: Box::new(cube([1.0; 3])),
1112 }),
1113 },
1114 ],
1115 };
1116 let csg = leaves(&obj);
1117 assert_eq!(csg.paths[0], LeafPath(vec![0]));
1118 assert_eq!(csg.paths[1], LeafPath(vec![1]));
1119 let single = DynamicObject::Difference {
1121 children: vec![cube([1.0; 3])],
1122 };
1123 let csg = leaves(&single);
1124 assert_eq!(csg.paths[0], LeafPath(Vec::new()));
1125 }
1126
1127 #[test]
1128 fn first_hit_skips_behind_camera() {
1129 let csg = leaves(&cube([1.0, 1.0, 1.0]));
1130 let hit = csg.first_hit(DVec3::splat(0.5), DVec3::X, 0.0).unwrap();
1132 assert_eq!(hit.face, FaceTag::CubePosX);
1133 }
1134
1135 #[test]
1136 fn face_outline_quad_and_circles() {
1137 let csg = leaves(&cube([1.0, 2.0, 3.0]));
1138 let outline = csg.leaves[0].face_outline(FaceTag::CubePosX);
1139 assert_eq!(outline.len(), 1);
1140 assert_eq!(outline[0].len(), 4);
1141 assert!(outline[0].iter().all(|p| (p.x - 1.0).abs() < 1e-12));
1142
1143 let cyl = leaves(&DynamicObject::Cylinder {
1144 radius: 1.0,
1145 height: 2.0,
1146 segments: 8,
1147 });
1148 assert_eq!(cyl.leaves[0].face_outline(FaceTag::CylSide).len(), 2);
1149 assert_eq!(cyl.leaves[0].face_outline(FaceTag::CylTop).len(), 1);
1150
1151 let sph = leaves(&DynamicObject::Sphere {
1152 radius: 1.0,
1153 segments: 8,
1154 stacks: 8,
1155 });
1156 assert!(sph.leaves[0].face_outline(FaceTag::Sphere).is_empty());
1157 }
1158}