1use super::types::{CsgOp, CsgTree, MarchingCell, PlaneSide};
6
7type VertsTris = (Vec<[f64; 3]>, Vec<[usize; 3]>);
9type PointNormals = Vec<([f64; 3], [f64; 3])>;
11type SurfaceAabb = ([f64; 3], [f64; 3]);
13
14#[inline]
15pub(super) fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
16 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
17}
18#[inline]
19pub(super) fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
20 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
21}
22#[inline]
23pub(super) fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
24 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
25}
26#[inline]
27pub(super) fn scale(a: [f64; 3], t: f64) -> [f64; 3] {
28 [a[0] * t, a[1] * t, a[2] * t]
29}
30#[inline]
31pub(super) fn length(a: [f64; 3]) -> f64 {
32 dot(a, a).sqrt()
33}
34#[inline]
35pub(super) fn normalize(a: [f64; 3]) -> [f64; 3] {
36 let l = length(a);
37 if l > 1e-15 {
38 scale(a, 1.0 / l)
39 } else {
40 [0.0, 0.0, 0.0]
41 }
42}
43
44#[inline]
45pub(super) fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
46 x.max(lo).min(hi)
47}
48#[inline]
49pub(super) fn mix(a: f64, b: f64, t: f64) -> f64 {
50 a * (1.0 - t) + b * t
51}
52#[inline]
53pub(super) fn lerp3(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
54 [
55 a[0] + t * (b[0] - a[0]),
56 a[1] + t * (b[1] - a[1]),
57 a[2] + t * (b[2] - a[2]),
58 ]
59}
60pub trait ImplicitSurface: Send + Sync {
66 fn sdf(&self, p: [f64; 3]) -> f64;
68 fn gradient(&self, p: [f64; 3]) -> [f64; 3];
70}
71pub fn sphere_march(
79 surface: &dyn ImplicitSurface,
80 ray_origin: [f64; 3],
81 ray_dir: [f64; 3],
82 max_dist: f64,
83 max_steps: usize,
84) -> Option<([f64; 3], [f64; 3])> {
85 pub(super) const HIT_EPS: f64 = 1e-5;
86 let dir = normalize(ray_dir);
87 let mut t = 0.0_f64;
88 for _ in 0..max_steps {
89 let p = add(ray_origin, scale(dir, t));
90 let d = surface.sdf(p);
91 if d.abs() < HIT_EPS {
92 let normal = central_diff_normal(surface, p);
93 return Some((p, normal));
94 }
95 if d < 0.0 {
96 t -= d.abs();
97 let p2 = add(ray_origin, scale(dir, t));
98 let normal = central_diff_normal(surface, p2);
99 return Some((p2, normal));
100 }
101 t += d;
102 if t > max_dist {
103 return None;
104 }
105 }
106 None
107}
108pub fn sphere_march_relaxed(
112 surface: &dyn ImplicitSurface,
113 ray_origin: [f64; 3],
114 ray_dir: [f64; 3],
115 max_dist: f64,
116 max_steps: usize,
117 omega: f64,
118) -> Option<([f64; 3], [f64; 3])> {
119 pub(super) const HIT_EPS: f64 = 1e-5;
120 let dir = normalize(ray_dir);
121 let mut t = 0.0_f64;
122 let mut prev_d = f64::MAX;
123 for _ in 0..max_steps {
124 let p = add(ray_origin, scale(dir, t));
125 let d = surface.sdf(p);
126 if d.abs() < HIT_EPS {
127 let normal = central_diff_normal(surface, p);
128 return Some((p, normal));
129 }
130 if d < 0.0 {
131 t -= d.abs();
132 let p2 = add(ray_origin, scale(dir, t));
133 let normal = central_diff_normal(surface, p2);
134 return Some((p2, normal));
135 }
136 let step = if d < prev_d { d * omega } else { d };
137 t += step;
138 prev_d = d;
139 if t > max_dist {
140 return None;
141 }
142 }
143 None
144}
145pub(super) fn central_diff_normal(surface: &dyn ImplicitSurface, p: [f64; 3]) -> [f64; 3] {
147 pub(super) const EPS: f64 = 1e-4;
148 let dx = surface.sdf([p[0] + EPS, p[1], p[2]]) - surface.sdf([p[0] - EPS, p[1], p[2]]);
149 let dy = surface.sdf([p[0], p[1] + EPS, p[2]]) - surface.sdf([p[0], p[1] - EPS, p[2]]);
150 let dz = surface.sdf([p[0], p[1], p[2] + EPS]) - surface.sdf([p[0], p[1], p[2] - EPS]);
151 normalize([dx, dy, dz])
152}
153pub fn csg_ray_test(
157 surface: &dyn ImplicitSurface,
158 ray_origin: [f64; 3],
159 ray_dir: [f64; 3],
160 max_dist: f64,
161) -> Option<f64> {
162 let dir = normalize(ray_dir);
163 sphere_march(surface, ray_origin, dir, max_dist, 256).map(|(hit, _)| {
164 let d = sub(hit, ray_origin);
165 length(d)
166 })
167}
168pub fn csg_contains_point(surface: &dyn ImplicitSurface, point: [f64; 3]) -> bool {
170 surface.sdf(point) < 0.0
171}
172pub fn classify_point(point: [f64; 3], normal: [f64; 3], d: f64, tolerance: f64) -> PlaneSide {
174 let dist = dot(point, normal) - d;
175 if dist > tolerance {
176 PlaneSide::Front
177 } else if dist < -tolerance {
178 PlaneSide::Back
179 } else {
180 PlaneSide::OnPlane
181 }
182}
183pub fn classify_triangle(
187 v0: [f64; 3],
188 v1: [f64; 3],
189 v2: [f64; 3],
190 normal: [f64; 3],
191 d: f64,
192 tolerance: f64,
193) -> (usize, usize, usize) {
194 let sides = [
195 classify_point(v0, normal, d, tolerance),
196 classify_point(v1, normal, d, tolerance),
197 classify_point(v2, normal, d, tolerance),
198 ];
199 let front = sides.iter().filter(|&&s| s == PlaneSide::Front).count();
200 let back = sides.iter().filter(|&&s| s == PlaneSide::Back).count();
201 let on = sides.iter().filter(|&&s| s == PlaneSide::OnPlane).count();
202 (front, back, on)
203}
204pub fn clip_polygon_front(vertices: &[[f64; 3]], normal: [f64; 3], d: f64) -> Vec<[f64; 3]> {
209 if vertices.is_empty() {
210 return vec![];
211 }
212 let mut output = Vec::new();
213 let n = vertices.len();
214 for i in 0..n {
215 let current = vertices[i];
216 let next = vertices[(i + 1) % n];
217 let d_current = dot(current, normal) - d;
218 let d_next = dot(next, normal) - d;
219 if d_current >= 0.0 {
220 output.push(current);
221 if d_next < 0.0 {
222 let t = d_current / (d_current - d_next);
223 output.push(lerp3(current, next, t));
224 }
225 } else if d_next >= 0.0 {
226 let t = d_current / (d_current - d_next);
227 output.push(lerp3(current, next, t));
228 }
229 }
230 output
231}
232pub fn clip_polygon_back(vertices: &[[f64; 3]], normal: [f64; 3], d: f64) -> Vec<[f64; 3]> {
234 clip_polygon_front(vertices, [-normal[0], -normal[1], -normal[2]], -d)
235}
236pub fn extract_mesh_simple(
242 surface: &dyn ImplicitSurface,
243 bounds_min: [f64; 3],
244 bounds_max: [f64; 3],
245 resolution: usize,
246) -> VertsTris {
247 let n = resolution.max(2);
248 let step = [
249 (bounds_max[0] - bounds_min[0]) / n as f64,
250 (bounds_max[1] - bounds_min[1]) / n as f64,
251 (bounds_max[2] - bounds_min[2]) / n as f64,
252 ];
253 let mut vertices = Vec::new();
254 let mut triangles = Vec::new();
255 for ix in 0..n {
256 for iy in 0..n {
257 for iz in 0..n {
258 let x0 = bounds_min[0] + ix as f64 * step[0];
259 let y0 = bounds_min[1] + iy as f64 * step[1];
260 let z0 = bounds_min[2] + iz as f64 * step[2];
261 let corners = [
262 [x0, y0, z0],
263 [x0 + step[0], y0, z0],
264 [x0 + step[0], y0 + step[1], z0],
265 [x0, y0 + step[1], z0],
266 ];
267 let vals: Vec<f64> = corners.iter().map(|&c| surface.sdf(c)).collect();
268 let edges = [(0, 1), (1, 2), (2, 3), (3, 0)];
269 let mut edge_verts = Vec::new();
270 for &(a, b) in &edges {
271 if vals[a].signum() != vals[b].signum() {
272 let t = vals[a] / (vals[a] - vals[b]);
273 let point = lerp3(corners[a], corners[b], t);
274 let vi = vertices.len();
275 vertices.push(point);
276 edge_verts.push(vi);
277 }
278 }
279 if edge_verts.len() >= 3 {
280 for i in 1..edge_verts.len() - 1 {
281 triangles.push([edge_verts[0], edge_verts[i], edge_verts[i + 1]]);
282 }
283 }
284 }
285 }
286 }
287 (vertices, triangles)
288}
289pub fn sample_surface_points(
298 surface: &dyn ImplicitSurface,
299 bounds_min: [f64; 3],
300 bounds_max: [f64; 3],
301 resolution: usize,
302) -> Vec<[f64; 3]> {
303 let n = resolution.max(1);
304 let step = [
305 (bounds_max[0] - bounds_min[0]) / n as f64,
306 (bounds_max[1] - bounds_min[1]) / n as f64,
307 (bounds_max[2] - bounds_min[2]) / n as f64,
308 ];
309 let threshold = step[0].max(step[1]).max(step[2]) * 0.55;
310 let mut points = Vec::new();
311 for ix in 0..=n {
312 let x = bounds_min[0] + ix as f64 * step[0];
313 for iy in 0..=n {
314 let y = bounds_min[1] + iy as f64 * step[1];
315 for iz in 0..=n {
316 let z = bounds_min[2] + iz as f64 * step[2];
317 let p = [x, y, z];
318 if surface.sdf(p).abs() < threshold {
319 points.push(p);
320 }
321 }
322 }
323 }
324 points
325}
326pub fn sample_surface_points_with_normals(
330 surface: &dyn ImplicitSurface,
331 bounds_min: [f64; 3],
332 bounds_max: [f64; 3],
333 resolution: usize,
334) -> PointNormals {
335 let points = sample_surface_points(surface, bounds_min, bounds_max, resolution);
336 points
337 .into_iter()
338 .map(|p| {
339 let n = surface.gradient(p);
340 (p, n)
341 })
342 .collect()
343}
344pub fn estimate_volume(
348 surface: &dyn ImplicitSurface,
349 bounds_min: [f64; 3],
350 bounds_max: [f64; 3],
351 resolution: usize,
352) -> f64 {
353 let n = resolution.max(1);
354 let step = [
355 (bounds_max[0] - bounds_min[0]) / n as f64,
356 (bounds_max[1] - bounds_min[1]) / n as f64,
357 (bounds_max[2] - bounds_min[2]) / n as f64,
358 ];
359 let cell_volume = step[0] * step[1] * step[2];
360 let mut count = 0usize;
361 for ix in 0..=n {
362 let x = bounds_min[0] + ix as f64 * step[0];
363 for iy in 0..=n {
364 let y = bounds_min[1] + iy as f64 * step[1];
365 for iz in 0..=n {
366 let z = bounds_min[2] + iz as f64 * step[2];
367 if surface.sdf([x, y, z]) < 0.0 {
368 count += 1;
369 }
370 }
371 }
372 }
373 count as f64 * cell_volume
374}
375pub fn csg_sdf(node: &CsgTree, p: [f64; 3]) -> f64 {
380 match node {
381 CsgTree::Leaf(s) => s.sdf(p),
382 CsgTree::Node { op, left, right } => {
383 let da = csg_sdf(left, p);
384 let db = csg_sdf(right, p);
385 match op {
386 CsgOp::Union => da.min(db),
387 CsgOp::Intersection => da.max(db),
388 CsgOp::Difference => da.max(-db),
389 }
390 }
391 }
392}
393pub fn csg_gradient(node: &CsgTree, p: [f64; 3]) -> [f64; 3] {
397 pub(super) const EPS: f64 = 1e-4;
398 let dx = csg_sdf(node, [p[0] + EPS, p[1], p[2]]) - csg_sdf(node, [p[0] - EPS, p[1], p[2]]);
399 let dy = csg_sdf(node, [p[0], p[1] + EPS, p[2]]) - csg_sdf(node, [p[0], p[1] - EPS, p[2]]);
400 let dz = csg_sdf(node, [p[0], p[1], p[2] + EPS]) - csg_sdf(node, [p[0], p[1], p[2] - EPS]);
401 normalize([dx, dy, dz])
402}
403pub fn csg_ray_march(
409 node: &CsgTree,
410 origin: [f64; 3],
411 dir: [f64; 3],
412 max_dist: f64,
413 max_steps: usize,
414) -> Option<f64> {
415 pub(super) const HIT_EPS: f64 = 1e-4;
416 let d = normalize(dir);
417 let mut t = 0.0_f64;
418 for _ in 0..max_steps {
419 let p = add(origin, scale(d, t));
420 let dist = csg_sdf(node, p);
421 if dist.abs() < HIT_EPS {
422 return Some(t);
423 }
424 if dist < 0.0 {
425 return Some(t);
426 }
427 t += dist;
428 if t > max_dist {
429 return None;
430 }
431 }
432 None
433}
434pub fn csg_tree_contains(node: &CsgTree, p: [f64; 3]) -> bool {
436 csg_sdf(node, p) < 0.0
437}
438pub fn csg_surface_area(
448 node: &CsgTree,
449 bbox_min: [f64; 3],
450 bbox_max: [f64; 3],
451 n_div: usize,
452) -> f64 {
453 let n = n_div.max(2);
454 let step = [
455 (bbox_max[0] - bbox_min[0]) / n as f64,
456 (bbox_max[1] - bbox_min[1]) / n as f64,
457 (bbox_max[2] - bbox_min[2]) / n as f64,
458 ];
459 let cell_area_x = step[1] * step[2];
460 let cell_area_y = step[0] * step[2];
461 let cell_area_z = step[0] * step[1];
462 let mut area = 0.0_f64;
463 for ix in 0..n {
464 for iy in 0..n {
465 for iz in 0..n {
466 let p = [
467 bbox_min[0] + (ix as f64 + 0.5) * step[0],
468 bbox_min[1] + (iy as f64 + 0.5) * step[1],
469 bbox_min[2] + (iz as f64 + 0.5) * step[2],
470 ];
471 let d = csg_sdf(node, p);
472 if ix + 1 < n {
473 let pn = [p[0] + step[0], p[1], p[2]];
474 let dn = csg_sdf(node, pn);
475 if d * dn < 0.0 {
476 area += cell_area_x;
477 }
478 }
479 if iy + 1 < n {
480 let pn = [p[0], p[1] + step[1], p[2]];
481 let dn = csg_sdf(node, pn);
482 if d * dn < 0.0 {
483 area += cell_area_y;
484 }
485 }
486 if iz + 1 < n {
487 let pn = [p[0], p[1], p[2] + step[2]];
488 let dn = csg_sdf(node, pn);
489 if d * dn < 0.0 {
490 area += cell_area_z;
491 }
492 }
493 }
494 }
495 }
496 area
497}
498pub fn csg_offset_sdf(node: &CsgTree, p: [f64; 3], offset: f64) -> f64 {
502 csg_sdf(node, p) - offset
503}
504pub fn csg_offset_contains(node: &CsgTree, p: [f64; 3], offset: f64) -> bool {
506 csg_offset_sdf(node, p, offset) < 0.0
507}
508pub fn csg_offset_gradient(node: &CsgTree, p: [f64; 3]) -> [f64; 3] {
511 csg_gradient(node, p)
512}
513pub fn csg_tree_simplify_check(node: &CsgTree, probe_center: [f64; 3]) -> bool {
529 match node {
530 CsgTree::Leaf(_) => true,
531 CsgTree::Node { op, left, right } => {
532 let da = csg_sdf(left, probe_center);
533 let db = csg_sdf(right, probe_center);
534 match op {
535 CsgOp::Union => da < 0.0 || db < 0.0,
536 CsgOp::Intersection => da < 0.0 && db < 0.0,
537 CsgOp::Difference => da < 0.0 && db >= 0.0,
538 }
539 }
540 }
541}
542pub fn csg_node_surface_area(node: &CsgTree, bbox_min: [f64; 3], bbox_max: [f64; 3]) -> f64 {
546 csg_surface_area(node, bbox_min, bbox_max, 16)
547}
548#[cfg(test)]
549mod tests {
550 use super::*;
551 use crate::csg::CsgDifference;
552 use crate::csg::CsgIntersection;
553 use crate::csg::CsgSmoothDifference;
554 use crate::csg::CsgSmoothIntersection;
555 use crate::csg::CsgSmoothUnion;
556
557 use crate::csg::CsgUnion;
558 use crate::csg::SdfBox;
559 use crate::csg::SdfCylinder;
560 use crate::csg::SdfSphere;
561 use crate::csg::SdfTorus;
562
563 #[test]
564 fn sphere_sdf_center_is_minus_radius() {
565 let s = SdfSphere::new([0.0, 0.0, 0.0], 2.0);
566 assert!((s.sdf([0.0, 0.0, 0.0]) - (-2.0)).abs() < 1e-12);
567 }
568 #[test]
569 fn sphere_sdf_on_surface_is_zero() {
570 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.5);
571 assert!(s.sdf([1.5, 0.0, 0.0]).abs() < 1e-12);
572 }
573 #[test]
574 fn sphere_sdf_outside_is_positive() {
575 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
576 assert!(s.sdf([3.0, 0.0, 0.0]) > 0.0);
577 }
578 #[test]
579 fn union_inside_either_sphere_is_negative() {
580 let a = Box::new(SdfSphere::new([-2.0, 0.0, 0.0], 1.0));
581 let b = Box::new(SdfSphere::new([2.0, 0.0, 0.0], 1.0));
582 let u = CsgUnion::new(a, b);
583 assert!(u.sdf([-2.0, 0.0, 0.0]) < 0.0);
584 assert!(u.sdf([2.0, 0.0, 0.0]) < 0.0);
585 assert!(u.sdf([0.0, 0.0, 0.0]) > 0.0);
586 }
587 #[test]
588 fn intersection_only_inside_both_is_negative() {
589 let a = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 2.0));
590 let b = Box::new(SdfSphere::new([1.0, 0.0, 0.0], 2.0));
591 let i = CsgIntersection::new(a, b);
592 assert!(i.sdf([0.5, 0.0, 0.0]) < 0.0);
593 assert!(i.sdf([-1.5, 0.0, 0.0]) > 0.0);
594 assert!(i.sdf([2.5, 0.0, 0.0]) > 0.0);
595 }
596 #[test]
597 fn difference_carved_out_region_is_positive() {
598 let a = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
599 let b = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 1.5));
600 let d = CsgDifference::new(a, b);
601 assert!(d.sdf([0.0, 0.0, 0.0]) > 0.0);
602 assert!(d.sdf([2.5, 0.0, 0.0]) < 0.0);
603 assert!(d.sdf([5.0, 0.0, 0.0]) > 0.0);
604 }
605 #[test]
606 fn smooth_union_blends_surfaces() {
607 let a = Box::new(SdfSphere::new([-0.5, 0.0, 0.0], 1.0));
608 let b = Box::new(SdfSphere::new([0.5, 0.0, 0.0], 1.0));
609 let su = CsgSmoothUnion::new(a, b, 0.5);
610 let sharp_a = Box::new(SdfSphere::new([-0.5, 0.0, 0.0], 1.0));
611 let sharp_b = Box::new(SdfSphere::new([0.5, 0.0, 0.0], 1.0));
612 let sharp = CsgUnion::new(sharp_a, sharp_b);
613 assert!(su.sdf([0.0, 0.0, 0.0]) <= sharp.sdf([0.0, 0.0, 0.0]) + 1e-10);
614 }
615 #[test]
616 fn smooth_intersection_blends() {
617 let a = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 2.0));
618 let b = Box::new(SdfSphere::new([1.0, 0.0, 0.0], 2.0));
619 let si = CsgSmoothIntersection::new(a, b, 0.3);
620 assert!(si.sdf([0.5, 0.0, 0.0]) < 0.0);
621 }
622 #[test]
623 fn smooth_difference_carves() {
624 let a = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
625 let b = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
626 let sd = CsgSmoothDifference::new(a, b, 0.2);
627 assert!(sd.sdf([0.0, 0.0, 0.0]) > 0.0);
628 assert!(sd.sdf([2.5, 0.0, 0.0]) < 0.0);
629 }
630 #[test]
631 fn sphere_march_hits_sphere() {
632 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
633 let origin = [-5.0, 0.0, 0.0];
634 let dir = [1.0, 0.0, 0.0];
635 let result = sphere_march(&s, origin, dir, 100.0, 256);
636 assert!(result.is_some(), "expected a hit");
637 let (hit, _normal) = result.unwrap();
638 assert!(
639 s.sdf(hit).abs() < 1e-4,
640 "hit point not on surface: sdf={}",
641 s.sdf(hit)
642 );
643 }
644 #[test]
645 fn sphere_march_misses_sphere() {
646 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
647 let origin = [-5.0, 5.0, 0.0];
648 let dir = [1.0, 0.0, 0.0];
649 let result = sphere_march(&s, origin, dir, 20.0, 256);
650 assert!(result.is_none());
651 }
652 #[test]
653 fn sphere_march_relaxed_hits() {
654 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
655 let result = sphere_march_relaxed(&s, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0, 256, 1.5);
656 assert!(result.is_some());
657 }
658 #[test]
659 fn box_sdf_corner_region() {
660 let b = SdfBox::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
661 assert!(b.sdf([0.0, 0.0, 0.0]) < 0.0);
662 assert!(b.sdf([1.0, 0.0, 0.0]).abs() < 1e-12);
663 let p = [2.0, 2.0, 2.0];
664 let expected = ((1.0_f64).powi(2) * 3.0).sqrt();
665 assert!((b.sdf(p) - expected).abs() < 1e-10);
666 }
667 #[test]
668 fn cylinder_sdf_on_axis() {
669 let c = SdfCylinder::new([0.0, 0.0, 0.0], 1.0);
670 assert!((c.sdf([0.0, 0.0, 0.0]) - (-1.0)).abs() < 1e-12);
671 }
672 #[test]
673 fn cylinder_sdf_on_surface() {
674 let c = SdfCylinder::new([0.0, 0.0, 0.0], 1.0);
675 assert!(c.sdf([1.0, 5.0, 0.0]).abs() < 1e-12);
676 }
677 #[test]
678 fn torus_sdf_on_surface() {
679 let t = SdfTorus::new(2.0, 0.5);
680 assert!(t.sdf([2.5, 0.0, 0.0]).abs() < 1e-12);
681 assert!(t.sdf([1.5, 0.0, 0.0]).abs() < 1e-12);
682 }
683 #[test]
684 fn sample_surface_points_returns_nonempty_for_sphere() {
685 let s = SdfSphere::new([0.0, 0.0, 0.0], 0.5);
686 let pts = sample_surface_points(&s, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], 20);
687 assert!(!pts.is_empty(), "expected surface points, got none");
688 }
689 #[test]
690 fn sample_surface_points_with_normals_has_unit_normals() {
691 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
692 let pts = sample_surface_points_with_normals(&s, [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5], 10);
693 for (_, n) in &pts {
694 let len = length(*n);
695 assert!((len - 1.0).abs() < 0.1, "normal length = {len}");
696 }
697 }
698 #[test]
699 fn estimate_volume_sphere() {
700 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
701 let vol = estimate_volume(&s, [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5], 20);
702 let expected = 4.0 / 3.0 * std::f64::consts::PI;
703 assert!(
704 (vol - expected).abs() < 1.0,
705 "estimated volume = {vol}, expected ~ {expected}"
706 );
707 }
708 #[test]
709 fn csg_ray_test_hits() {
710 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
711 let dist = csg_ray_test(&s, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
712 assert!(dist.is_some());
713 let d = dist.unwrap();
714 assert!((d - 4.0).abs() < 0.1, "hit distance = {d}");
715 }
716 #[test]
717 fn csg_contains_point_inside() {
718 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
719 assert!(csg_contains_point(&s, [0.0, 0.0, 0.0]));
720 assert!(!csg_contains_point(&s, [2.0, 0.0, 0.0]));
721 }
722 #[test]
723 fn classify_point_front_back() {
724 let normal = [0.0, 1.0, 0.0];
725 let d = 0.0;
726 assert_eq!(
727 classify_point([0.0, 1.0, 0.0], normal, d, 1e-6),
728 PlaneSide::Front
729 );
730 assert_eq!(
731 classify_point([0.0, -1.0, 0.0], normal, d, 1e-6),
732 PlaneSide::Back
733 );
734 assert_eq!(
735 classify_point([0.0, 0.0, 0.0], normal, d, 1e-6),
736 PlaneSide::OnPlane
737 );
738 }
739 #[test]
740 fn classify_triangle_straddling() {
741 let normal = [0.0, 1.0, 0.0];
742 let d = 0.0;
743 let (f, b, _on) = classify_triangle(
744 [0.0, 1.0, 0.0],
745 [0.0, -1.0, 0.0],
746 [1.0, 0.0, 0.0],
747 normal,
748 d,
749 1e-6,
750 );
751 assert_eq!(f, 1);
752 assert_eq!(b, 1);
753 }
754 #[test]
755 fn clip_polygon_front_basic() {
756 let tri = [[0.0, 1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, -1.0, 0.0]];
757 let result = clip_polygon_front(&tri, [0.0, 1.0, 0.0], 0.0);
758 assert!(
759 result.len() >= 3,
760 "clipped polygon has {} verts",
761 result.len()
762 );
763 for v in &result {
764 assert!(dot(*v, [0.0, 1.0, 0.0]) >= -1e-10);
765 }
766 }
767 #[test]
768 fn clip_polygon_back_basic() {
769 let tri = [[0.0, 1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, -1.0, 0.0]];
770 let result = clip_polygon_back(&tri, [0.0, 1.0, 0.0], 0.0);
771 assert!(result.len() >= 3);
772 for v in &result {
773 assert!(dot(*v, [0.0, 1.0, 0.0]) <= 1e-10);
774 }
775 }
776 #[test]
777 fn extract_mesh_simple_nonempty() {
778 let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
779 let (verts, _tris) = extract_mesh_simple(&s, [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5], 20);
780 assert!(!verts.is_empty(), "mesh has no vertices");
781 }
782}
783#[cfg(test)]
784mod tests_csg_tree {
785
786 use crate::csg::CsgTree;
787
788 use crate::csg::SdfSphere;
789
790 use crate::csg::csg_gradient;
791 use crate::csg::csg_ray_march;
792 use crate::csg::csg_sdf;
793 use crate::csg::csg_tree_contains;
794 #[test]
795 fn csg_tree_leaf_sphere_sdf() {
796 let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
797 assert!(csg_sdf(&tree, [0.0, 0.0, 0.0]) < 0.0);
798 assert!(csg_sdf(&tree, [3.0, 0.0, 0.0]) > 0.0);
799 assert!(csg_sdf(&tree, [1.0, 0.0, 0.0]).abs() < 1e-12);
800 }
801 #[test]
802 fn csg_tree_union_inside_either() {
803 let a = CsgTree::leaf(SdfSphere::new([-2.0, 0.0, 0.0], 1.0));
804 let b = CsgTree::leaf(SdfSphere::new([2.0, 0.0, 0.0], 1.0));
805 let u = CsgTree::union(a, b);
806 assert!(csg_sdf(&u, [-2.0, 0.0, 0.0]) < 0.0);
807 assert!(csg_sdf(&u, [2.0, 0.0, 0.0]) < 0.0);
808 assert!(csg_sdf(&u, [0.0, 0.0, 0.0]) > 0.0);
809 }
810 #[test]
811 fn csg_tree_intersection_only_shared_region() {
812 let a = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 2.0));
813 let b = CsgTree::leaf(SdfSphere::new([1.0, 0.0, 0.0], 2.0));
814 let i = CsgTree::intersection(a, b);
815 assert!(csg_sdf(&i, [0.5, 0.0, 0.0]) < 0.0);
816 assert!(csg_sdf(&i, [-1.5, 0.0, 0.0]) > 0.0);
817 assert!(csg_sdf(&i, [2.5, 0.0, 0.0]) > 0.0);
818 }
819 #[test]
820 fn csg_tree_difference_carves_out() {
821 let a = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
822 let b = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.5));
823 let d = CsgTree::difference(a, b);
824 assert!(csg_sdf(&d, [0.0, 0.0, 0.0]) > 0.0);
825 assert!(csg_sdf(&d, [2.5, 0.0, 0.0]) < 0.0);
826 assert!(csg_sdf(&d, [5.0, 0.0, 0.0]) > 0.0);
827 }
828 #[test]
829 fn csg_tree_nested_union_of_three_spheres() {
830 let a = CsgTree::leaf(SdfSphere::new([-3.0, 0.0, 0.0], 1.0));
831 let b = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
832 let c = CsgTree::leaf(SdfSphere::new([3.0, 0.0, 0.0], 1.0));
833 let ab = CsgTree::union(a, b);
834 let abc = CsgTree::union(ab, c);
835 assert!(csg_sdf(&abc, [-3.0, 0.0, 0.0]) < 0.0);
836 assert!(csg_sdf(&abc, [0.0, 0.0, 0.0]) < 0.0);
837 assert!(csg_sdf(&abc, [3.0, 0.0, 0.0]) < 0.0);
838 assert!(csg_sdf(&abc, [1.5, 0.0, 0.0]) > 0.0);
839 }
840 #[test]
841 fn csg_gradient_leaf_sphere_is_unit() {
842 let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
843 let g = csg_gradient(&tree, [2.0, 0.0, 0.0]);
844 let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
845 assert!((len - 1.0).abs() < 0.01, "gradient length = {len}");
846 }
847 #[test]
848 fn csg_ray_march_hits_sphere() {
849 let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
850 let t = csg_ray_march(&tree, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0, 256);
851 assert!(t.is_some(), "should hit sphere");
852 let t = t.unwrap();
853 assert!((t - 4.0).abs() < 0.1, "expected t≈4, got {t}");
854 }
855 #[test]
856 fn csg_ray_march_misses_sphere() {
857 let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
858 let t = csg_ray_march(&tree, [-5.0, 10.0, 0.0], [1.0, 0.0, 0.0], 20.0, 256);
859 assert!(t.is_none(), "should miss sphere");
860 }
861 #[test]
862 fn csg_ray_march_union_hits_either() {
863 let a = CsgTree::leaf(SdfSphere::new([-5.0, 0.0, 0.0], 1.0));
864 let b = CsgTree::leaf(SdfSphere::new([5.0, 0.0, 0.0], 1.0));
865 let u = CsgTree::union(a, b);
866 let t = csg_ray_march(&u, [-10.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0, 512);
867 assert!(t.is_some(), "should hit union");
868 }
869 #[test]
870 fn csg_ray_march_difference_misses_carved_region() {
871 let big = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
872 let cut = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 2.9));
873 let shell = CsgTree::difference(big, cut);
874 let t = csg_ray_march(&shell, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 20.0, 512);
875 assert!(t.is_some(), "should hit shell");
876 }
877 #[test]
878 fn csg_tree_contains_inside() {
879 let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 2.0));
880 assert!(csg_tree_contains(&tree, [0.0, 0.0, 0.0]));
881 assert!(!csg_tree_contains(&tree, [3.0, 0.0, 0.0]));
882 }
883 #[test]
884 fn csg_tree_contains_difference() {
885 let a = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
886 let b = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.5));
887 let d = CsgTree::difference(a, b);
888 assert!(!csg_tree_contains(&d, [0.0, 0.0, 0.0]));
889 assert!(csg_tree_contains(&d, [2.5, 0.0, 0.0]));
890 }
891}
892pub fn csg_tree_aabb<F>(node: &CsgTree, bounds: &F) -> ([f64; 3], [f64; 3])
900where
901 F: Fn(&dyn ImplicitSurface) -> SurfaceAabb,
902{
903 match node {
904 CsgTree::Leaf(s) => bounds(s.as_ref()),
905 CsgTree::Node { op, left, right } => {
906 let (l_min, l_max) = csg_tree_aabb(left, bounds);
907 let (r_min, r_max) = csg_tree_aabb(right, bounds);
908 match op {
909 CsgOp::Union => {
910 let mn = [
911 l_min[0].min(r_min[0]),
912 l_min[1].min(r_min[1]),
913 l_min[2].min(r_min[2]),
914 ];
915 let mx = [
916 l_max[0].max(r_max[0]),
917 l_max[1].max(r_max[1]),
918 l_max[2].max(r_max[2]),
919 ];
920 (mn, mx)
921 }
922 CsgOp::Intersection => {
923 let mn = [
924 l_min[0].max(r_min[0]),
925 l_min[1].max(r_min[1]),
926 l_min[2].max(r_min[2]),
927 ];
928 let mx = [
929 l_max[0].min(r_max[0]),
930 l_max[1].min(r_max[1]),
931 l_max[2].min(r_max[2]),
932 ];
933 (mn, mx)
934 }
935 CsgOp::Difference => (l_min, l_max),
936 }
937 }
938 }
939}
940pub fn marching_cubes_sample(
946 surface: &dyn ImplicitSurface,
947 bounds_min: [f64; 3],
948 bounds_max: [f64; 3],
949 resolution: usize,
950) -> Vec<MarchingCell> {
951 let n = resolution.max(2);
952 let step_x = (bounds_max[0] - bounds_min[0]) / n as f64;
953 let step_y = (bounds_max[1] - bounds_min[1]) / n as f64;
954 let step_z = (bounds_max[2] - bounds_min[2]) / n as f64;
955 let step = step_x.min(step_y).min(step_z);
956 let offsets: [[f64; 3]; 8] = [
957 [0.0, 0.0, 0.0],
958 [step, 0.0, 0.0],
959 [step, step, 0.0],
960 [0.0, step, 0.0],
961 [0.0, 0.0, step],
962 [step, 0.0, step],
963 [step, step, step],
964 [0.0, step, step],
965 ];
966 let mut cells = Vec::new();
967 for ix in 0..n {
968 let x0 = bounds_min[0] + ix as f64 * step;
969 for iy in 0..n {
970 let y0 = bounds_min[1] + iy as f64 * step;
971 for iz in 0..n {
972 let z0 = bounds_min[2] + iz as f64 * step;
973 let cell_min = [x0, y0, z0];
974 let mut corner_values = [0.0f64; 8];
975 for (k, &off) in offsets.iter().enumerate() {
976 let p = add(cell_min, off);
977 corner_values[k] = surface.sdf(p);
978 }
979 let cell = MarchingCell {
980 min: cell_min,
981 step,
982 corner_values,
983 };
984 if cell.has_surface() {
985 cells.push(cell);
986 }
987 }
988 }
989 }
990 cells
991}
992pub fn marching_cubes_vertices(
996 surface: &dyn ImplicitSurface,
997 bounds_min: [f64; 3],
998 bounds_max: [f64; 3],
999 resolution: usize,
1000) -> Vec<[f64; 3]> {
1001 let cells = marching_cubes_sample(surface, bounds_min, bounds_max, resolution);
1002 cells.iter().flat_map(|c| c.extract_vertices()).collect()
1003}