1use crate::shape::{RayHit, Shape};
7use oxiphysics_core::Aabb;
8use oxiphysics_core::math::{Mat3, Real, Vec3};
9use std::f64::consts::PI;
10
11#[derive(Debug, Clone)]
14pub struct Cone {
15 pub radius: Real,
17 pub half_height: Real,
19}
20
21impl Cone {
22 pub fn new(radius: Real, half_height: Real) -> Self {
24 Self {
25 radius,
26 half_height,
27 }
28 }
29
30 #[allow(dead_code)]
32 pub fn volume_explicit(&self) -> Real {
33 let h = 2.0 * self.half_height;
34 PI * self.radius * self.radius * h / 3.0
35 }
36
37 #[allow(dead_code)]
40 pub fn surface_area(&self) -> Real {
41 let r = self.radius;
42 let h = 2.0 * self.half_height;
43 let slant = (r * r + h * h).sqrt();
44 PI * r * r + PI * r * slant
45 }
46
47 #[allow(dead_code)]
49 pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
50 let r2 = self.radius * self.radius;
51 let h2 = (2.0 * self.half_height).powi(2);
52 let iy = 3.0 * mass * r2 / 10.0;
53 let ixz = mass * (3.0 * r2 + 2.0 * h2) / 20.0;
54 [[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
55 }
56
57 #[allow(dead_code)]
59 pub fn ray_cast_array(
60 &self,
61 origin: [f64; 3],
62 direction: [f64; 3],
63 max_toi: f64,
64 ) -> Option<(f64, [f64; 3])> {
65 let o = Vec3::new(origin[0], origin[1], origin[2]);
66 let d = Vec3::new(direction[0], direction[1], direction[2]);
67 let hit = self.ray_cast(&o, &d, max_toi)?;
68 Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
69 }
70
71 #[allow(dead_code)]
73 pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
74 let px = p[0];
75 let py = p[1];
76 let pz = p[2];
77
78 let cy = py.clamp(-self.half_height, self.half_height);
80
81 let h = 2.0 * self.half_height;
83 let r_at_cy = self.radius * (self.half_height - cy) / h;
84
85 let xz_len = (px * px + pz * pz).sqrt();
87
88 if xz_len <= r_at_cy {
89 let dist_to_base = py - (-self.half_height);
92 let dist_to_side = r_at_cy - xz_len;
94 if dist_to_base <= dist_to_side {
95 [px, -self.half_height, pz]
96 } else {
97 if xz_len < 1e-12 {
99 [r_at_cy, cy, 0.0]
100 } else {
101 let s = r_at_cy / xz_len;
102 [px * s, cy, pz * s]
103 }
104 }
105 } else {
106 if xz_len < 1e-12 {
108 [r_at_cy, cy, 0.0]
109 } else {
110 let s = r_at_cy / xz_len;
111 [px * s, cy, pz * s]
112 }
113 }
114 }
115
116 #[allow(dead_code)]
121 pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
122 let h = 2.0 * self.half_height;
123 let xz_len = (direction[0] * direction[0] + direction[2] * direction[2]).sqrt();
124
125 let apex_dot = direction[1] * self.half_height;
126 let base_center_dot = -direction[1] * self.half_height;
127 let base_rim_dot = base_center_dot + self.radius * xz_len;
128
129 if apex_dot >= base_rim_dot {
130 [0.0, self.half_height, 0.0]
131 } else if xz_len > 1e-10 {
132 let _sin_angle = self.radius / (self.radius * self.radius + h * h).sqrt();
133 [
134 self.radius * direction[0] / xz_len,
135 -self.half_height,
136 self.radius * direction[2] / xz_len,
137 ]
138 } else {
139 [self.radius, -self.half_height, 0.0]
140 }
141 }
142
143 #[allow(dead_code)]
145 pub fn slant_height(&self) -> f64 {
146 let h = 2.0 * self.half_height;
147 (self.radius * self.radius + h * h).sqrt()
148 }
149
150 #[allow(dead_code)]
152 pub fn half_angle(&self) -> f64 {
153 let h = 2.0 * self.half_height;
154 (self.radius / h).atan()
155 }
156
157 #[allow(dead_code)]
159 pub fn lateral_surface_area(&self) -> f64 {
160 PI * self.radius * self.slant_height()
161 }
162
163 #[allow(dead_code)]
165 pub fn base_area(&self) -> f64 {
166 PI * self.radius * self.radius
167 }
168
169 #[allow(dead_code)]
171 pub fn contains_point(&self, p: [f64; 3]) -> bool {
172 if p[1] < -self.half_height || p[1] > self.half_height {
173 return false;
174 }
175 let h = 2.0 * self.half_height;
176 let r_at_y = self.radius * (self.half_height - p[1]) / h;
177 let xz2 = p[0] * p[0] + p[2] * p[2];
178 xz2 <= r_at_y * r_at_y
179 }
180
181 #[allow(dead_code)]
184 pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
185 let cp = self.closest_point(p);
186 let dx = p[0] - cp[0];
187 let dy = p[1] - cp[1];
188 let dz = p[2] - cp[2];
189 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
190 if self.contains_point(p) { -dist } else { dist }
191 }
192
193 #[allow(dead_code)]
195 pub fn radius_at_y(&self, y: f64) -> f64 {
196 let clamped = y.clamp(-self.half_height, self.half_height);
197 let h = 2.0 * self.half_height;
198 self.radius * (self.half_height - clamped) / h
199 }
200
201 #[allow(dead_code)]
205 pub fn truncated_cone(&self, cut_ratio: f64) -> (f64, f64, f64) {
206 let ratio = cut_ratio.clamp(0.0, 1.0);
207 let top_r = self.radius * (1.0 - ratio);
208 let bottom_r = self.radius;
209 let hh = self.half_height * ratio;
210 (top_r, bottom_r, hh)
211 }
212
213 #[allow(dead_code)]
216 pub fn truncated_cone_volume(&self, cut_ratio: f64) -> f64 {
217 let (r1, r2, _) = self.truncated_cone(cut_ratio);
218 let h = 2.0 * self.half_height * cut_ratio.clamp(0.0, 1.0);
219 PI * h * (r1 * r1 + r1 * r2 + r2 * r2) / 3.0
220 }
221
222 #[allow(dead_code)]
224 pub fn ray_cast_base_cap(
225 &self,
226 origin: [f64; 3],
227 direction: [f64; 3],
228 max_toi: f64,
229 ) -> Option<(f64, [f64; 3])> {
230 if direction[1].abs() < 1e-12 {
231 return None;
232 }
233 let t = (-self.half_height - origin[1]) / direction[1];
234 if t < 0.0 || t > max_toi {
235 return None;
236 }
237 let px = origin[0] + t * direction[0];
238 let pz = origin[2] + t * direction[2];
239 if px * px + pz * pz <= self.radius * self.radius {
240 Some((t, [0.0, -1.0, 0.0]))
241 } else {
242 None
243 }
244 }
245
246 #[allow(dead_code)]
249 pub fn closest_point_on_lateral(&self, p: [f64; 3]) -> [f64; 3] {
250 let xz_len = (p[0] * p[0] + p[2] * p[2]).sqrt();
251 let h = 2.0 * self.half_height;
252
253 let side_len = self.slant_height();
254 let side_dx = self.radius / side_len;
255 let side_dy = -h / side_len;
256
257 let vx = xz_len;
258 let vy = p[1] - self.half_height;
259
260 let t = (vx * side_dx + vy * side_dy).clamp(0.0, side_len);
261 let proj_xz = t * side_dx;
262 let proj_y = self.half_height + t * side_dy;
263
264 if xz_len < 1e-12 {
265 [proj_xz, proj_y, 0.0]
266 } else {
267 let s = proj_xz / xz_len;
268 [p[0] * s, proj_y, p[2] * s]
269 }
270 }
271
272 #[allow(dead_code)]
278 pub fn ray_cone_analytic(
279 &self,
280 origin: [f64; 3],
281 direction: [f64; 3],
282 max_toi: f64,
283 ) -> Option<(f64, [f64; 3], bool)> {
284 let o = Vec3::new(origin[0], origin[1], origin[2]);
285 let d = Vec3::new(direction[0], direction[1], direction[2]);
286
287 let h = 2.0 * self.half_height;
288 let k = self.radius / h;
289 let k2 = k * k;
290 let apex_y = self.half_height;
291
292 let fy = apex_y - o.y;
293 let a = d.x * d.x + d.z * d.z - k2 * d.y * d.y;
294 let b = 2.0 * (o.x * d.x + o.z * d.z + k2 * fy * d.y);
295 let c_val = o.x * o.x + o.z * o.z - k2 * fy * fy;
296
297 let mut best_t = f64::INFINITY;
298 let mut best_is_lateral = true;
299
300 if a.abs() > 1e-12 {
301 let disc = b * b - 4.0 * a * c_val;
302 if disc >= 0.0 {
303 let sqrt_disc = disc.sqrt();
304 for t in [(-b - sqrt_disc) / (2.0 * a), (-b + sqrt_disc) / (2.0 * a)] {
305 if t >= 0.0 && t <= max_toi {
306 let py = o.y + t * d.y;
307 if py >= -self.half_height && py <= self.half_height && t < best_t {
308 best_t = t;
309 best_is_lateral = true;
310 }
311 }
312 }
313 }
314 }
315
316 if d.y.abs() > 1e-12 {
318 let t = (-self.half_height - o.y) / d.y;
319 if t >= 0.0 && t <= max_toi {
320 let px = o.x + t * d.x;
321 let pz = o.z + t * d.z;
322 if px * px + pz * pz <= self.radius * self.radius && t < best_t {
323 best_t = t;
324 best_is_lateral = false;
325 }
326 }
327 }
328
329 if best_t.is_infinite() {
330 return None;
331 }
332
333 let hit_pt = [
334 origin[0] + best_t * direction[0],
335 origin[1] + best_t * direction[1],
336 origin[2] + best_t * direction[2],
337 ];
338 let normal = if best_is_lateral {
339 let xz_len = (hit_pt[0] * hit_pt[0] + hit_pt[2] * hit_pt[2])
340 .sqrt()
341 .max(1e-30);
342 let slope = self.radius / h;
343 let nx = hit_pt[0] / xz_len;
344 let nz = hit_pt[2] / xz_len;
345 let len = (nx * nx + slope * slope + nz * nz).sqrt();
346 [nx / len, slope / len, nz / len]
347 } else {
348 [0.0, -1.0, 0.0]
349 };
350
351 Some((best_t, normal, best_is_lateral))
352 }
353
354 #[allow(dead_code)]
362 pub fn intersects_plane(&self, plane_normal: [f64; 3], plane_d: f64) -> bool {
363 let apex = [0.0_f64, self.half_height, 0.0];
365 let apex_sd =
366 plane_normal[0] * apex[0] + plane_normal[1] * apex[1] + plane_normal[2] * apex[2]
367 - plane_d;
368
369 let base_center_sd = -plane_normal[1] * self.half_height - plane_d;
371
372 let xz_proj =
374 (plane_normal[0] * plane_normal[0] + plane_normal[2] * plane_normal[2]).sqrt();
375 let rim_sd = base_center_sd + self.radius * xz_proj;
376 let rim_sd_neg = base_center_sd - self.radius * xz_proj;
377
378 let signs = [apex_sd, rim_sd, rim_sd_neg];
380 let any_pos = signs.iter().any(|&s| s >= 0.0);
381 let any_neg = signs.iter().any(|&s| s <= 0.0);
382 any_pos && any_neg
383 }
384
385 #[allow(dead_code)]
392 pub fn frustum_from_cone(&self, y_bottom: f64, y_top: f64) -> (f64, f64, f64) {
393 let yb = y_bottom.clamp(-self.half_height, self.half_height);
394 let yt = y_top.clamp(yb, self.half_height);
395 let r_bottom = self.radius_at_y(yb);
396 let r_top = self.radius_at_y(yt);
397 let frustum_hh = (yt - yb) * 0.5;
398 (r_bottom, r_top, frustum_hh)
399 }
400
401 #[allow(dead_code)]
406 pub fn sdf(&self, p: [f64; 3]) -> f64 {
407 self.signed_distance(p)
408 }
409
410 #[allow(dead_code)]
416 pub fn intersects_sphere(&self, sphere_center: [f64; 3], sphere_radius: f64) -> bool {
417 let cp = self.closest_point(sphere_center);
418 let dx = sphere_center[0] - cp[0];
419 let dy = sphere_center[1] - cp[1];
420 let dz = sphere_center[2] - cp[2];
421 let dist_sq = dx * dx + dy * dy + dz * dz;
422 dist_sq <= sphere_radius * sphere_radius
423 }
424
425 #[allow(dead_code)]
430 pub fn frustum_lateral_area(&self, y_bottom: f64, y_top: f64) -> f64 {
431 let (r_bottom, r_top, _hh) = self.frustum_from_cone(y_bottom, y_top);
432 let dy = (y_top - y_bottom).abs();
433 let dr = (r_bottom - r_top).abs();
434 let slant = (dy * dy + dr * dr).sqrt();
435 PI * (r_bottom + r_top) * slant
436 }
437
438 #[allow(dead_code)]
444 pub fn apex_angle(&self) -> f64 {
445 2.0 * self.half_angle()
446 }
447
448 #[allow(dead_code)]
453 pub fn double_cone_params(&self) -> (f64, f64) {
454 (self.radius, self.half_height)
455 }
456
457 #[allow(dead_code)]
462 pub fn contains_point_double_cone(&self, p: [f64; 3]) -> bool {
463 let h = 2.0 * self.half_height;
464 let xz2 = p[0] * p[0] + p[2] * p[2];
465 let y_abs = p[1].abs();
466 if y_abs > self.half_height {
467 return false;
468 }
469 let r_at = self.radius * y_abs / self.half_height;
471 xz2 <= r_at * r_at + 1e-14 * h
472 }
473
474 #[allow(dead_code)]
480 pub fn unfold_lateral(&self, u: f64, t_slant: f64) -> [f64; 2] {
481 let sin_alpha = self.radius / self.slant_height().max(1e-14);
484 let phi = u * sin_alpha;
485 [t_slant * phi.cos(), t_slant * phi.sin()]
486 }
487
488 #[allow(dead_code)]
494 pub fn bounding_cone_of_points(points: &[[f64; 3]]) -> (f64, f64) {
495 if points.is_empty() {
496 return (0.0, 0.0);
497 }
498 let mut max_half_angle: f64 = 0.0;
499 let mut max_elev: f64 = 0.0;
500 for &p in points {
501 let len = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
502 if len < 1e-14 {
503 continue;
504 }
505 let cos_a = (p[1] / len).clamp(-1.0, 1.0);
507 let half_angle = cos_a.acos();
508 let elev = p[1] / len;
509 if half_angle > max_half_angle {
510 max_half_angle = half_angle;
511 max_elev = elev;
512 }
513 }
514 (max_half_angle, max_elev)
515 }
516
517 #[allow(dead_code)]
521 pub fn volume_swept(&self, delta: [f64; 3]) -> f64 {
522 let dist = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
523 self.volume() + self.base_area() * dist
524 }
525
526 #[allow(dead_code)]
530 pub fn sdf_gradient(&self, p: [f64; 3]) -> [f64; 3] {
531 let eps = 1e-5;
532 let s0 = self.sdf(p);
533 let gx = (self.sdf([p[0] + eps, p[1], p[2]]) - s0) / eps;
534 let gy = (self.sdf([p[0], p[1] + eps, p[2]]) - s0) / eps;
535 let gz = (self.sdf([p[0], p[1], p[2] + eps]) - s0) / eps;
536 let len = (gx * gx + gy * gy + gz * gz).sqrt().max(1e-14);
537 [gx / len, gy / len, gz / len]
538 }
539
540 #[allow(dead_code)]
544 pub fn ray_intersection_count(
545 &self,
546 origin: [f64; 3],
547 direction: [f64; 3],
548 max_toi: f64,
549 ) -> usize {
550 let mut count = 0usize;
551 let h = 2.0 * self.half_height;
552 let k = self.radius / h;
553 let k2 = k * k;
554 let apex_y = self.half_height;
555 let fy = apex_y - origin[1];
556 let a = direction[0] * direction[0] + direction[2] * direction[2]
557 - k2 * direction[1] * direction[1];
558 let b =
559 2.0 * (origin[0] * direction[0] + origin[2] * direction[2] + k2 * fy * direction[1]);
560 let c = origin[0] * origin[0] + origin[2] * origin[2] - k2 * fy * fy;
561 if a.abs() > 1e-12 {
562 let disc = b * b - 4.0 * a * c;
563 if disc >= 0.0 {
564 let sq = disc.sqrt();
565 for t in [(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)] {
566 if t >= 0.0 && t <= max_toi {
567 let py = origin[1] + t * direction[1];
568 if py >= -self.half_height && py <= self.half_height {
569 count += 1;
570 }
571 }
572 }
573 }
574 }
575 if direction[1].abs() > 1e-12 {
577 let t = (-self.half_height - origin[1]) / direction[1];
578 if t >= 0.0 && t <= max_toi {
579 let px = origin[0] + t * direction[0];
580 let pz = origin[2] + t * direction[2];
581 if px * px + pz * pz <= self.radius * self.radius {
582 count += 1;
583 }
584 }
585 }
586 count
587 }
588
589 #[allow(dead_code)]
593 pub fn solid_angle(&self) -> f64 {
594 let alpha = self.half_angle();
595 2.0 * PI * (1.0 - alpha.cos())
596 }
597
598 #[allow(dead_code)]
602 pub fn base_rim_points(&self, n: usize) -> Vec<[f64; 3]> {
603 let n = n.max(3);
604 (0..n)
605 .map(|i| {
606 let t = 2.0 * PI * i as f64 / n as f64;
607 [
608 self.radius * t.cos(),
609 -self.half_height,
610 self.radius * t.sin(),
611 ]
612 })
613 .collect()
614 }
615
616 #[allow(dead_code)]
619 pub fn sphere_inside_cone(&self, center: [f64; 3], sphere_radius: f64) -> bool {
620 let sd = self.sdf(center);
624 sd + sphere_radius <= 0.0
625 }
626
627 #[allow(dead_code)]
629 pub fn random_lateral_surface_point(&self, seed: u64) -> [f64; 3] {
630 let mut state = seed;
632 let xorshift = |s: &mut u64| -> f64 {
633 *s ^= *s << 13;
634 *s ^= *s >> 7;
635 *s ^= *s << 17;
636 (*s as f64) / (u64::MAX as f64)
637 };
638
639 let u = xorshift(&mut state);
644 let frac_t = 1.0 - (1.0 - u).sqrt(); let y = self.half_height - frac_t * 2.0 * self.half_height;
646 let r_at_y = self.radius_at_y(y);
647 let theta = xorshift(&mut state) * 2.0 * PI;
648 [r_at_y * theta.cos(), y, r_at_y * theta.sin()]
649 }
650}
651
652impl Shape for Cone {
653 fn bounding_box(&self) -> Aabb {
654 Aabb::new(
655 Vec3::new(-self.radius, -self.half_height, -self.radius),
656 Vec3::new(self.radius, self.half_height, self.radius),
657 )
658 }
659
660 fn support_point(&self, direction: &Vec3) -> Vec3 {
661 let h = 2.0 * self.half_height;
662 let xz_len = (direction.x * direction.x + direction.z * direction.z).sqrt();
663
664 let apex_dot = direction.y * self.half_height;
666 let base_center_dot = -direction.y * self.half_height;
667 let base_rim_dot = base_center_dot + self.radius * xz_len;
668
669 if apex_dot >= base_rim_dot {
670 Vec3::new(0.0, self.half_height, 0.0)
671 } else if xz_len > 1e-10 {
672 let sin_angle = self.radius / (self.radius * self.radius + h * h).sqrt();
673 let _ = sin_angle; Vec3::new(
675 self.radius * direction.x / xz_len,
676 -self.half_height,
677 self.radius * direction.z / xz_len,
678 )
679 } else {
680 Vec3::new(self.radius, -self.half_height, 0.0)
681 }
682 }
683
684 fn volume(&self) -> Real {
685 let h = 2.0 * self.half_height;
686 PI * self.radius * self.radius * h / 3.0
687 }
688
689 fn center_of_mass(&self) -> Vec3 {
690 Vec3::new(0.0, -self.half_height * 0.5, 0.0)
692 }
693
694 fn inertia_tensor(&self, mass: Real) -> Mat3 {
695 let r2 = self.radius * self.radius;
696 let h2 = (2.0 * self.half_height).powi(2);
698 let iy = 3.0 * mass * r2 / 10.0;
700 let ixz = mass * (3.0 * r2 + 2.0 * h2) / 20.0;
702 Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
703 }
704
705 fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
706 let h = 2.0 * self.half_height;
712 let k = self.radius / h;
713 let k2 = k * k;
714 let apex_y = self.half_height;
715
716 let ox = ray_origin.x;
717 let oy = ray_origin.y;
718 let oz = ray_origin.z;
719 let dx = ray_direction.x;
720 let dy = ray_direction.y;
721 let dz = ray_direction.z;
722
723 let fy = apex_y - oy;
726 let a = dx * dx + dz * dz - k2 * dy * dy;
727 let b = 2.0 * (ox * dx + oz * dz + k2 * fy * dy);
728 let c_val = ox * ox + oz * oz - k2 * fy * fy;
729
730 let mut best: Option<RayHit> = None;
731
732 let try_t = |t: Real, best: &mut Option<RayHit>| {
733 if t < 0.0 || t > max_toi {
734 return;
735 }
736 let p = ray_origin + ray_direction * t;
737 if p.y < -self.half_height || p.y > self.half_height {
738 return;
739 }
740 let xz_len = (p.x * p.x + p.z * p.z).sqrt();
742 let normal = if xz_len > 1e-12 {
743 let slope = self.radius / h;
744 Vec3::new(p.x / xz_len, slope, p.z / xz_len).normalize()
745 } else {
746 Vec3::new(0.0, 1.0, 0.0)
747 };
748 if best.as_ref().is_none_or(|prev| t < prev.toi) {
749 *best = Some(RayHit {
750 point: p,
751 normal,
752 toi: t,
753 });
754 }
755 };
756
757 if a.abs() > 1e-12 {
758 let disc = b * b - 4.0 * a * c_val;
759 if disc >= 0.0 {
760 let sqrt_disc = disc.sqrt();
761 try_t((-b - sqrt_disc) / (2.0 * a), &mut best);
762 try_t((-b + sqrt_disc) / (2.0 * a), &mut best);
763 }
764 } else if b.abs() > 1e-12 {
765 try_t(-c_val / b, &mut best);
766 }
767
768 if ray_direction.y.abs() > 1e-12 {
770 let t = (-self.half_height - ray_origin.y) / ray_direction.y;
771 if t >= 0.0 && t <= max_toi {
772 let p = ray_origin + ray_direction * t;
773 if p.x * p.x + p.z * p.z <= self.radius * self.radius {
774 let normal = Vec3::new(0.0, -1.0, 0.0);
775 if best.as_ref().is_none_or(|prev| t < prev.toi) {
776 best = Some(RayHit {
777 point: p,
778 normal,
779 toi: t,
780 });
781 }
782 }
783 }
784 }
785
786 best
787 }
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793
794 #[test]
795 fn test_cone_volume() {
796 let c = Cone::new(1.0, 1.5);
797 let expected = PI * 1.0 * 3.0 / 3.0;
798 assert!((c.volume() - expected).abs() < 1e-10);
799 }
800
801 #[test]
802 fn test_cone_surface_area() {
803 let c = Cone::new(1.0, 1.0);
805 let slant = (1.0_f64 + 4.0_f64).sqrt();
806 let expected = PI + PI * slant;
807 assert!((c.surface_area() - expected).abs() < 1e-10);
808 }
809
810 #[test]
811 fn test_cone_support_apex() {
812 let c = Cone::new(1.0, 2.0);
813 let sp = c.support_point(&Vec3::new(0.0, 1.0, 0.0));
814 assert!((sp.y - 2.0).abs() < 1e-10);
815 }
816
817 #[test]
818 fn test_cone_inertia_symmetry() {
819 let c = Cone::new(1.5, 2.0);
820 let it = c.inertia_tensor(3.0);
821 let diff = (it[(0, 0)] - it[(2, 2)]).abs();
823 assert!(
824 diff < 1e-10,
825 "I_xx != I_zz: {} vs {}",
826 it[(0, 0)],
827 it[(2, 2)]
828 );
829 assert!(it[(0, 1)].abs() < 1e-10);
831 assert!(it[(1, 0)].abs() < 1e-10);
832 }
833
834 #[test]
835 fn test_cone_inertia_array() {
836 let c = Cone::new(1.0, 1.0);
837 let it = c.inertia_tensor_array(1.0);
838 assert!(it[0][0] > 0.0);
839 assert!(it[1][1] > 0.0);
840 assert!((it[0][0] - it[2][2]).abs() < 1e-10);
841 }
842
843 #[test]
844 fn test_cone_raycast_side() {
845 let c = Cone::new(1.0, 2.0); let origin = Vec3::new(-5.0, 0.0, 0.0);
849 let dir = Vec3::new(1.0, 0.0, 0.0);
850 let hit = c.ray_cast(&origin, &dir, 100.0);
852 assert!(hit.is_some(), "expected a hit on cone side");
853 let hit = hit.unwrap();
854 assert!(hit.toi > 0.0);
855 assert!(
856 (hit.point.x + 0.5).abs() < 1e-6,
857 "expected hit at x=-0.5, got x={}",
858 hit.point.x
859 );
860 }
861
862 #[test]
863 fn test_cone_raycast_base_cap() {
864 let c = Cone::new(1.0, 1.0); let origin = Vec3::new(0.0, -5.0, 0.0);
867 let dir = Vec3::new(0.0, 1.0, 0.0);
868 let hit = c.ray_cast(&origin, &dir, 100.0);
869 assert!(hit.is_some(), "expected a hit on cone base");
870 let hit = hit.unwrap();
871 assert!(
872 (hit.toi - 4.0).abs() < 1e-6,
873 "expected toi=4, got {}",
874 hit.toi
875 );
876 assert!((hit.normal.y + 1.0).abs() < 1e-10);
877 }
878
879 #[test]
880 fn test_cone_closest_point_outside() {
881 let c = Cone::new(1.0, 1.0); let cp = c.closest_point([5.0, 0.0, 0.0]);
884 assert!(
886 (cp[0] - 0.5).abs() < 1e-6,
887 "expected cp[0]=0.5, got {}",
888 cp[0]
889 );
890 assert!((cp[1]).abs() < 1e-6);
891 }
892
893 #[test]
896 fn test_cone_support_array_apex() {
897 let c = Cone::new(1.0, 2.0);
898 let sp = c.support([0.0, 1.0, 0.0]);
899 assert!((sp[1] - 2.0).abs() < 1e-10);
900 }
901
902 #[test]
903 fn test_cone_support_array_base_rim() {
904 let c = Cone::new(1.0, 2.0);
905 let sp = c.support([1.0, -1.0, 0.0]);
906 assert!((sp[0] - 1.0).abs() < 1e-10);
907 assert!((sp[1] + 2.0).abs() < 1e-10);
908 }
909
910 #[test]
911 fn test_cone_slant_height() {
912 let c = Cone::new(3.0, 2.0); let expected = (9.0 + 16.0_f64).sqrt(); assert!((c.slant_height() - expected).abs() < 1e-10);
915 }
916
917 #[test]
918 fn test_cone_half_angle() {
919 let c = Cone::new(1.0, 1.0); let expected = (0.5_f64).atan(); assert!((c.half_angle() - expected).abs() < 1e-10);
922 }
923
924 #[test]
925 fn test_cone_lateral_surface_area() {
926 let c = Cone::new(1.0, 1.0);
927 let slant = c.slant_height();
928 let expected = PI * 1.0 * slant;
929 assert!((c.lateral_surface_area() - expected).abs() < 1e-10);
930 }
931
932 #[test]
933 fn test_cone_base_area() {
934 let c = Cone::new(2.0, 1.0);
935 assert!((c.base_area() - 4.0 * PI).abs() < 1e-10);
936 }
937
938 #[test]
939 fn test_cone_surface_area_decomposition() {
940 let c = Cone::new(1.5, 2.0);
941 let total = c.surface_area();
942 let decomposed = c.lateral_surface_area() + c.base_area();
943 assert!((total - decomposed).abs() < 1e-10);
944 }
945
946 #[test]
947 fn test_cone_contains_point() {
948 let c = Cone::new(1.0, 1.0); assert!(c.contains_point([0.0, 0.0, 0.0])); assert!(c.contains_point([0.0, -1.0, 0.0])); assert!(!c.contains_point([0.0, 2.0, 0.0])); assert!(!c.contains_point([1.0, 0.0, 0.0])); }
954
955 #[test]
956 fn test_cone_contains_point_base_rim() {
957 let c = Cone::new(1.0, 1.0);
958 assert!(c.contains_point([1.0, -1.0, 0.0]));
960 assert!(!c.contains_point([1.1, -1.0, 0.0]));
961 }
962
963 #[test]
964 fn test_cone_signed_distance_inside() {
965 let c = Cone::new(2.0, 2.0);
966 let d = c.signed_distance([0.0, -2.0, 0.0]); assert!(d <= 0.0);
968 }
969
970 #[test]
971 fn test_cone_signed_distance_outside() {
972 let c = Cone::new(1.0, 1.0);
973 let d = c.signed_distance([5.0, 0.0, 0.0]);
974 assert!(d > 0.0);
975 }
976
977 #[test]
978 fn test_cone_radius_at_y() {
979 let c = Cone::new(2.0, 2.0); assert!((c.radius_at_y(-2.0) - 2.0).abs() < 1e-10); assert!((c.radius_at_y(2.0)).abs() < 1e-10); assert!((c.radius_at_y(0.0) - 1.0).abs() < 1e-10); }
984
985 #[test]
986 fn test_cone_truncated_cone() {
987 let c = Cone::new(2.0, 2.0);
988 let (top_r, bot_r, hh) = c.truncated_cone(0.5);
989 assert!((bot_r - 2.0).abs() < 1e-10);
990 assert!((top_r - 1.0).abs() < 1e-10); assert!((hh - 1.0).abs() < 1e-10); }
993
994 #[test]
995 fn test_cone_truncated_cone_full() {
996 let c = Cone::new(2.0, 2.0);
997 let (top_r, _, _) = c.truncated_cone(1.0);
998 assert!(top_r.abs() < 1e-10); }
1000
1001 #[test]
1002 fn test_cone_truncated_cone_volume() {
1003 let c = Cone::new(1.0, 1.0);
1004 let full_vol = c.volume();
1006 let trunc_vol = c.truncated_cone_volume(1.0);
1008 assert!((trunc_vol - full_vol).abs() < 1e-10);
1009 }
1010
1011 #[test]
1012 fn test_cone_ray_cast_base_cap_array() {
1013 let c = Cone::new(1.0, 1.0);
1014 let result = c.ray_cast_base_cap([0.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
1015 assert!(result.is_some());
1016 let (t, n) = result.unwrap();
1017 assert!((t - 4.0).abs() < 1e-10);
1018 assert!((n[1] + 1.0).abs() < 1e-10);
1019 }
1020
1021 #[test]
1022 fn test_cone_ray_cast_base_cap_miss() {
1023 let c = Cone::new(1.0, 1.0);
1024 let result = c.ray_cast_base_cap([5.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
1025 assert!(result.is_none());
1026 }
1027
1028 #[test]
1029 fn test_cone_closest_point_on_lateral() {
1030 let c = Cone::new(1.0, 1.0);
1031 let cp = c.closest_point_on_lateral([5.0, 0.0, 0.0]);
1033 assert!(cp[0] > 0.0);
1036 let r = (cp[0] * cp[0] + cp[2] * cp[2]).sqrt();
1038 let expected_r = c.radius_at_y(cp[1]);
1039 assert!((r - expected_r).abs() < 0.1);
1040 }
1041
1042 #[test]
1043 fn test_cone_closest_point_on_lateral_apex() {
1044 let c = Cone::new(1.0, 1.0);
1045 let cp = c.closest_point_on_lateral([0.0, 5.0, 0.0]);
1047 assert!((cp[1] - 1.0).abs() < 1e-10);
1049 assert!(cp[0].abs() < 1e-10);
1050 }
1051
1052 #[test]
1053 fn test_cone_volume_explicit_matches() {
1054 let c = Cone::new(1.5, 2.5);
1055 assert!((c.volume_explicit() - c.volume()).abs() < 1e-10);
1056 }
1057
1058 #[test]
1061 fn test_cone_ray_analytic_lateral() {
1062 let c = Cone::new(1.0, 2.0);
1063 let result = c.ray_cone_analytic([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
1064 assert!(result.is_some(), "should hit lateral surface");
1065 let (t, n, is_lateral) = result.unwrap();
1066 assert!(t > 0.0);
1067 assert!(is_lateral, "should be lateral hit");
1068 assert!(
1069 n[1] > 0.0,
1070 "normal should have positive Y component for lateral cone"
1071 );
1072 }
1073
1074 #[test]
1075 fn test_cone_ray_analytic_base() {
1076 let c = Cone::new(1.0, 1.0);
1077 let result = c.ray_cone_analytic([0.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
1078 assert!(result.is_some());
1079 let (t, n, is_lateral) = result.unwrap();
1080 assert!((t - 4.0).abs() < 1e-6, "toi should be 4, got {t}");
1081 assert!(!is_lateral);
1082 assert!((n[1] + 1.0).abs() < 1e-10);
1083 }
1084
1085 #[test]
1086 fn test_cone_plane_intersection_horizontal_through_base() {
1087 let c = Cone::new(1.0, 1.0);
1088 let hit = c.intersects_plane([0.0, 1.0, 0.0], -1.0);
1090 assert!(hit, "plane through base should intersect cone");
1091 }
1092
1093 #[test]
1094 fn test_cone_plane_intersection_above_apex() {
1095 let c = Cone::new(1.0, 1.0);
1096 let hit = c.intersects_plane([0.0, 1.0, 0.0], 5.0);
1098 assert!(!hit, "plane far above should not intersect cone");
1099 }
1100
1101 #[test]
1102 fn test_cone_frustum_from_cone() {
1103 let c = Cone::new(2.0, 2.0); let (r_bot, r_top, hh) = c.frustum_from_cone(-1.0, 1.0);
1106 assert!((r_bot - 1.5).abs() < 1e-10, "r_bot={r_bot}");
1108 assert!((r_top - 0.5).abs() < 1e-10, "r_top={r_top}");
1110 assert!((hh - 1.0).abs() < 1e-10, "hh={hh}");
1111 }
1112
1113 #[test]
1114 fn test_cone_sdf_matches_signed_distance() {
1115 let c = Cone::new(1.0, 1.0);
1116 let p = [3.0, 0.0, 0.0];
1117 assert!((c.sdf(p) - c.signed_distance(p)).abs() < 1e-12);
1118 }
1119
1120 #[test]
1121 fn test_cone_intersects_sphere_overlap() {
1122 let c = Cone::new(1.0, 1.0);
1123 assert!(c.intersects_sphere([0.0, 0.0, 0.0], 5.0));
1125 }
1126
1127 #[test]
1128 fn test_cone_intersects_sphere_no_overlap() {
1129 let c = Cone::new(1.0, 1.0);
1130 assert!(!c.intersects_sphere([10.0, 0.0, 0.0], 0.1));
1132 }
1133
1134 #[test]
1135 fn test_cone_frustum_lateral_area_full_cone() {
1136 let c = Cone::new(1.0, 1.0); let area = c.frustum_lateral_area(-1.0, 1.0);
1139 let expected = c.lateral_surface_area();
1140 assert!(
1142 (area - expected).abs() < 1e-9,
1143 "frustum area {area} vs lateral {expected}"
1144 );
1145 }
1146
1147 #[test]
1148 fn test_cone_random_lateral_surface_point_on_surface() {
1149 let c = Cone::new(1.0, 2.0);
1150 for seed in [1u64, 42, 99, 1337, 65535] {
1151 let p = c.random_lateral_surface_point(seed);
1152 assert!(
1154 p[1] >= -c.half_height - 1e-9 && p[1] <= c.half_height + 1e-9,
1155 "y={} out of range",
1156 p[1]
1157 );
1158 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
1160 let r_at = c.radius_at_y(p[1]);
1161 assert!((xz - r_at).abs() < 1e-9, "xz={xz}, r_at={r_at}");
1162 }
1163 }
1164
1165 #[test]
1166 fn test_cone_ray_analytic_miss() {
1167 let c = Cone::new(1.0, 1.0);
1168 let result = c.ray_cone_analytic([0.0, 10.0, 0.0], [0.0, 1.0, 0.0], 5.0);
1170 assert!(result.is_none(), "ray going away should miss");
1171 }
1172
1173 #[test]
1176 fn test_cone_apex_angle() {
1177 let c = Cone::new(1.0, 1.0); let expected = 2.0 * (0.5_f64).atan();
1180 assert!((c.apex_angle() - expected).abs() < 1e-10);
1181 }
1182
1183 #[test]
1184 fn test_cone_apex_angle_90_degrees() {
1185 let c = Cone::new(1.0, 0.5); let expected = PI / 2.0;
1189 assert!((c.apex_angle() - expected).abs() < 1e-10);
1190 }
1191
1192 #[test]
1193 fn test_cone_double_cone_params() {
1194 let c = Cone::new(1.5, 2.0);
1195 let (r, hh) = c.double_cone_params();
1196 assert!((r - 1.5).abs() < 1e-12);
1197 assert!((hh - 2.0).abs() < 1e-12);
1198 }
1199
1200 #[test]
1201 fn test_cone_contains_point_double_cone_at_origin() {
1202 let c = Cone::new(1.0, 1.0);
1204 assert!(c.contains_point_double_cone([0.0, 0.0, 0.0]));
1205 }
1206
1207 #[test]
1208 fn test_cone_contains_point_double_cone_above_apex() {
1209 let c = Cone::new(1.0, 1.0);
1210 assert!(!c.contains_point_double_cone([0.0, 1.5, 0.0]));
1211 }
1212
1213 #[test]
1214 fn test_cone_contains_point_double_cone_on_surface() {
1215 let c = Cone::new(1.0, 1.0);
1216 assert!(c.contains_point_double_cone([1.0, 1.0, 0.0]));
1218 }
1219
1220 #[test]
1221 fn test_cone_unfold_lateral_origin() {
1222 let c = Cone::new(1.0, 1.0);
1223 let [x, y] = c.unfold_lateral(0.0, 0.0);
1225 assert!(x.abs() < 1e-12);
1226 assert!(y.abs() < 1e-12);
1227 }
1228
1229 #[test]
1230 fn test_cone_unfold_lateral_nonzero() {
1231 let c = Cone::new(1.0, 1.0);
1232 let [x, y] = c.unfold_lateral(0.0, 1.0);
1233 let len = (x * x + y * y).sqrt();
1234 assert!((len - 1.0).abs() < 1e-10, "len={len}");
1235 }
1236
1237 #[test]
1238 fn test_cone_bounding_cone_of_points_empty() {
1239 let (ha, _) = Cone::bounding_cone_of_points(&[]);
1240 assert_eq!(ha, 0.0);
1241 }
1242
1243 #[test]
1244 fn test_cone_bounding_cone_of_points_single() {
1245 let pts = [[1.0, 1.0, 0.0]];
1246 let (ha, _) = Cone::bounding_cone_of_points(&pts);
1247 let expected = PI / 4.0; assert!((ha - expected).abs() < 1e-9, "ha={ha} expected={expected}");
1249 }
1250
1251 #[test]
1252 fn test_cone_bounding_cone_contains_all_points() {
1253 let pts = [[0.1, 1.0, 0.0], [0.0, 1.0, 0.1], [0.0, 1.0, -0.1]];
1254 let (max_ha, _) = Cone::bounding_cone_of_points(&pts);
1255 for &p in &pts {
1257 let len = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
1258 let cos_a = (p[1] / len).clamp(-1.0, 1.0);
1259 let ha = cos_a.acos();
1260 assert!(ha <= max_ha + 1e-12, "ha={ha} > max={max_ha}");
1261 }
1262 }
1263
1264 #[test]
1265 fn test_cone_volume_swept_zero_delta() {
1266 let c = Cone::new(1.0, 1.0);
1267 let swept = c.volume_swept([0.0, 0.0, 0.0]);
1268 assert!((swept - c.volume()).abs() < 1e-10);
1269 }
1270
1271 #[test]
1272 fn test_cone_volume_swept_positive_delta() {
1273 let c = Cone::new(1.0, 1.0);
1274 let swept = c.volume_swept([1.0, 0.0, 0.0]);
1275 assert!(swept > c.volume());
1276 assert!((swept - c.volume() - PI).abs() < 1e-10);
1278 }
1279
1280 #[test]
1281 fn test_cone_sdf_gradient_outside_outward() {
1282 let c = Cone::new(1.0, 1.0);
1283 let g = c.sdf_gradient([5.0, 0.0, 0.0]);
1284 assert!(g[0] > 0.0, "gx={}", g[0]);
1286 }
1287
1288 #[test]
1289 fn test_cone_sdf_gradient_unit_length() {
1290 let c = Cone::new(1.0, 1.0);
1291 let g = c.sdf_gradient([3.0, 0.0, 0.0]);
1292 let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
1293 assert!((len - 1.0).abs() < 1e-6, "gradient not unit: len={len}");
1294 }
1295
1296 #[test]
1297 fn test_cone_ray_intersection_count_lateral() {
1298 let c = Cone::new(1.0, 2.0);
1299 let count = c.ray_intersection_count([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
1301 assert!(count >= 1, "expected >=1 hit, got {count}");
1302 }
1303
1304 #[test]
1305 fn test_cone_ray_intersection_count_miss() {
1306 let c = Cone::new(1.0, 1.0);
1307 let count = c.ray_intersection_count([0.0, 10.0, 0.0], [0.0, 1.0, 0.0], 5.0);
1308 assert_eq!(count, 0);
1309 }
1310
1311 #[test]
1312 fn test_cone_solid_angle_full_sphere() {
1313 let c = Cone::new(1.0, 0.5); let sa = c.solid_angle();
1317 let expected = 2.0 * PI * (1.0 - (PI / 4.0).cos());
1318 assert!((sa - expected).abs() < 1e-10);
1319 }
1320
1321 #[test]
1322 fn test_cone_solid_angle_positive() {
1323 let c = Cone::new(1.0, 1.0);
1324 assert!(c.solid_angle() > 0.0);
1325 }
1326
1327 #[test]
1328 fn test_cone_base_rim_points_count() {
1329 let c = Cone::new(1.0, 2.0);
1330 let pts = c.base_rim_points(12);
1331 assert_eq!(pts.len(), 12);
1332 }
1333
1334 #[test]
1335 fn test_cone_base_rim_points_on_base() {
1336 let c = Cone::new(1.5, 2.0);
1337 let pts = c.base_rim_points(16);
1338 for p in &pts {
1339 assert!((p[1] + c.half_height).abs() < 1e-12);
1340 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
1341 assert!((xz - c.radius).abs() < 1e-9);
1342 }
1343 }
1344
1345 #[test]
1346 fn test_cone_sphere_inside_cone_yes() {
1347 let c = Cone::new(2.0, 2.0);
1348 assert!(c.sphere_inside_cone([0.0, -1.9, 0.0], 0.05));
1350 }
1351
1352 #[test]
1353 fn test_cone_sphere_inside_cone_no() {
1354 let c = Cone::new(1.0, 1.0);
1355 assert!(!c.sphere_inside_cone([5.0, 0.0, 0.0], 1.0));
1357 }
1358
1359 #[test]
1360 fn test_cone_unfold_lateral_angle_scaling() {
1361 let c = Cone::new(1.0, 1.0);
1362 let p1 = c.unfold_lateral(0.0, 2.0);
1364 let p2 = c.unfold_lateral(PI / 2.0, 2.0);
1365 let d = ((p1[0] - p2[0]).powi(2) + (p1[1] - p2[1]).powi(2)).sqrt();
1366 assert!(d > 0.1, "unrolled points should differ");
1367 }
1368
1369 #[test]
1370 fn test_cone_double_cone_waist_radius_zero() {
1371 let c = Cone::new(2.0, 1.0);
1372 assert!(c.contains_point_double_cone([0.0, 0.0, 0.0]));
1375 }
1376}