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 pub fn volume_explicit(&self) -> Real {
32 let h = 2.0 * self.half_height;
33 PI * self.radius * self.radius * h / 3.0
34 }
35
36 pub fn surface_area(&self) -> Real {
39 let r = self.radius;
40 let h = 2.0 * self.half_height;
41 let slant = (r * r + h * h).sqrt();
42 PI * r * r + PI * r * slant
43 }
44
45 pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
47 let r2 = self.radius * self.radius;
48 let h2 = (2.0 * self.half_height).powi(2);
49 let iy = 3.0 * mass * r2 / 10.0;
50 let ixz = mass * (3.0 * r2 + 2.0 * h2) / 20.0;
51 [[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
52 }
53
54 pub fn ray_cast_array(
56 &self,
57 origin: [f64; 3],
58 direction: [f64; 3],
59 max_toi: f64,
60 ) -> Option<(f64, [f64; 3])> {
61 let o = Vec3::new(origin[0], origin[1], origin[2]);
62 let d = Vec3::new(direction[0], direction[1], direction[2]);
63 let hit = self.ray_cast(&o, &d, max_toi)?;
64 Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
65 }
66
67 pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
69 let px = p[0];
70 let py = p[1];
71 let pz = p[2];
72
73 let cy = py.clamp(-self.half_height, self.half_height);
75
76 let h = 2.0 * self.half_height;
78 let r_at_cy = self.radius * (self.half_height - cy) / h;
79
80 let xz_len = (px * px + pz * pz).sqrt();
82
83 if xz_len <= r_at_cy {
84 let dist_to_base = py - (-self.half_height);
87 let dist_to_side = r_at_cy - xz_len;
89 if dist_to_base <= dist_to_side {
90 [px, -self.half_height, pz]
91 } else {
92 if xz_len < 1e-12 {
94 [r_at_cy, cy, 0.0]
95 } else {
96 let s = r_at_cy / xz_len;
97 [px * s, cy, pz * s]
98 }
99 }
100 } else {
101 if xz_len < 1e-12 {
103 [r_at_cy, cy, 0.0]
104 } else {
105 let s = r_at_cy / xz_len;
106 [px * s, cy, pz * s]
107 }
108 }
109 }
110
111 pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
116 let h = 2.0 * self.half_height;
117 let xz_len = (direction[0] * direction[0] + direction[2] * direction[2]).sqrt();
118
119 let apex_dot = direction[1] * self.half_height;
120 let base_center_dot = -direction[1] * self.half_height;
121 let base_rim_dot = base_center_dot + self.radius * xz_len;
122
123 if apex_dot >= base_rim_dot {
124 [0.0, self.half_height, 0.0]
125 } else if xz_len > 1e-10 {
126 let _sin_angle = self.radius / (self.radius * self.radius + h * h).sqrt();
127 [
128 self.radius * direction[0] / xz_len,
129 -self.half_height,
130 self.radius * direction[2] / xz_len,
131 ]
132 } else {
133 [self.radius, -self.half_height, 0.0]
134 }
135 }
136
137 pub fn slant_height(&self) -> f64 {
139 let h = 2.0 * self.half_height;
140 (self.radius * self.radius + h * h).sqrt()
141 }
142
143 pub fn half_angle(&self) -> f64 {
145 let h = 2.0 * self.half_height;
146 (self.radius / h).atan()
147 }
148
149 pub fn lateral_surface_area(&self) -> f64 {
151 PI * self.radius * self.slant_height()
152 }
153
154 pub fn base_area(&self) -> f64 {
156 PI * self.radius * self.radius
157 }
158
159 pub fn contains_point(&self, p: [f64; 3]) -> bool {
161 if p[1] < -self.half_height || p[1] > self.half_height {
162 return false;
163 }
164 let h = 2.0 * self.half_height;
165 let r_at_y = self.radius * (self.half_height - p[1]) / h;
166 let xz2 = p[0] * p[0] + p[2] * p[2];
167 xz2 <= r_at_y * r_at_y
168 }
169
170 pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
173 let cp = self.closest_point(p);
174 let dx = p[0] - cp[0];
175 let dy = p[1] - cp[1];
176 let dz = p[2] - cp[2];
177 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
178 if self.contains_point(p) { -dist } else { dist }
179 }
180
181 pub fn radius_at_y(&self, y: f64) -> f64 {
183 let clamped = y.clamp(-self.half_height, self.half_height);
184 let h = 2.0 * self.half_height;
185 self.radius * (self.half_height - clamped) / h
186 }
187
188 pub fn truncated_cone(&self, cut_ratio: f64) -> (f64, f64, f64) {
192 let ratio = cut_ratio.clamp(0.0, 1.0);
193 let top_r = self.radius * (1.0 - ratio);
194 let bottom_r = self.radius;
195 let hh = self.half_height * ratio;
196 (top_r, bottom_r, hh)
197 }
198
199 pub fn truncated_cone_volume(&self, cut_ratio: f64) -> f64 {
202 let (r1, r2, _) = self.truncated_cone(cut_ratio);
203 let h = 2.0 * self.half_height * cut_ratio.clamp(0.0, 1.0);
204 PI * h * (r1 * r1 + r1 * r2 + r2 * r2) / 3.0
205 }
206
207 pub fn ray_cast_base_cap(
209 &self,
210 origin: [f64; 3],
211 direction: [f64; 3],
212 max_toi: f64,
213 ) -> Option<(f64, [f64; 3])> {
214 if direction[1].abs() < 1e-12 {
215 return None;
216 }
217 let t = (-self.half_height - origin[1]) / direction[1];
218 if t < 0.0 || t > max_toi {
219 return None;
220 }
221 let px = origin[0] + t * direction[0];
222 let pz = origin[2] + t * direction[2];
223 if px * px + pz * pz <= self.radius * self.radius {
224 Some((t, [0.0, -1.0, 0.0]))
225 } else {
226 None
227 }
228 }
229
230 pub fn closest_point_on_lateral(&self, p: [f64; 3]) -> [f64; 3] {
233 let xz_len = (p[0] * p[0] + p[2] * p[2]).sqrt();
234 let h = 2.0 * self.half_height;
235
236 let side_len = self.slant_height();
237 let side_dx = self.radius / side_len;
238 let side_dy = -h / side_len;
239
240 let vx = xz_len;
241 let vy = p[1] - self.half_height;
242
243 let t = (vx * side_dx + vy * side_dy).clamp(0.0, side_len);
244 let proj_xz = t * side_dx;
245 let proj_y = self.half_height + t * side_dy;
246
247 if xz_len < 1e-12 {
248 [proj_xz, proj_y, 0.0]
249 } else {
250 let s = proj_xz / xz_len;
251 [p[0] * s, proj_y, p[2] * s]
252 }
253 }
254
255 pub fn ray_cone_analytic(
261 &self,
262 origin: [f64; 3],
263 direction: [f64; 3],
264 max_toi: f64,
265 ) -> Option<(f64, [f64; 3], bool)> {
266 let o = Vec3::new(origin[0], origin[1], origin[2]);
267 let d = Vec3::new(direction[0], direction[1], direction[2]);
268
269 let h = 2.0 * self.half_height;
270 let k = self.radius / h;
271 let k2 = k * k;
272 let apex_y = self.half_height;
273
274 let fy = apex_y - o.y;
275 let a = d.x * d.x + d.z * d.z - k2 * d.y * d.y;
276 let b = 2.0 * (o.x * d.x + o.z * d.z + k2 * fy * d.y);
277 let c_val = o.x * o.x + o.z * o.z - k2 * fy * fy;
278
279 let mut best_t = f64::INFINITY;
280 let mut best_is_lateral = true;
281
282 if a.abs() > 1e-12 {
283 let disc = b * b - 4.0 * a * c_val;
284 if disc >= 0.0 {
285 let sqrt_disc = disc.sqrt();
286 for t in [(-b - sqrt_disc) / (2.0 * a), (-b + sqrt_disc) / (2.0 * a)] {
287 if t >= 0.0 && t <= max_toi {
288 let py = o.y + t * d.y;
289 if py >= -self.half_height && py <= self.half_height && t < best_t {
290 best_t = t;
291 best_is_lateral = true;
292 }
293 }
294 }
295 }
296 }
297
298 if d.y.abs() > 1e-12 {
300 let t = (-self.half_height - o.y) / d.y;
301 if t >= 0.0 && t <= max_toi {
302 let px = o.x + t * d.x;
303 let pz = o.z + t * d.z;
304 if px * px + pz * pz <= self.radius * self.radius && t < best_t {
305 best_t = t;
306 best_is_lateral = false;
307 }
308 }
309 }
310
311 if best_t.is_infinite() {
312 return None;
313 }
314
315 let hit_pt = [
316 origin[0] + best_t * direction[0],
317 origin[1] + best_t * direction[1],
318 origin[2] + best_t * direction[2],
319 ];
320 let normal = if best_is_lateral {
321 let xz_len = (hit_pt[0] * hit_pt[0] + hit_pt[2] * hit_pt[2])
322 .sqrt()
323 .max(1e-30);
324 let slope = self.radius / h;
325 let nx = hit_pt[0] / xz_len;
326 let nz = hit_pt[2] / xz_len;
327 let len = (nx * nx + slope * slope + nz * nz).sqrt();
328 [nx / len, slope / len, nz / len]
329 } else {
330 [0.0, -1.0, 0.0]
331 };
332
333 Some((best_t, normal, best_is_lateral))
334 }
335
336 pub fn intersects_plane(&self, plane_normal: [f64; 3], plane_d: f64) -> bool {
344 let apex = [0.0_f64, self.half_height, 0.0];
346 let apex_sd =
347 plane_normal[0] * apex[0] + plane_normal[1] * apex[1] + plane_normal[2] * apex[2]
348 - plane_d;
349
350 let base_center_sd = -plane_normal[1] * self.half_height - plane_d;
352
353 let xz_proj =
355 (plane_normal[0] * plane_normal[0] + plane_normal[2] * plane_normal[2]).sqrt();
356 let rim_sd = base_center_sd + self.radius * xz_proj;
357 let rim_sd_neg = base_center_sd - self.radius * xz_proj;
358
359 let signs = [apex_sd, rim_sd, rim_sd_neg];
361 let any_pos = signs.iter().any(|&s| s >= 0.0);
362 let any_neg = signs.iter().any(|&s| s <= 0.0);
363 any_pos && any_neg
364 }
365
366 pub fn frustum_from_cone(&self, y_bottom: f64, y_top: f64) -> (f64, f64, f64) {
373 let yb = y_bottom.clamp(-self.half_height, self.half_height);
374 let yt = y_top.clamp(yb, self.half_height);
375 let r_bottom = self.radius_at_y(yb);
376 let r_top = self.radius_at_y(yt);
377 let frustum_hh = (yt - yb) * 0.5;
378 (r_bottom, r_top, frustum_hh)
379 }
380
381 pub fn sdf(&self, p: [f64; 3]) -> f64 {
386 self.signed_distance(p)
387 }
388
389 pub fn intersects_sphere(&self, sphere_center: [f64; 3], sphere_radius: f64) -> bool {
395 let cp = self.closest_point(sphere_center);
396 let dx = sphere_center[0] - cp[0];
397 let dy = sphere_center[1] - cp[1];
398 let dz = sphere_center[2] - cp[2];
399 let dist_sq = dx * dx + dy * dy + dz * dz;
400 dist_sq <= sphere_radius * sphere_radius
401 }
402
403 pub fn frustum_lateral_area(&self, y_bottom: f64, y_top: f64) -> f64 {
408 let (r_bottom, r_top, _hh) = self.frustum_from_cone(y_bottom, y_top);
409 let dy = (y_top - y_bottom).abs();
410 let dr = (r_bottom - r_top).abs();
411 let slant = (dy * dy + dr * dr).sqrt();
412 PI * (r_bottom + r_top) * slant
413 }
414
415 pub fn apex_angle(&self) -> f64 {
421 2.0 * self.half_angle()
422 }
423
424 pub fn double_cone_params(&self) -> (f64, f64) {
429 (self.radius, self.half_height)
430 }
431
432 pub fn contains_point_double_cone(&self, p: [f64; 3]) -> bool {
437 let h = 2.0 * self.half_height;
438 let xz2 = p[0] * p[0] + p[2] * p[2];
439 let y_abs = p[1].abs();
440 if y_abs > self.half_height {
441 return false;
442 }
443 let r_at = self.radius * y_abs / self.half_height;
445 xz2 <= r_at * r_at + 1e-14 * h
446 }
447
448 pub fn unfold_lateral(&self, u: f64, t_slant: f64) -> [f64; 2] {
454 let sin_alpha = self.radius / self.slant_height().max(1e-14);
457 let phi = u * sin_alpha;
458 [t_slant * phi.cos(), t_slant * phi.sin()]
459 }
460
461 pub fn bounding_cone_of_points(points: &[[f64; 3]]) -> (f64, f64) {
467 if points.is_empty() {
468 return (0.0, 0.0);
469 }
470 let mut max_half_angle: f64 = 0.0;
471 let mut max_elev: f64 = 0.0;
472 for &p in points {
473 let len = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
474 if len < 1e-14 {
475 continue;
476 }
477 let cos_a = (p[1] / len).clamp(-1.0, 1.0);
479 let half_angle = cos_a.acos();
480 let elev = p[1] / len;
481 if half_angle > max_half_angle {
482 max_half_angle = half_angle;
483 max_elev = elev;
484 }
485 }
486 (max_half_angle, max_elev)
487 }
488
489 pub fn volume_swept(&self, delta: [f64; 3]) -> f64 {
493 let dist = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
494 self.volume() + self.base_area() * dist
495 }
496
497 pub fn sdf_gradient(&self, p: [f64; 3]) -> [f64; 3] {
501 let eps = 1e-5;
502 let s0 = self.sdf(p);
503 let gx = (self.sdf([p[0] + eps, p[1], p[2]]) - s0) / eps;
504 let gy = (self.sdf([p[0], p[1] + eps, p[2]]) - s0) / eps;
505 let gz = (self.sdf([p[0], p[1], p[2] + eps]) - s0) / eps;
506 let len = (gx * gx + gy * gy + gz * gz).sqrt().max(1e-14);
507 [gx / len, gy / len, gz / len]
508 }
509
510 pub fn ray_intersection_count(
514 &self,
515 origin: [f64; 3],
516 direction: [f64; 3],
517 max_toi: f64,
518 ) -> usize {
519 let mut count = 0usize;
520 let h = 2.0 * self.half_height;
521 let k = self.radius / h;
522 let k2 = k * k;
523 let apex_y = self.half_height;
524 let fy = apex_y - origin[1];
525 let a = direction[0] * direction[0] + direction[2] * direction[2]
526 - k2 * direction[1] * direction[1];
527 let b =
528 2.0 * (origin[0] * direction[0] + origin[2] * direction[2] + k2 * fy * direction[1]);
529 let c = origin[0] * origin[0] + origin[2] * origin[2] - k2 * fy * fy;
530 if a.abs() > 1e-12 {
531 let disc = b * b - 4.0 * a * c;
532 if disc >= 0.0 {
533 let sq = disc.sqrt();
534 for t in [(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)] {
535 if t >= 0.0 && t <= max_toi {
536 let py = origin[1] + t * direction[1];
537 if py >= -self.half_height && py <= self.half_height {
538 count += 1;
539 }
540 }
541 }
542 }
543 }
544 if direction[1].abs() > 1e-12 {
546 let t = (-self.half_height - origin[1]) / direction[1];
547 if t >= 0.0 && t <= max_toi {
548 let px = origin[0] + t * direction[0];
549 let pz = origin[2] + t * direction[2];
550 if px * px + pz * pz <= self.radius * self.radius {
551 count += 1;
552 }
553 }
554 }
555 count
556 }
557
558 pub fn solid_angle(&self) -> f64 {
562 let alpha = self.half_angle();
563 2.0 * PI * (1.0 - alpha.cos())
564 }
565
566 pub fn base_rim_points(&self, n: usize) -> Vec<[f64; 3]> {
570 let n = n.max(3);
571 (0..n)
572 .map(|i| {
573 let t = 2.0 * PI * i as f64 / n as f64;
574 [
575 self.radius * t.cos(),
576 -self.half_height,
577 self.radius * t.sin(),
578 ]
579 })
580 .collect()
581 }
582
583 pub fn sphere_inside_cone(&self, center: [f64; 3], sphere_radius: f64) -> bool {
586 let sd = self.sdf(center);
590 sd + sphere_radius <= 0.0
591 }
592
593 pub fn random_lateral_surface_point(&self, seed: u64) -> [f64; 3] {
595 let mut state = seed;
597 let xorshift = |s: &mut u64| -> f64 {
598 *s ^= *s << 13;
599 *s ^= *s >> 7;
600 *s ^= *s << 17;
601 (*s as f64) / (u64::MAX as f64)
602 };
603
604 let u = xorshift(&mut state);
609 let frac_t = 1.0 - (1.0 - u).sqrt(); let y = self.half_height - frac_t * 2.0 * self.half_height;
611 let r_at_y = self.radius_at_y(y);
612 let theta = xorshift(&mut state) * 2.0 * PI;
613 [r_at_y * theta.cos(), y, r_at_y * theta.sin()]
614 }
615}
616
617impl Shape for Cone {
618 fn bounding_box(&self) -> Aabb {
619 Aabb::new(
620 Vec3::new(-self.radius, -self.half_height, -self.radius),
621 Vec3::new(self.radius, self.half_height, self.radius),
622 )
623 }
624
625 fn support_point(&self, direction: &Vec3) -> Vec3 {
626 let h = 2.0 * self.half_height;
627 let xz_len = (direction.x * direction.x + direction.z * direction.z).sqrt();
628
629 let apex_dot = direction.y * self.half_height;
631 let base_center_dot = -direction.y * self.half_height;
632 let base_rim_dot = base_center_dot + self.radius * xz_len;
633
634 if apex_dot >= base_rim_dot {
635 Vec3::new(0.0, self.half_height, 0.0)
636 } else if xz_len > 1e-10 {
637 let sin_angle = self.radius / (self.radius * self.radius + h * h).sqrt();
638 let _ = sin_angle; Vec3::new(
640 self.radius * direction.x / xz_len,
641 -self.half_height,
642 self.radius * direction.z / xz_len,
643 )
644 } else {
645 Vec3::new(self.radius, -self.half_height, 0.0)
646 }
647 }
648
649 fn volume(&self) -> Real {
650 let h = 2.0 * self.half_height;
651 PI * self.radius * self.radius * h / 3.0
652 }
653
654 fn center_of_mass(&self) -> Vec3 {
655 Vec3::new(0.0, -self.half_height * 0.5, 0.0)
657 }
658
659 fn inertia_tensor(&self, mass: Real) -> Mat3 {
660 let r2 = self.radius * self.radius;
661 let h2 = (2.0 * self.half_height).powi(2);
663 let iy = 3.0 * mass * r2 / 10.0;
665 let ixz = mass * (3.0 * r2 + 2.0 * h2) / 20.0;
667 Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
668 }
669
670 fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
671 let h = 2.0 * self.half_height;
677 let k = self.radius / h;
678 let k2 = k * k;
679 let apex_y = self.half_height;
680
681 let ox = ray_origin.x;
682 let oy = ray_origin.y;
683 let oz = ray_origin.z;
684 let dx = ray_direction.x;
685 let dy = ray_direction.y;
686 let dz = ray_direction.z;
687
688 let fy = apex_y - oy;
691 let a = dx * dx + dz * dz - k2 * dy * dy;
692 let b = 2.0 * (ox * dx + oz * dz + k2 * fy * dy);
693 let c_val = ox * ox + oz * oz - k2 * fy * fy;
694
695 let mut best: Option<RayHit> = None;
696
697 let try_t = |t: Real, best: &mut Option<RayHit>| {
698 if t < 0.0 || t > max_toi {
699 return;
700 }
701 let p = ray_origin + ray_direction * t;
702 if p.y < -self.half_height || p.y > self.half_height {
703 return;
704 }
705 let xz_len = (p.x * p.x + p.z * p.z).sqrt();
707 let normal = if xz_len > 1e-12 {
708 let slope = self.radius / h;
709 Vec3::new(p.x / xz_len, slope, p.z / xz_len).normalize()
710 } else {
711 Vec3::new(0.0, 1.0, 0.0)
712 };
713 if best.as_ref().is_none_or(|prev| t < prev.toi) {
714 *best = Some(RayHit {
715 point: p,
716 normal,
717 toi: t,
718 });
719 }
720 };
721
722 if a.abs() > 1e-12 {
723 let disc = b * b - 4.0 * a * c_val;
724 if disc >= 0.0 {
725 let sqrt_disc = disc.sqrt();
726 try_t((-b - sqrt_disc) / (2.0 * a), &mut best);
727 try_t((-b + sqrt_disc) / (2.0 * a), &mut best);
728 }
729 } else if b.abs() > 1e-12 {
730 try_t(-c_val / b, &mut best);
731 }
732
733 if ray_direction.y.abs() > 1e-12 {
735 let t = (-self.half_height - ray_origin.y) / ray_direction.y;
736 if t >= 0.0 && t <= max_toi {
737 let p = ray_origin + ray_direction * t;
738 if p.x * p.x + p.z * p.z <= self.radius * self.radius {
739 let normal = Vec3::new(0.0, -1.0, 0.0);
740 if best.as_ref().is_none_or(|prev| t < prev.toi) {
741 best = Some(RayHit {
742 point: p,
743 normal,
744 toi: t,
745 });
746 }
747 }
748 }
749 }
750
751 best
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[test]
760 fn test_cone_volume() {
761 let c = Cone::new(1.0, 1.5);
762 let expected = PI * 1.0 * 3.0 / 3.0;
763 assert!((c.volume() - expected).abs() < 1e-10);
764 }
765
766 #[test]
767 fn test_cone_surface_area() {
768 let c = Cone::new(1.0, 1.0);
770 let slant = (1.0_f64 + 4.0_f64).sqrt();
771 let expected = PI + PI * slant;
772 assert!((c.surface_area() - expected).abs() < 1e-10);
773 }
774
775 #[test]
776 fn test_cone_support_apex() {
777 let c = Cone::new(1.0, 2.0);
778 let sp = c.support_point(&Vec3::new(0.0, 1.0, 0.0));
779 assert!((sp.y - 2.0).abs() < 1e-10);
780 }
781
782 #[test]
783 fn test_cone_inertia_symmetry() {
784 let c = Cone::new(1.5, 2.0);
785 let it = c.inertia_tensor(3.0);
786 let diff = (it[(0, 0)] - it[(2, 2)]).abs();
788 assert!(
789 diff < 1e-10,
790 "I_xx != I_zz: {} vs {}",
791 it[(0, 0)],
792 it[(2, 2)]
793 );
794 assert!(it[(0, 1)].abs() < 1e-10);
796 assert!(it[(1, 0)].abs() < 1e-10);
797 }
798
799 #[test]
800 fn test_cone_inertia_array() {
801 let c = Cone::new(1.0, 1.0);
802 let it = c.inertia_tensor_array(1.0);
803 assert!(it[0][0] > 0.0);
804 assert!(it[1][1] > 0.0);
805 assert!((it[0][0] - it[2][2]).abs() < 1e-10);
806 }
807
808 #[test]
809 fn test_cone_raycast_side() {
810 let c = Cone::new(1.0, 2.0); let origin = Vec3::new(-5.0, 0.0, 0.0);
814 let dir = Vec3::new(1.0, 0.0, 0.0);
815 let hit = c.ray_cast(&origin, &dir, 100.0);
817 assert!(hit.is_some(), "expected a hit on cone side");
818 let hit = hit.unwrap();
819 assert!(hit.toi > 0.0);
820 assert!(
821 (hit.point.x + 0.5).abs() < 1e-6,
822 "expected hit at x=-0.5, got x={}",
823 hit.point.x
824 );
825 }
826
827 #[test]
828 fn test_cone_raycast_base_cap() {
829 let c = Cone::new(1.0, 1.0); let origin = Vec3::new(0.0, -5.0, 0.0);
832 let dir = Vec3::new(0.0, 1.0, 0.0);
833 let hit = c.ray_cast(&origin, &dir, 100.0);
834 assert!(hit.is_some(), "expected a hit on cone base");
835 let hit = hit.unwrap();
836 assert!(
837 (hit.toi - 4.0).abs() < 1e-6,
838 "expected toi=4, got {}",
839 hit.toi
840 );
841 assert!((hit.normal.y + 1.0).abs() < 1e-10);
842 }
843
844 #[test]
845 fn test_cone_closest_point_outside() {
846 let c = Cone::new(1.0, 1.0); let cp = c.closest_point([5.0, 0.0, 0.0]);
849 assert!(
851 (cp[0] - 0.5).abs() < 1e-6,
852 "expected cp[0]=0.5, got {}",
853 cp[0]
854 );
855 assert!((cp[1]).abs() < 1e-6);
856 }
857
858 #[test]
861 fn test_cone_support_array_apex() {
862 let c = Cone::new(1.0, 2.0);
863 let sp = c.support([0.0, 1.0, 0.0]);
864 assert!((sp[1] - 2.0).abs() < 1e-10);
865 }
866
867 #[test]
868 fn test_cone_support_array_base_rim() {
869 let c = Cone::new(1.0, 2.0);
870 let sp = c.support([1.0, -1.0, 0.0]);
871 assert!((sp[0] - 1.0).abs() < 1e-10);
872 assert!((sp[1] + 2.0).abs() < 1e-10);
873 }
874
875 #[test]
876 fn test_cone_slant_height() {
877 let c = Cone::new(3.0, 2.0); let expected = (9.0 + 16.0_f64).sqrt(); assert!((c.slant_height() - expected).abs() < 1e-10);
880 }
881
882 #[test]
883 fn test_cone_half_angle() {
884 let c = Cone::new(1.0, 1.0); let expected = (0.5_f64).atan(); assert!((c.half_angle() - expected).abs() < 1e-10);
887 }
888
889 #[test]
890 fn test_cone_lateral_surface_area() {
891 let c = Cone::new(1.0, 1.0);
892 let slant = c.slant_height();
893 let expected = PI * 1.0 * slant;
894 assert!((c.lateral_surface_area() - expected).abs() < 1e-10);
895 }
896
897 #[test]
898 fn test_cone_base_area() {
899 let c = Cone::new(2.0, 1.0);
900 assert!((c.base_area() - 4.0 * PI).abs() < 1e-10);
901 }
902
903 #[test]
904 fn test_cone_surface_area_decomposition() {
905 let c = Cone::new(1.5, 2.0);
906 let total = c.surface_area();
907 let decomposed = c.lateral_surface_area() + c.base_area();
908 assert!((total - decomposed).abs() < 1e-10);
909 }
910
911 #[test]
912 fn test_cone_contains_point() {
913 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])); }
919
920 #[test]
921 fn test_cone_contains_point_base_rim() {
922 let c = Cone::new(1.0, 1.0);
923 assert!(c.contains_point([1.0, -1.0, 0.0]));
925 assert!(!c.contains_point([1.1, -1.0, 0.0]));
926 }
927
928 #[test]
929 fn test_cone_signed_distance_inside() {
930 let c = Cone::new(2.0, 2.0);
931 let d = c.signed_distance([0.0, -2.0, 0.0]); assert!(d <= 0.0);
933 }
934
935 #[test]
936 fn test_cone_signed_distance_outside() {
937 let c = Cone::new(1.0, 1.0);
938 let d = c.signed_distance([5.0, 0.0, 0.0]);
939 assert!(d > 0.0);
940 }
941
942 #[test]
943 fn test_cone_radius_at_y() {
944 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); }
949
950 #[test]
951 fn test_cone_truncated_cone() {
952 let c = Cone::new(2.0, 2.0);
953 let (top_r, bot_r, hh) = c.truncated_cone(0.5);
954 assert!((bot_r - 2.0).abs() < 1e-10);
955 assert!((top_r - 1.0).abs() < 1e-10); assert!((hh - 1.0).abs() < 1e-10); }
958
959 #[test]
960 fn test_cone_truncated_cone_full() {
961 let c = Cone::new(2.0, 2.0);
962 let (top_r, _, _) = c.truncated_cone(1.0);
963 assert!(top_r.abs() < 1e-10); }
965
966 #[test]
967 fn test_cone_truncated_cone_volume() {
968 let c = Cone::new(1.0, 1.0);
969 let full_vol = c.volume();
971 let trunc_vol = c.truncated_cone_volume(1.0);
973 assert!((trunc_vol - full_vol).abs() < 1e-10);
974 }
975
976 #[test]
977 fn test_cone_ray_cast_base_cap_array() {
978 let c = Cone::new(1.0, 1.0);
979 let result = c.ray_cast_base_cap([0.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
980 assert!(result.is_some());
981 let (t, n) = result.unwrap();
982 assert!((t - 4.0).abs() < 1e-10);
983 assert!((n[1] + 1.0).abs() < 1e-10);
984 }
985
986 #[test]
987 fn test_cone_ray_cast_base_cap_miss() {
988 let c = Cone::new(1.0, 1.0);
989 let result = c.ray_cast_base_cap([5.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
990 assert!(result.is_none());
991 }
992
993 #[test]
994 fn test_cone_closest_point_on_lateral() {
995 let c = Cone::new(1.0, 1.0);
996 let cp = c.closest_point_on_lateral([5.0, 0.0, 0.0]);
998 assert!(cp[0] > 0.0);
1001 let r = (cp[0] * cp[0] + cp[2] * cp[2]).sqrt();
1003 let expected_r = c.radius_at_y(cp[1]);
1004 assert!((r - expected_r).abs() < 0.1);
1005 }
1006
1007 #[test]
1008 fn test_cone_closest_point_on_lateral_apex() {
1009 let c = Cone::new(1.0, 1.0);
1010 let cp = c.closest_point_on_lateral([0.0, 5.0, 0.0]);
1012 assert!((cp[1] - 1.0).abs() < 1e-10);
1014 assert!(cp[0].abs() < 1e-10);
1015 }
1016
1017 #[test]
1018 fn test_cone_volume_explicit_matches() {
1019 let c = Cone::new(1.5, 2.5);
1020 assert!((c.volume_explicit() - c.volume()).abs() < 1e-10);
1021 }
1022
1023 #[test]
1026 fn test_cone_ray_analytic_lateral() {
1027 let c = Cone::new(1.0, 2.0);
1028 let result = c.ray_cone_analytic([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
1029 assert!(result.is_some(), "should hit lateral surface");
1030 let (t, n, is_lateral) = result.unwrap();
1031 assert!(t > 0.0);
1032 assert!(is_lateral, "should be lateral hit");
1033 assert!(
1034 n[1] > 0.0,
1035 "normal should have positive Y component for lateral cone"
1036 );
1037 }
1038
1039 #[test]
1040 fn test_cone_ray_analytic_base() {
1041 let c = Cone::new(1.0, 1.0);
1042 let result = c.ray_cone_analytic([0.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
1043 assert!(result.is_some());
1044 let (t, n, is_lateral) = result.unwrap();
1045 assert!((t - 4.0).abs() < 1e-6, "toi should be 4, got {t}");
1046 assert!(!is_lateral);
1047 assert!((n[1] + 1.0).abs() < 1e-10);
1048 }
1049
1050 #[test]
1051 fn test_cone_plane_intersection_horizontal_through_base() {
1052 let c = Cone::new(1.0, 1.0);
1053 let hit = c.intersects_plane([0.0, 1.0, 0.0], -1.0);
1055 assert!(hit, "plane through base should intersect cone");
1056 }
1057
1058 #[test]
1059 fn test_cone_plane_intersection_above_apex() {
1060 let c = Cone::new(1.0, 1.0);
1061 let hit = c.intersects_plane([0.0, 1.0, 0.0], 5.0);
1063 assert!(!hit, "plane far above should not intersect cone");
1064 }
1065
1066 #[test]
1067 fn test_cone_frustum_from_cone() {
1068 let c = Cone::new(2.0, 2.0); let (r_bot, r_top, hh) = c.frustum_from_cone(-1.0, 1.0);
1071 assert!((r_bot - 1.5).abs() < 1e-10, "r_bot={r_bot}");
1073 assert!((r_top - 0.5).abs() < 1e-10, "r_top={r_top}");
1075 assert!((hh - 1.0).abs() < 1e-10, "hh={hh}");
1076 }
1077
1078 #[test]
1079 fn test_cone_sdf_matches_signed_distance() {
1080 let c = Cone::new(1.0, 1.0);
1081 let p = [3.0, 0.0, 0.0];
1082 assert!((c.sdf(p) - c.signed_distance(p)).abs() < 1e-12);
1083 }
1084
1085 #[test]
1086 fn test_cone_intersects_sphere_overlap() {
1087 let c = Cone::new(1.0, 1.0);
1088 assert!(c.intersects_sphere([0.0, 0.0, 0.0], 5.0));
1090 }
1091
1092 #[test]
1093 fn test_cone_intersects_sphere_no_overlap() {
1094 let c = Cone::new(1.0, 1.0);
1095 assert!(!c.intersects_sphere([10.0, 0.0, 0.0], 0.1));
1097 }
1098
1099 #[test]
1100 fn test_cone_frustum_lateral_area_full_cone() {
1101 let c = Cone::new(1.0, 1.0); let area = c.frustum_lateral_area(-1.0, 1.0);
1104 let expected = c.lateral_surface_area();
1105 assert!(
1107 (area - expected).abs() < 1e-9,
1108 "frustum area {area} vs lateral {expected}"
1109 );
1110 }
1111
1112 #[test]
1113 fn test_cone_random_lateral_surface_point_on_surface() {
1114 let c = Cone::new(1.0, 2.0);
1115 for seed in [1u64, 42, 99, 1337, 65535] {
1116 let p = c.random_lateral_surface_point(seed);
1117 assert!(
1119 p[1] >= -c.half_height - 1e-9 && p[1] <= c.half_height + 1e-9,
1120 "y={} out of range",
1121 p[1]
1122 );
1123 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
1125 let r_at = c.radius_at_y(p[1]);
1126 assert!((xz - r_at).abs() < 1e-9, "xz={xz}, r_at={r_at}");
1127 }
1128 }
1129
1130 #[test]
1131 fn test_cone_ray_analytic_miss() {
1132 let c = Cone::new(1.0, 1.0);
1133 let result = c.ray_cone_analytic([0.0, 10.0, 0.0], [0.0, 1.0, 0.0], 5.0);
1135 assert!(result.is_none(), "ray going away should miss");
1136 }
1137
1138 #[test]
1141 fn test_cone_apex_angle() {
1142 let c = Cone::new(1.0, 1.0); let expected = 2.0 * (0.5_f64).atan();
1145 assert!((c.apex_angle() - expected).abs() < 1e-10);
1146 }
1147
1148 #[test]
1149 fn test_cone_apex_angle_90_degrees() {
1150 let c = Cone::new(1.0, 0.5); let expected = PI / 2.0;
1154 assert!((c.apex_angle() - expected).abs() < 1e-10);
1155 }
1156
1157 #[test]
1158 fn test_cone_double_cone_params() {
1159 let c = Cone::new(1.5, 2.0);
1160 let (r, hh) = c.double_cone_params();
1161 assert!((r - 1.5).abs() < 1e-12);
1162 assert!((hh - 2.0).abs() < 1e-12);
1163 }
1164
1165 #[test]
1166 fn test_cone_contains_point_double_cone_at_origin() {
1167 let c = Cone::new(1.0, 1.0);
1169 assert!(c.contains_point_double_cone([0.0, 0.0, 0.0]));
1170 }
1171
1172 #[test]
1173 fn test_cone_contains_point_double_cone_above_apex() {
1174 let c = Cone::new(1.0, 1.0);
1175 assert!(!c.contains_point_double_cone([0.0, 1.5, 0.0]));
1176 }
1177
1178 #[test]
1179 fn test_cone_contains_point_double_cone_on_surface() {
1180 let c = Cone::new(1.0, 1.0);
1181 assert!(c.contains_point_double_cone([1.0, 1.0, 0.0]));
1183 }
1184
1185 #[test]
1186 fn test_cone_unfold_lateral_origin() {
1187 let c = Cone::new(1.0, 1.0);
1188 let [x, y] = c.unfold_lateral(0.0, 0.0);
1190 assert!(x.abs() < 1e-12);
1191 assert!(y.abs() < 1e-12);
1192 }
1193
1194 #[test]
1195 fn test_cone_unfold_lateral_nonzero() {
1196 let c = Cone::new(1.0, 1.0);
1197 let [x, y] = c.unfold_lateral(0.0, 1.0);
1198 let len = (x * x + y * y).sqrt();
1199 assert!((len - 1.0).abs() < 1e-10, "len={len}");
1200 }
1201
1202 #[test]
1203 fn test_cone_bounding_cone_of_points_empty() {
1204 let (ha, _) = Cone::bounding_cone_of_points(&[]);
1205 assert_eq!(ha, 0.0);
1206 }
1207
1208 #[test]
1209 fn test_cone_bounding_cone_of_points_single() {
1210 let pts = [[1.0, 1.0, 0.0]];
1211 let (ha, _) = Cone::bounding_cone_of_points(&pts);
1212 let expected = PI / 4.0; assert!((ha - expected).abs() < 1e-9, "ha={ha} expected={expected}");
1214 }
1215
1216 #[test]
1217 fn test_cone_bounding_cone_contains_all_points() {
1218 let pts = [[0.1, 1.0, 0.0], [0.0, 1.0, 0.1], [0.0, 1.0, -0.1]];
1219 let (max_ha, _) = Cone::bounding_cone_of_points(&pts);
1220 for &p in &pts {
1222 let len = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
1223 let cos_a = (p[1] / len).clamp(-1.0, 1.0);
1224 let ha = cos_a.acos();
1225 assert!(ha <= max_ha + 1e-12, "ha={ha} > max={max_ha}");
1226 }
1227 }
1228
1229 #[test]
1230 fn test_cone_volume_swept_zero_delta() {
1231 let c = Cone::new(1.0, 1.0);
1232 let swept = c.volume_swept([0.0, 0.0, 0.0]);
1233 assert!((swept - c.volume()).abs() < 1e-10);
1234 }
1235
1236 #[test]
1237 fn test_cone_volume_swept_positive_delta() {
1238 let c = Cone::new(1.0, 1.0);
1239 let swept = c.volume_swept([1.0, 0.0, 0.0]);
1240 assert!(swept > c.volume());
1241 assert!((swept - c.volume() - PI).abs() < 1e-10);
1243 }
1244
1245 #[test]
1246 fn test_cone_sdf_gradient_outside_outward() {
1247 let c = Cone::new(1.0, 1.0);
1248 let g = c.sdf_gradient([5.0, 0.0, 0.0]);
1249 assert!(g[0] > 0.0, "gx={}", g[0]);
1251 }
1252
1253 #[test]
1254 fn test_cone_sdf_gradient_unit_length() {
1255 let c = Cone::new(1.0, 1.0);
1256 let g = c.sdf_gradient([3.0, 0.0, 0.0]);
1257 let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
1258 assert!((len - 1.0).abs() < 1e-6, "gradient not unit: len={len}");
1259 }
1260
1261 #[test]
1262 fn test_cone_ray_intersection_count_lateral() {
1263 let c = Cone::new(1.0, 2.0);
1264 let count = c.ray_intersection_count([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
1266 assert!(count >= 1, "expected >=1 hit, got {count}");
1267 }
1268
1269 #[test]
1270 fn test_cone_ray_intersection_count_miss() {
1271 let c = Cone::new(1.0, 1.0);
1272 let count = c.ray_intersection_count([0.0, 10.0, 0.0], [0.0, 1.0, 0.0], 5.0);
1273 assert_eq!(count, 0);
1274 }
1275
1276 #[test]
1277 fn test_cone_solid_angle_full_sphere() {
1278 let c = Cone::new(1.0, 0.5); let sa = c.solid_angle();
1282 let expected = 2.0 * PI * (1.0 - (PI / 4.0).cos());
1283 assert!((sa - expected).abs() < 1e-10);
1284 }
1285
1286 #[test]
1287 fn test_cone_solid_angle_positive() {
1288 let c = Cone::new(1.0, 1.0);
1289 assert!(c.solid_angle() > 0.0);
1290 }
1291
1292 #[test]
1293 fn test_cone_base_rim_points_count() {
1294 let c = Cone::new(1.0, 2.0);
1295 let pts = c.base_rim_points(12);
1296 assert_eq!(pts.len(), 12);
1297 }
1298
1299 #[test]
1300 fn test_cone_base_rim_points_on_base() {
1301 let c = Cone::new(1.5, 2.0);
1302 let pts = c.base_rim_points(16);
1303 for p in &pts {
1304 assert!((p[1] + c.half_height).abs() < 1e-12);
1305 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
1306 assert!((xz - c.radius).abs() < 1e-9);
1307 }
1308 }
1309
1310 #[test]
1311 fn test_cone_sphere_inside_cone_yes() {
1312 let c = Cone::new(2.0, 2.0);
1313 assert!(c.sphere_inside_cone([0.0, -1.9, 0.0], 0.05));
1315 }
1316
1317 #[test]
1318 fn test_cone_sphere_inside_cone_no() {
1319 let c = Cone::new(1.0, 1.0);
1320 assert!(!c.sphere_inside_cone([5.0, 0.0, 0.0], 1.0));
1322 }
1323
1324 #[test]
1325 fn test_cone_unfold_lateral_angle_scaling() {
1326 let c = Cone::new(1.0, 1.0);
1327 let p1 = c.unfold_lateral(0.0, 2.0);
1329 let p2 = c.unfold_lateral(PI / 2.0, 2.0);
1330 let d = ((p1[0] - p2[0]).powi(2) + (p1[1] - p2[1]).powi(2)).sqrt();
1331 assert!(d > 0.1, "unrolled points should differ");
1332 }
1333
1334 #[test]
1335 fn test_cone_double_cone_waist_radius_zero() {
1336 let c = Cone::new(2.0, 1.0);
1337 assert!(c.contains_point_double_cone([0.0, 0.0, 0.0]));
1340 }
1341}