1#![allow(clippy::manual_strip, clippy::ptr_arg)]
2#![allow(clippy::needless_range_loop)]
3#![allow(dead_code)]
12#![allow(missing_docs)]
13
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::f64::consts::PI;
17
18fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
23 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
24}
25
26fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
27 [
28 a[1] * b[2] - a[2] * b[1],
29 a[2] * b[0] - a[0] * b[2],
30 a[0] * b[1] - a[1] * b[0],
31 ]
32}
33
34fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
35 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
36}
37
38fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
39 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
40}
41
42fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
43 [a[0] * s, a[1] * s, a[2] * s]
44}
45
46fn len3(a: [f64; 3]) -> f64 {
47 dot3(a, a).sqrt()
48}
49
50fn normalize3(a: [f64; 3]) -> [f64; 3] {
51 let l = len3(a);
52 if l < 1e-15 {
53 [0.0; 3]
54 } else {
55 scale3(a, 1.0 / l)
56 }
57}
58
59fn lerp3(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
60 [
61 a[0] + (b[0] - a[0]) * t,
62 a[1] + (b[1] - a[1]) * t,
63 a[2] + (b[2] - a[2]) * t,
64 ]
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub enum PyShape {
74 Sphere(f64),
76 Box([f64; 3]),
78 Capsule { radius: f64, half_height: f64 },
80 Cylinder { radius: f64, half_height: f64 },
82 Cone { radius: f64, half_height: f64 },
84 Torus { major_r: f64, minor_r: f64 },
86}
87
88impl PyShape {
89 pub fn volume(&self) -> f64 {
91 use std::f64::consts::PI;
92 match self {
93 PyShape::Sphere(r) => (4.0 / 3.0) * PI * r * r * r,
94 PyShape::Box(h) => 8.0 * h[0] * h[1] * h[2],
95 PyShape::Capsule {
96 radius: r,
97 half_height: hh,
98 } => PI * r * r * (2.0 * hh + (4.0 / 3.0) * r),
99 PyShape::Cylinder {
100 radius: r,
101 half_height: hh,
102 } => PI * r * r * 2.0 * hh,
103 PyShape::Cone {
104 radius: r,
105 half_height: hh,
106 } => (1.0 / 3.0) * PI * r * r * 2.0 * hh,
107 PyShape::Torus {
108 major_r: rr,
109 minor_r: rt,
110 } => 2.0 * PI * PI * rr * rt * rt,
111 }
112 }
113
114 pub fn surface_area(&self) -> f64 {
116 match self {
117 PyShape::Sphere(r) => 4.0 * PI * r * r,
118 PyShape::Box(h) => 2.0 * (h[0] * h[1] + h[1] * h[2] + h[2] * h[0]) * 4.0,
119 PyShape::Capsule {
120 radius: r,
121 half_height: hh,
122 } => 4.0 * PI * r * r + 2.0 * PI * r * 2.0 * hh,
123 PyShape::Cylinder {
124 radius: r,
125 half_height: hh,
126 } => 2.0 * PI * r * (r + 2.0 * hh),
127 PyShape::Cone {
128 radius: r,
129 half_height: hh,
130 } => {
131 let slant = (r * r + (2.0 * hh) * (2.0 * hh)).sqrt();
132 PI * r * (r + slant)
133 }
134 PyShape::Torus {
135 major_r: rr,
136 minor_r: rt,
137 } => 4.0 * PI * PI * rr * rt,
138 }
139 }
140
141 pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
143 match self {
144 PyShape::Sphere(r) => ([-r, -r, -r], [*r, *r, *r]),
145 PyShape::Box(h) => ([-h[0], -h[1], -h[2]], [h[0], h[1], h[2]]),
146 PyShape::Capsule {
147 radius: r,
148 half_height: hh,
149 } => {
150 let hy = r + hh;
151 ([-r, -hy, -r], [*r, hy, *r])
152 }
153 PyShape::Cylinder {
154 radius: r,
155 half_height: hh,
156 } => ([-r, -hh, -r], [*r, *hh, *r]),
157 PyShape::Cone {
158 radius: r,
159 half_height: hh,
160 } => ([-r, -hh, -r], [*r, *hh, *r]),
161 PyShape::Torus {
162 major_r: rr,
163 minor_r: rt,
164 } => {
165 let e = rr + rt;
166 ([-e, -rt, -e], [e, *rt, e])
167 }
168 }
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct PyConvexHull {
181 pub vertices: Vec<[f64; 3]>,
183 pub faces: Vec<usize>,
185}
186
187impl PyConvexHull {
188 pub fn from_points(points: &[[f64; 3]]) -> Self {
193 if points.is_empty() {
194 return Self {
195 vertices: vec![],
196 faces: vec![],
197 };
198 }
199 let mut min = points[0];
201 let mut max = points[0];
202 for &p in points {
203 for i in 0..3 {
204 if p[i] < min[i] {
205 min[i] = p[i];
206 }
207 if p[i] > max[i] {
208 max[i] = p[i];
209 }
210 }
211 }
212 let vertices = vec![
214 [min[0], min[1], min[2]],
215 [max[0], min[1], min[2]],
216 [max[0], max[1], min[2]],
217 [min[0], max[1], min[2]],
218 [min[0], min[1], max[2]],
219 [max[0], min[1], max[2]],
220 [max[0], max[1], max[2]],
221 [min[0], max[1], max[2]],
222 ];
223 let faces = vec![
225 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 5, 1, 0, 4, 5, 2, 7, 3, 2, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5, 6, 1, 6, 2, ];
232 Self { vertices, faces }
233 }
234
235 pub fn volume(&self) -> f64 {
237 let n = self.faces.len() / 3;
238 let mut vol = 0.0;
239 for i in 0..n {
240 let a = self.vertices[self.faces[i * 3]];
241 let b = self.vertices[self.faces[i * 3 + 1]];
242 let c = self.vertices[self.faces[i * 3 + 2]];
243 let cr = cross3(b, c);
244 vol += dot3(a, cr);
245 }
246 (vol / 6.0).abs()
247 }
248
249 pub fn surface_area(&self) -> f64 {
251 let n = self.faces.len() / 3;
252 let mut area = 0.0;
253 for i in 0..n {
254 let a = self.vertices[self.faces[i * 3]];
255 let b = self.vertices[self.faces[i * 3 + 1]];
256 let c = self.vertices[self.faces[i * 3 + 2]];
257 let ab = sub3(b, a);
258 let ac = sub3(c, a);
259 let cr = cross3(ab, ac);
260 area += len3(cr) * 0.5;
261 }
262 area
263 }
264
265 pub fn gjk_overlap(&self, other: &PyConvexHull) -> bool {
268 if self.vertices.is_empty() || other.vertices.is_empty() {
269 return false;
270 }
271 let c1 = centroid(&self.vertices);
272 let c2 = centroid(&other.vertices);
273 let r1 = bounding_radius(&self.vertices, c1);
274 let r2 = bounding_radius(&other.vertices, c2);
275 let dist = len3(sub3(c1, c2));
276 dist < r1 + r2
277 }
278}
279
280fn centroid(pts: &[[f64; 3]]) -> [f64; 3] {
281 let n = pts.len() as f64;
282 let mut s = [0.0f64; 3];
283 for &p in pts {
284 s[0] += p[0];
285 s[1] += p[1];
286 s[2] += p[2];
287 }
288 [s[0] / n, s[1] / n, s[2] / n]
289}
290
291fn bounding_radius(pts: &[[f64; 3]], center: [f64; 3]) -> f64 {
292 pts.iter()
293 .map(|&p| len3(sub3(p, center)))
294 .fold(0.0f64, f64::max)
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct PyTriangleMesh {
304 pub vertices: Vec<[f64; 3]>,
306 pub indices: Vec<usize>,
308 pub normals: Vec<[f64; 3]>,
310}
311
312impl PyTriangleMesh {
313 pub fn new() -> Self {
315 Self {
316 vertices: vec![],
317 indices: vec![],
318 normals: vec![],
319 }
320 }
321
322 pub fn from_raw(vertices: Vec<[f64; 3]>, indices: Vec<usize>) -> Self {
324 let mut mesh = Self {
325 vertices,
326 indices,
327 normals: vec![],
328 };
329 mesh.compute_normals();
330 mesh
331 }
332
333 pub fn compute_normals(&mut self) {
335 let n = self.vertices.len();
336 self.normals = vec![[0.0; 3]; n];
337 let tri_count = self.indices.len() / 3;
338 for t in 0..tri_count {
339 let ia = self.indices[t * 3];
340 let ib = self.indices[t * 3 + 1];
341 let ic = self.indices[t * 3 + 2];
342 if ia >= n || ib >= n || ic >= n {
343 continue;
344 }
345 let a = self.vertices[ia];
346 let b = self.vertices[ib];
347 let c = self.vertices[ic];
348 let ab = sub3(b, a);
349 let ac = sub3(c, a);
350 let face_n = cross3(ab, ac);
351 for &idx in &[ia, ib, ic] {
352 self.normals[idx][0] += face_n[0];
353 self.normals[idx][1] += face_n[1];
354 self.normals[idx][2] += face_n[2];
355 }
356 }
357 for n in &mut self.normals {
358 *n = normalize3(*n);
359 }
360 }
361
362 pub fn repair(&mut self) {
364 let mut good = Vec::new();
366 let n = self.vertices.len();
367 let tri_count = self.indices.len() / 3;
368 for t in 0..tri_count {
369 let ia = self.indices[t * 3];
370 let ib = self.indices[t * 3 + 1];
371 let ic = self.indices[t * 3 + 2];
372 if ia >= n || ib >= n || ic >= n {
373 continue;
374 }
375 if ia == ib || ib == ic || ia == ic {
376 continue;
377 }
378 let a = self.vertices[ia];
379 let b = self.vertices[ib];
380 let c = self.vertices[ic];
381 let area = len3(cross3(sub3(b, a), sub3(c, a)));
382 if area > 1e-15 {
383 good.extend_from_slice(&[ia, ib, ic]);
384 }
385 }
386 self.indices = good;
387 }
388
389 pub fn smooth(&mut self, iterations: usize) {
391 for _ in 0..iterations {
392 let n = self.vertices.len();
393 let mut acc = vec![[0.0f64; 3]; n];
394 let mut cnt = vec![0usize; n];
395 let tri_count = self.indices.len() / 3;
396 for t in 0..tri_count {
397 let ia = self.indices[t * 3];
398 let ib = self.indices[t * 3 + 1];
399 let ic = self.indices[t * 3 + 2];
400 if ia >= n || ib >= n || ic >= n {
401 continue;
402 }
403 let va = self.vertices[ia];
404 let vb = self.vertices[ib];
405 let vc = self.vertices[ic];
406 acc[ia] = add3(acc[ia], add3(vb, vc));
407 cnt[ia] += 2;
408 acc[ib] = add3(acc[ib], add3(va, vc));
409 cnt[ib] += 2;
410 acc[ic] = add3(acc[ic], add3(va, vb));
411 cnt[ic] += 2;
412 }
413 for i in 0..n {
414 if cnt[i] > 0 {
415 let c = cnt[i] as f64;
416 self.vertices[i] = lerp3(
417 self.vertices[i],
418 [acc[i][0] / c, acc[i][1] / c, acc[i][2] / c],
419 0.5,
420 );
421 }
422 }
423 }
424 self.compute_normals();
425 }
426
427 pub fn compute_volume(&self) -> f64 {
429 let tri_count = self.indices.len() / 3;
430 let n = self.vertices.len();
431 let mut vol = 0.0;
432 for t in 0..tri_count {
433 let ia = self.indices[t * 3];
434 let ib = self.indices[t * 3 + 1];
435 let ic = self.indices[t * 3 + 2];
436 if ia >= n || ib >= n || ic >= n {
437 continue;
438 }
439 let a = self.vertices[ia];
440 let b = self.vertices[ib];
441 let c = self.vertices[ic];
442 vol += dot3(a, cross3(b, c));
443 }
444 (vol / 6.0).abs()
445 }
446
447 pub fn compute_surface_area(&self) -> f64 {
449 let tri_count = self.indices.len() / 3;
450 let n = self.vertices.len();
451 let mut area = 0.0;
452 for t in 0..tri_count {
453 let ia = self.indices[t * 3];
454 let ib = self.indices[t * 3 + 1];
455 let ic = self.indices[t * 3 + 2];
456 if ia >= n || ib >= n || ic >= n {
457 continue;
458 }
459 let a = self.vertices[ia];
460 let b = self.vertices[ib];
461 let c = self.vertices[ic];
462 area += len3(cross3(sub3(b, a), sub3(c, a))) * 0.5;
463 }
464 area
465 }
466
467 pub fn triangle_count(&self) -> usize {
469 self.indices.len() / 3
470 }
471}
472
473impl Default for PyTriangleMesh {
474 fn default() -> Self {
475 Self::new()
476 }
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct PyCsg;
489
490impl PyCsg {
491 pub fn union(a: &PyTriangleMesh, b: &PyTriangleMesh) -> PyTriangleMesh {
493 let offset = a.vertices.len();
494 let mut verts = a.vertices.clone();
495 verts.extend_from_slice(&b.vertices);
496 let mut idx = a.indices.clone();
497 for &i in &b.indices {
498 idx.push(i + offset);
499 }
500 PyTriangleMesh::from_raw(verts, idx)
501 }
502
503 pub fn intersection(a: &PyTriangleMesh, b: &PyTriangleMesh) -> PyTriangleMesh {
505 let (bmin, bmax) = compute_aabb(&b.vertices);
506 let mut verts = Vec::new();
507 let mut idx = Vec::new();
508 let tri_count = a.indices.len() / 3;
509 let n = a.vertices.len();
510 let mut remap = vec![usize::MAX; n];
511 for t in 0..tri_count {
512 let ia = a.indices[t * 3];
513 let ib = a.indices[t * 3 + 1];
514 let ic = a.indices[t * 3 + 2];
515 if ia >= n || ib >= n || ic >= n {
516 continue;
517 }
518 let ctr = scale3(
519 add3(a.vertices[ia], add3(a.vertices[ib], a.vertices[ic])),
520 1.0 / 3.0,
521 );
522 if point_in_aabb(ctr, bmin, bmax) {
523 for &vi in &[ia, ib, ic] {
524 if remap[vi] == usize::MAX {
525 remap[vi] = verts.len();
526 verts.push(a.vertices[vi]);
527 }
528 idx.push(remap[vi]);
529 }
530 }
531 }
532 PyTriangleMesh::from_raw(verts, idx)
533 }
534
535 pub fn subtraction(a: &PyTriangleMesh, b: &PyTriangleMesh) -> PyTriangleMesh {
537 let (bmin, bmax) = compute_aabb(&b.vertices);
538 let mut verts = Vec::new();
539 let mut idx = Vec::new();
540 let tri_count = a.indices.len() / 3;
541 let n = a.vertices.len();
542 let mut remap = vec![usize::MAX; n];
543 for t in 0..tri_count {
544 let ia = a.indices[t * 3];
545 let ib = a.indices[t * 3 + 1];
546 let ic = a.indices[t * 3 + 2];
547 if ia >= n || ib >= n || ic >= n {
548 continue;
549 }
550 let ctr = scale3(
551 add3(a.vertices[ia], add3(a.vertices[ib], a.vertices[ic])),
552 1.0 / 3.0,
553 );
554 if !point_in_aabb(ctr, bmin, bmax) {
555 for &vi in &[ia, ib, ic] {
556 if remap[vi] == usize::MAX {
557 remap[vi] = verts.len();
558 verts.push(a.vertices[vi]);
559 }
560 idx.push(remap[vi]);
561 }
562 }
563 }
564 PyTriangleMesh::from_raw(verts, idx)
565 }
566}
567
568fn point_in_aabb(p: [f64; 3], mn: [f64; 3], mx: [f64; 3]) -> bool {
569 p[0] >= mn[0]
570 && p[0] <= mx[0]
571 && p[1] >= mn[1]
572 && p[1] <= mx[1]
573 && p[2] >= mn[2]
574 && p[2] <= mx[2]
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct PyHeightfield {
584 pub cols: usize,
586 pub rows: usize,
588 pub data: Vec<f64>,
590 pub scale_x: f64,
592 pub scale_z: f64,
594 pub scale_y: f64,
596}
597
598impl PyHeightfield {
599 pub fn new(cols: usize, rows: usize, scale_x: f64, scale_z: f64, scale_y: f64) -> Self {
601 Self {
602 cols,
603 rows,
604 data: vec![0.0; cols * rows],
605 scale_x,
606 scale_z,
607 scale_y,
608 }
609 }
610
611 pub fn set_height(&mut self, col: usize, row: usize, h: f64) {
613 if col < self.cols && row < self.rows {
614 self.data[row * self.cols + col] = h;
615 }
616 }
617
618 pub fn height_at(&self, x: f64, z: f64) -> f64 {
620 let gx = x / self.scale_x;
621 let gz = z / self.scale_z;
622 let col = gx.floor() as isize;
623 let row = gz.floor() as isize;
624 let fx = gx - col as f64;
625 let fz = gz - row as f64;
626
627 let sample = |c: isize, r: isize| -> f64 {
628 let c = c.clamp(0, self.cols as isize - 1) as usize;
629 let r = r.clamp(0, self.rows as isize - 1) as usize;
630 self.data[r * self.cols + c] * self.scale_y
631 };
632
633 let h00 = sample(col, row);
634 let h10 = sample(col + 1, row);
635 let h01 = sample(col, row + 1);
636 let h11 = sample(col + 1, row + 1);
637
638 let h0 = h00 + (h10 - h00) * fx;
639 let h1 = h01 + (h11 - h01) * fx;
640 h0 + (h1 - h0) * fz
641 }
642
643 pub fn normal_at(&self, x: f64, z: f64) -> [f64; 3] {
645 let eps = self.scale_x.min(self.scale_z) * 0.5;
646 let dhdx = (self.height_at(x + eps, z) - self.height_at(x - eps, z)) / (2.0 * eps);
647 let dhdz = (self.height_at(x, z + eps) - self.height_at(x, z - eps)) / (2.0 * eps);
648 normalize3([-dhdx, 1.0, -dhdz])
649 }
650
651 pub fn raycast(&self, origin: [f64; 3], direction: [f64; 3]) -> Option<f64> {
653 let dir = normalize3(direction);
654 let max_dist = (self.cols as f64 * self.scale_x + self.rows as f64 * self.scale_z) * 2.0;
655 let steps = 1000usize;
656 let step = max_dist / steps as f64;
657 let mut t = 0.0;
658 for _ in 0..steps {
659 let p = [
660 origin[0] + dir[0] * t,
661 origin[1] + dir[1] * t,
662 origin[2] + dir[2] * t,
663 ];
664 let h = self.height_at(p[0], p[2]);
665 if p[1] <= h {
666 return Some(t);
667 }
668 t += step;
669 }
670 None
671 }
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
680pub struct PySpatialHash {
681 pub cell_size: f64,
683 pub points: Vec<[f64; 3]>,
685 grid: HashMap<(i64, i64, i64), Vec<usize>>,
687}
688
689impl PySpatialHash {
690 pub fn new(cell_size: f64) -> Self {
692 Self {
693 cell_size,
694 points: vec![],
695 grid: HashMap::new(),
696 }
697 }
698
699 fn cell_of(&self, p: [f64; 3]) -> (i64, i64, i64) {
700 (
701 (p[0] / self.cell_size).floor() as i64,
702 (p[1] / self.cell_size).floor() as i64,
703 (p[2] / self.cell_size).floor() as i64,
704 )
705 }
706
707 pub fn insert(&mut self, p: [f64; 3]) -> usize {
709 let id = self.points.len();
710 self.points.push(p);
711 let cell = self.cell_of(p);
712 self.grid.entry(cell).or_default().push(id);
713 id
714 }
715
716 pub fn remove(&mut self, id: usize) {
718 if id >= self.points.len() {
719 return;
720 }
721 let p = self.points[id];
722 let cell = self.cell_of(p);
723 if let Some(list) = self.grid.get_mut(&cell) {
724 list.retain(|&x| x != id);
725 }
726 }
727
728 pub fn query(&self, p: [f64; 3]) -> Vec<usize> {
730 let cell = self.cell_of(p);
731 self.grid.get(&cell).cloned().unwrap_or_default()
732 }
733
734 pub fn sphere_query(&self, center: [f64; 3], radius: f64) -> Vec<usize> {
736 let r2 = radius * radius;
737 let cells = (radius / self.cell_size).ceil() as i64 + 1;
738 let base = self.cell_of(center);
739 let mut result = Vec::new();
740 for dx in -cells..=cells {
741 for dy in -cells..=cells {
742 for dz in -cells..=cells {
743 let key = (base.0 + dx, base.1 + dy, base.2 + dz);
744 if let Some(list) = self.grid.get(&key) {
745 for &id in list {
746 if id < self.points.len() {
747 let d = sub3(self.points[id], center);
748 if dot3(d, d) <= r2 {
749 result.push(id);
750 }
751 }
752 }
753 }
754 }
755 }
756 }
757 result
758 }
759}
760
761#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct PyAabb {
768 pub min: [f64; 3],
770 pub max: [f64; 3],
772}
773
774impl PyAabb {
775 pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
777 Self { min, max }
778 }
779
780 pub fn overlaps(&self, other: &PyAabb) -> bool {
782 self.min[0] <= other.max[0]
783 && self.max[0] >= other.min[0]
784 && self.min[1] <= other.max[1]
785 && self.max[1] >= other.min[1]
786 && self.min[2] <= other.max[2]
787 && self.max[2] >= other.min[2]
788 }
789
790 pub fn center(&self) -> [f64; 3] {
792 scale3(add3(self.min, self.max), 0.5)
793 }
794
795 pub fn half_extents(&self) -> [f64; 3] {
797 scale3(sub3(self.max, self.min), 0.5)
798 }
799
800 pub fn surface_area(&self) -> f64 {
802 let e = sub3(self.max, self.min);
803 2.0 * (e[0] * e[1] + e[1] * e[2] + e[2] * e[0])
804 }
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize)]
809pub struct PyBvhNode {
810 pub aabb: PyAabb,
812 pub left: usize,
814 pub right: usize,
816 pub item: usize,
818}
819
820#[derive(Debug, Clone, Serialize, Deserialize)]
822pub struct PyBvh {
823 pub nodes: Vec<PyBvhNode>,
825 pub root: usize,
827 pub items: Vec<PyAabb>,
829}
830
831impl PyBvh {
832 pub fn build(aabbs: Vec<PyAabb>) -> Self {
834 let n = aabbs.len();
835 if n == 0 {
836 return Self {
837 nodes: vec![],
838 root: 0,
839 items: vec![],
840 };
841 }
842 let mut bvh = Self {
843 nodes: Vec::with_capacity(2 * n),
844 root: 0,
845 items: aabbs,
846 };
847 let indices: Vec<usize> = (0..n).collect();
848 bvh.root = bvh.build_recursive(&indices);
849 bvh
850 }
851
852 fn build_recursive(&mut self, indices: &[usize]) -> usize {
853 if indices.len() == 1 {
854 let id = indices[0];
855 let node = PyBvhNode {
856 aabb: self.items[id].clone(),
857 left: usize::MAX,
858 right: usize::MAX,
859 item: id,
860 };
861 let idx = self.nodes.len();
862 self.nodes.push(node);
863 return idx;
864 }
865 let combined = merge_aabbs(indices.iter().map(|&i| &self.items[i]));
867 let axis = longest_axis(&combined);
869 let mut sorted = indices.to_vec();
870 sorted.sort_by(|&a, &b| {
871 let ca = self.items[a].center()[axis];
872 let cb = self.items[b].center()[axis];
873 ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
874 });
875 let mid = sorted.len() / 2;
876 let left = self.build_recursive(&sorted[..mid]);
877 let right = self.build_recursive(&sorted[mid..]);
878 let node = PyBvhNode {
879 aabb: combined,
880 left,
881 right,
882 item: usize::MAX,
883 };
884 let idx = self.nodes.len();
885 self.nodes.push(node);
886 idx
887 }
888
889 pub fn query_aabb(&self, query: &PyAabb) -> Vec<usize> {
891 if self.nodes.is_empty() {
892 return vec![];
893 }
894 let mut result = Vec::new();
895 self.query_recursive(self.root, query, &mut result);
896 result
897 }
898
899 fn query_recursive(&self, node_idx: usize, query: &PyAabb, out: &mut Vec<usize>) {
900 if node_idx >= self.nodes.len() {
901 return;
902 }
903 let node = &self.nodes[node_idx];
904 if !node.aabb.overlaps(query) {
905 return;
906 }
907 if node.left == usize::MAX {
908 out.push(node.item);
909 return;
910 }
911 self.query_recursive(node.left, query, out);
912 self.query_recursive(node.right, query, out);
913 }
914
915 pub fn raycast(&self, origin: [f64; 3], direction: [f64; 3]) -> Vec<(f64, usize)> {
917 if self.nodes.is_empty() {
918 return vec![];
919 }
920 let dir = normalize3(direction);
921 let mut hits = Vec::new();
922 self.raycast_recursive(self.root, origin, dir, &mut hits);
923 hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
924 hits
925 }
926
927 fn raycast_recursive(
928 &self,
929 node_idx: usize,
930 origin: [f64; 3],
931 dir: [f64; 3],
932 out: &mut Vec<(f64, usize)>,
933 ) {
934 if node_idx >= self.nodes.len() {
935 return;
936 }
937 let node = &self.nodes[node_idx];
938 if let Some(t) = ray_aabb_intersect(origin, dir, &node.aabb) {
939 if node.left == usize::MAX {
940 out.push((t, node.item));
941 return;
942 }
943 self.raycast_recursive(node.left, origin, dir, out);
944 self.raycast_recursive(node.right, origin, dir, out);
945 }
946 }
947
948 pub fn nearest_neighbor(&self, point: [f64; 3]) -> Option<usize> {
950 if self.items.is_empty() {
951 return None;
952 }
953 let mut best_dist = f64::MAX;
954 let mut best = 0usize;
955 for (i, item) in self.items.iter().enumerate() {
956 let d = len3(sub3(item.center(), point));
957 if d < best_dist {
958 best_dist = d;
959 best = i;
960 }
961 }
962 Some(best)
963 }
964}
965
966fn merge_aabbs<'a>(iter: impl Iterator<Item = &'a PyAabb>) -> PyAabb {
967 let mut mn = [f64::MAX; 3];
968 let mut mx = [f64::MIN; 3];
969 for a in iter {
970 for i in 0..3 {
971 if a.min[i] < mn[i] {
972 mn[i] = a.min[i];
973 }
974 if a.max[i] > mx[i] {
975 mx[i] = a.max[i];
976 }
977 }
978 }
979 PyAabb { min: mn, max: mx }
980}
981
982fn longest_axis(aabb: &PyAabb) -> usize {
983 let e = sub3(aabb.max, aabb.min);
984 if e[0] >= e[1] && e[0] >= e[2] {
985 0
986 } else if e[1] >= e[2] {
987 1
988 } else {
989 2
990 }
991}
992
993fn ray_aabb_intersect(origin: [f64; 3], dir: [f64; 3], aabb: &PyAabb) -> Option<f64> {
994 let mut tmin = f64::NEG_INFINITY;
995 let mut tmax = f64::INFINITY;
996 for i in 0..3 {
997 if dir[i].abs() < 1e-15 {
998 if origin[i] < aabb.min[i] || origin[i] > aabb.max[i] {
999 return None;
1000 }
1001 } else {
1002 let inv = 1.0 / dir[i];
1003 let t1 = (aabb.min[i] - origin[i]) * inv;
1004 let t2 = (aabb.max[i] - origin[i]) * inv;
1005 tmin = tmin.max(t1.min(t2));
1006 tmax = tmax.min(t1.max(t2));
1007 }
1008 }
1009 if tmax >= tmin && tmax >= 0.0 {
1010 Some(tmin.max(0.0))
1011 } else {
1012 None
1013 }
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1022pub struct PyPointCloud {
1023 pub points: Vec<[f64; 3]>,
1025 pub normals: Vec<[f64; 3]>,
1027}
1028
1029impl PyPointCloud {
1030 pub fn new() -> Self {
1032 Self {
1033 points: vec![],
1034 normals: vec![],
1035 }
1036 }
1037
1038 pub fn from_points(points: Vec<[f64; 3]>) -> Self {
1040 Self {
1041 points,
1042 normals: vec![],
1043 }
1044 }
1045
1046 pub fn compute_normals(&mut self, _k_neighbours: usize) {
1049 let c = if self.points.is_empty() {
1050 [0.0; 3]
1051 } else {
1052 centroid(&self.points)
1053 };
1054 self.normals = self
1055 .points
1056 .iter()
1057 .map(|&p| normalize3(sub3(p, c)))
1058 .collect();
1059 }
1060
1061 pub fn simplify(&mut self, voxel_size: f64) {
1063 let mut grid: HashMap<(i64, i64, i64), [f64; 3]> = HashMap::new();
1064 for &p in &self.points {
1065 let key = (
1066 (p[0] / voxel_size).floor() as i64,
1067 (p[1] / voxel_size).floor() as i64,
1068 (p[2] / voxel_size).floor() as i64,
1069 );
1070 grid.entry(key).or_insert(p);
1071 }
1072 self.points = grid.into_values().collect();
1073 self.normals.clear();
1074 }
1075
1076 pub fn poisson_reconstruct(&self) -> PyTriangleMesh {
1080 PyTriangleMesh::new()
1081 }
1082}
1083
1084impl Default for PyPointCloud {
1085 fn default() -> Self {
1086 Self::new()
1087 }
1088}
1089
1090#[derive(Debug, Clone, Serialize, Deserialize)]
1096pub struct PyGeometryTransform {
1097 pub translation: [f64; 3],
1099 pub axis: [f64; 3],
1101 pub angle: f64,
1103 pub scale: f64,
1105}
1106
1107impl PyGeometryTransform {
1108 pub fn identity() -> Self {
1110 Self {
1111 translation: [0.0; 3],
1112 axis: [0.0, 1.0, 0.0],
1113 angle: 0.0,
1114 scale: 1.0,
1115 }
1116 }
1117
1118 pub fn translate(mut self, t: [f64; 3]) -> Self {
1120 self.translation = t;
1121 self
1122 }
1123
1124 pub fn rotate(mut self, axis: [f64; 3], angle: f64) -> Self {
1126 self.axis = normalize3(axis);
1127 self.angle = angle;
1128 self
1129 }
1130
1131 pub fn with_scale(mut self, s: f64) -> Self {
1133 self.scale = s;
1134 self
1135 }
1136
1137 pub fn apply_point(&self, p: [f64; 3]) -> [f64; 3] {
1139 let ps = scale3(p, self.scale);
1141 let pr = rodrigues(ps, self.axis, self.angle);
1143 add3(pr, self.translation)
1145 }
1146
1147 pub fn apply_to_mesh(&self, mesh: &mut PyTriangleMesh) {
1149 for v in &mut mesh.vertices {
1150 *v = self.apply_point(*v);
1151 }
1152 mesh.compute_normals();
1153 }
1154
1155 pub fn apply_to_points(&self, points: &mut Vec<[f64; 3]>) {
1157 for p in points.iter_mut() {
1158 *p = self.apply_point(*p);
1159 }
1160 }
1161}
1162
1163fn rodrigues(v: [f64; 3], axis: [f64; 3], angle: f64) -> [f64; 3] {
1164 let cos_a = angle.cos();
1165 let sin_a = angle.sin();
1166 let k = normalize3(axis);
1167
1168 add3(
1169 add3(scale3(v, cos_a), scale3(cross3(k, v), sin_a)),
1170 scale3(k, dot3(k, v) * (1.0 - cos_a)),
1171 )
1172}
1173
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1180pub struct PyMeshQuality {
1181 pub aspect_ratios: Vec<f64>,
1183 pub jacobians: Vec<f64>,
1185 pub skewness: Vec<f64>,
1187 pub areas: Vec<f64>,
1189}
1190
1191impl PyMeshQuality {
1192 pub fn compute(mesh: &PyTriangleMesh) -> Self {
1194 let tri_count = mesh.indices.len() / 3;
1195 let n = mesh.vertices.len();
1196 let mut aspect_ratios = Vec::with_capacity(tri_count);
1197 let mut jacobians = Vec::with_capacity(tri_count);
1198 let mut skewness = Vec::with_capacity(tri_count);
1199 let mut areas = Vec::with_capacity(tri_count);
1200 for t in 0..tri_count {
1201 let ia = mesh.indices[t * 3];
1202 let ib = mesh.indices[t * 3 + 1];
1203 let ic = mesh.indices[t * 3 + 2];
1204 if ia >= n || ib >= n || ic >= n {
1205 aspect_ratios.push(f64::NAN);
1206 jacobians.push(f64::NAN);
1207 skewness.push(f64::NAN);
1208 areas.push(0.0);
1209 continue;
1210 }
1211 let a = mesh.vertices[ia];
1212 let b = mesh.vertices[ib];
1213 let c = mesh.vertices[ic];
1214 let ab = sub3(b, a);
1215 let ac = sub3(c, a);
1216 let bc = sub3(c, b);
1217 let la = len3(ab);
1218 let lb = len3(ac);
1219 let lc = len3(bc);
1220 let cr = cross3(ab, ac);
1221 let area = len3(cr) * 0.5;
1222 areas.push(area);
1223 let longest = la.max(lb).max(lc);
1225 let inradius = if la + lb + lc > 0.0 {
1226 2.0 * area / (la + lb + lc)
1227 } else {
1228 0.0
1229 };
1230 let ar = if inradius > 0.0 {
1231 longest / (2.0 * 3.0_f64.sqrt() * inradius)
1232 } else {
1233 f64::MAX
1234 };
1235 aspect_ratios.push(ar);
1236 let face_n = normalize3(cr);
1238 let jac = dot3(cross3(ab, ac), face_n);
1239 jacobians.push(jac);
1240 let ideal_angle = std::f64::consts::PI / 3.0;
1242 let cos_ab_ac = if la * lb > 0.0 {
1243 dot3(ab, ac) / (la * lb)
1244 } else {
1245 0.0
1246 };
1247 let angle_a = cos_ab_ac.clamp(-1.0, 1.0).acos();
1248 let skew = ((angle_a - ideal_angle) / ideal_angle).abs();
1249 skewness.push(skew.min(1.0));
1250 }
1251 Self {
1252 aspect_ratios,
1253 jacobians,
1254 skewness,
1255 areas,
1256 }
1257 }
1258
1259 pub fn worst_elements(&self, n: usize) -> Vec<usize> {
1261 let mut indexed: Vec<(usize, f64)> = self
1262 .aspect_ratios
1263 .iter()
1264 .copied()
1265 .enumerate()
1266 .filter(|(_, v)| v.is_finite())
1267 .collect();
1268 indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1269 indexed.into_iter().take(n).map(|(i, _)| i).collect()
1270 }
1271
1272 pub fn mean_aspect_ratio(&self) -> f64 {
1274 let vals: Vec<f64> = self
1275 .aspect_ratios
1276 .iter()
1277 .copied()
1278 .filter(|v| v.is_finite())
1279 .collect();
1280 if vals.is_empty() {
1281 return 0.0;
1282 }
1283 vals.iter().sum::<f64>() / vals.len() as f64
1284 }
1285}
1286
1287pub fn compute_aabb(points: &[[f64; 3]]) -> ([f64; 3], [f64; 3]) {
1293 if points.is_empty() {
1294 return ([0.0; 3], [0.0; 3]);
1295 }
1296 let mut mn = points[0];
1297 let mut mx = points[0];
1298 for &p in points {
1299 for i in 0..3 {
1300 if p[i] < mn[i] {
1301 mn[i] = p[i];
1302 }
1303 if p[i] > mx[i] {
1304 mx[i] = p[i];
1305 }
1306 }
1307 }
1308 (mn, mx)
1309}
1310
1311pub fn mesh_to_obj_string(mesh: &PyTriangleMesh) -> String {
1313 let mut out = String::new();
1314 for v in &mesh.vertices {
1315 out.push_str(&format!("v {} {} {}\n", v[0], v[1], v[2]));
1316 }
1317 for n in &mesh.normals {
1318 out.push_str(&format!("vn {} {} {}\n", n[0], n[1], n[2]));
1319 }
1320 let tri_count = mesh.indices.len() / 3;
1321 for t in 0..tri_count {
1322 let ia = mesh.indices[t * 3] + 1;
1323 let ib = mesh.indices[t * 3 + 1] + 1;
1324 let ic = mesh.indices[t * 3 + 2] + 1;
1325 out.push_str(&format!("f {} {} {}\n", ia, ib, ic));
1326 }
1327 out
1328}
1329
1330pub fn obj_string_to_mesh(obj: &str) -> PyTriangleMesh {
1334 let mut verts = Vec::new();
1335 let mut indices = Vec::new();
1336 for line in obj.lines() {
1337 let line = line.trim();
1338 if line.starts_with("v ") {
1339 let parts: Vec<f64> = line[2..]
1340 .split_whitespace()
1341 .filter_map(|s| s.parse().ok())
1342 .collect();
1343 if parts.len() >= 3 {
1344 verts.push([parts[0], parts[1], parts[2]]);
1345 }
1346 } else if line.starts_with("f ") {
1347 let parts: Vec<usize> = line[2..]
1348 .split_whitespace()
1349 .filter_map(|s| {
1350 s.split('/')
1351 .next()
1352 .and_then(|n| n.parse::<usize>().ok())
1353 .map(|n| n - 1)
1354 })
1355 .collect();
1356 if parts.len() >= 3 {
1357 for i in 1..parts.len() - 1 {
1359 indices.push(parts[0]);
1360 indices.push(parts[i]);
1361 indices.push(parts[i + 1]);
1362 }
1363 }
1364 }
1365 }
1366 PyTriangleMesh::from_raw(verts, indices)
1367}
1368
1369pub fn convex_hull_from_mesh(mesh: &PyTriangleMesh) -> PyConvexHull {
1371 PyConvexHull::from_points(&mesh.vertices)
1372}
1373
1374#[cfg(test)]
1379mod tests {
1380 use super::*;
1381
1382 #[test]
1385 fn test_sphere_volume() {
1386 let s = PyShape::Sphere(1.0);
1387 let expected = (4.0 / 3.0) * PI;
1388 assert!((s.volume() - expected).abs() < 1e-10);
1389 }
1390
1391 #[test]
1392 fn test_box_volume() {
1393 let b = PyShape::Box([1.0, 2.0, 3.0]);
1394 assert!((b.volume() - 48.0).abs() < 1e-10);
1395 }
1396
1397 #[test]
1398 fn test_cylinder_volume() {
1399 let c = PyShape::Cylinder {
1400 radius: 1.0,
1401 half_height: 1.0,
1402 };
1403 assert!((c.volume() - 2.0 * PI).abs() < 1e-10);
1404 }
1405
1406 #[test]
1407 fn test_torus_volume() {
1408 let t = PyShape::Torus {
1409 major_r: 3.0,
1410 minor_r: 1.0,
1411 };
1412 let expected = 2.0 * PI * PI * 3.0 * 1.0;
1413 assert!((t.volume() - expected).abs() < 1e-10);
1414 }
1415
1416 #[test]
1417 fn test_shape_aabb_sphere() {
1418 let (mn, mx) = PyShape::Sphere(2.0).aabb();
1419 assert_eq!(mn, [-2.0, -2.0, -2.0]);
1420 assert_eq!(mx, [2.0, 2.0, 2.0]);
1421 }
1422
1423 #[test]
1424 fn test_capsule_surface_area() {
1425 let c = PyShape::Capsule {
1426 radius: 1.0,
1427 half_height: 1.0,
1428 };
1429 let expected = 4.0 * PI + 2.0 * PI * 2.0;
1430 assert!((c.surface_area() - expected).abs() < 1e-10);
1431 }
1432
1433 #[test]
1436 fn test_convex_hull_from_points_empty() {
1437 let hull = PyConvexHull::from_points(&[]);
1438 assert!(hull.vertices.is_empty());
1439 }
1440
1441 #[test]
1442 fn test_convex_hull_from_points_basic() {
1443 let pts = vec![
1444 [0.0, 0.0, 0.0],
1445 [1.0, 0.0, 0.0],
1446 [0.0, 1.0, 0.0],
1447 [0.0, 0.0, 1.0],
1448 ];
1449 let hull = PyConvexHull::from_points(&pts);
1450 assert!(!hull.vertices.is_empty());
1451 assert!(!hull.faces.is_empty());
1452 }
1453
1454 #[test]
1455 fn test_convex_hull_volume_positive() {
1456 let pts: Vec<[f64; 3]> = vec![
1457 [-1.0, -1.0, -1.0],
1458 [1.0, -1.0, -1.0],
1459 [1.0, 1.0, -1.0],
1460 [-1.0, 1.0, -1.0],
1461 [-1.0, -1.0, 1.0],
1462 [1.0, -1.0, 1.0],
1463 [1.0, 1.0, 1.0],
1464 [-1.0, 1.0, 1.0],
1465 ];
1466 let hull = PyConvexHull::from_points(&pts);
1467 assert!(hull.volume() > 0.0);
1468 }
1469
1470 #[test]
1471 fn test_convex_hull_gjk_overlap() {
1472 let pts1 = vec![
1473 [0.0, 0.0, 0.0],
1474 [1.0, 0.0, 0.0],
1475 [0.0, 1.0, 0.0],
1476 [0.0, 0.0, 1.0],
1477 ];
1478 let pts2 = vec![
1479 [0.5, 0.5, 0.5],
1480 [1.5, 0.5, 0.5],
1481 [0.5, 1.5, 0.5],
1482 [0.5, 0.5, 1.5],
1483 ];
1484 let h1 = PyConvexHull::from_points(&pts1);
1485 let h2 = PyConvexHull::from_points(&pts2);
1486 assert!(h1.gjk_overlap(&h2));
1487 }
1488
1489 #[test]
1490 fn test_convex_hull_gjk_no_overlap() {
1491 let pts1 = vec![
1492 [0.0, 0.0, 0.0],
1493 [0.1, 0.0, 0.0],
1494 [0.0, 0.1, 0.0],
1495 [0.0, 0.0, 0.1],
1496 ];
1497 let pts2 = vec![
1498 [10.0, 10.0, 10.0],
1499 [10.1, 10.0, 10.0],
1500 [10.0, 10.1, 10.0],
1501 [10.0, 10.0, 10.1],
1502 ];
1503 let h1 = PyConvexHull::from_points(&pts1);
1504 let h2 = PyConvexHull::from_points(&pts2);
1505 assert!(!h1.gjk_overlap(&h2));
1506 }
1507
1508 fn unit_box_mesh() -> PyTriangleMesh {
1511 let verts = vec![
1512 [0.0, 0.0, 0.0],
1513 [1.0, 0.0, 0.0],
1514 [1.0, 1.0, 0.0],
1515 [0.0, 1.0, 0.0],
1516 [0.0, 0.0, 1.0],
1517 [1.0, 0.0, 1.0],
1518 [1.0, 1.0, 1.0],
1519 [0.0, 1.0, 1.0],
1520 ];
1521 let idx = vec![
1522 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 5, 1, 0, 4, 5, 2, 7, 3, 2, 6, 7, 0, 3, 7, 0, 7,
1523 4, 1, 5, 6, 1, 6, 2,
1524 ];
1525 PyTriangleMesh::from_raw(verts, idx)
1526 }
1527
1528 #[test]
1529 fn test_mesh_compute_normals_not_empty() {
1530 let mesh = unit_box_mesh();
1531 assert_eq!(mesh.normals.len(), mesh.vertices.len());
1532 }
1533
1534 #[test]
1535 fn test_mesh_triangle_count() {
1536 let mesh = unit_box_mesh();
1537 assert_eq!(mesh.triangle_count(), 12);
1538 }
1539
1540 #[test]
1541 fn test_mesh_surface_area_unit_box() {
1542 let mesh = unit_box_mesh();
1543 let sa = mesh.compute_surface_area();
1544 assert!((sa - 6.0).abs() < 1e-10);
1545 }
1546
1547 #[test]
1548 fn test_mesh_volume_unit_box() {
1549 let mesh = unit_box_mesh();
1550 let vol = mesh.compute_volume();
1551 assert!((vol - 1.0).abs() < 0.1, "vol={vol}");
1552 }
1553
1554 #[test]
1555 fn test_mesh_repair_removes_degenerate() {
1556 let mut mesh = PyTriangleMesh::from_raw(
1557 vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1558 vec![0, 0, 0, 0, 1, 2], );
1560 mesh.repair();
1561 assert_eq!(mesh.triangle_count(), 1);
1562 }
1563
1564 #[test]
1565 fn test_mesh_smooth_runs() {
1566 let mut mesh = unit_box_mesh();
1567 let before = mesh.vertices.clone();
1568 mesh.smooth(2);
1569 assert_eq!(mesh.vertices.len(), before.len());
1571 }
1572
1573 #[test]
1576 fn test_csg_union_vertex_count() {
1577 let a = unit_box_mesh();
1578 let b = unit_box_mesh();
1579 let u = PyCsg::union(&a, &b);
1580 assert_eq!(u.vertices.len(), a.vertices.len() + b.vertices.len());
1581 }
1582
1583 #[test]
1584 fn test_csg_intersection_subset() {
1585 let a = unit_box_mesh();
1586 let mut b_big = unit_box_mesh();
1587 for v in &mut b_big.vertices {
1589 v[0] *= 2.0;
1590 v[1] *= 2.0;
1591 v[2] *= 2.0;
1592 v[0] -= 0.5;
1593 v[1] -= 0.5;
1594 v[2] -= 0.5;
1595 }
1596 let inter = PyCsg::intersection(&a, &b_big);
1597 assert!(inter.vertices.len() <= a.vertices.len() + 8);
1598 }
1599
1600 #[test]
1601 fn test_csg_subtraction_removes_some() {
1602 let a = unit_box_mesh();
1603 let b = unit_box_mesh(); let sub = PyCsg::subtraction(&a, &b);
1605 assert!(sub.triangle_count() <= a.triangle_count());
1607 }
1608
1609 #[test]
1612 fn test_heightfield_flat() {
1613 let hf = PyHeightfield::new(10, 10, 1.0, 1.0, 1.0);
1614 assert!((hf.height_at(5.0, 5.0)).abs() < 1e-10);
1615 }
1616
1617 #[test]
1618 fn test_heightfield_set_and_query() {
1619 let mut hf = PyHeightfield::new(10, 10, 1.0, 1.0, 1.0);
1620 hf.set_height(3, 4, 5.0);
1621 let h = hf.height_at(3.0, 4.0);
1622 assert!((h - 5.0).abs() < 1e-6);
1623 }
1624
1625 #[test]
1626 fn test_heightfield_normal_flat() {
1627 let hf = PyHeightfield::new(10, 10, 1.0, 1.0, 1.0);
1628 let n = hf.normal_at(5.0, 5.0);
1629 assert!((n[1] - 1.0).abs() < 1e-6);
1631 }
1632
1633 #[test]
1634 fn test_heightfield_raycast_hits() {
1635 let mut hf = PyHeightfield::new(20, 20, 1.0, 1.0, 1.0);
1636 for r in 0..20 {
1638 for c in 0..20 {
1639 hf.set_height(c, r, 1.0);
1640 }
1641 }
1642 let origin = [10.0, 10.0, 10.0];
1643 let dir = [0.0, -1.0, 0.0];
1644 let hit = hf.raycast(origin, dir);
1645 assert!(hit.is_some());
1646 }
1647
1648 #[test]
1651 fn test_spatial_hash_insert_query() {
1652 let mut sh = PySpatialHash::new(1.0);
1653 let id = sh.insert([0.5, 0.5, 0.5]);
1654 let res = sh.query([0.5, 0.5, 0.5]);
1655 assert!(res.contains(&id));
1656 }
1657
1658 #[test]
1659 fn test_spatial_hash_sphere_query() {
1660 let mut sh = PySpatialHash::new(1.0);
1661 sh.insert([0.0, 0.0, 0.0]);
1662 sh.insert([5.0, 5.0, 5.0]);
1663 let res = sh.sphere_query([0.0, 0.0, 0.0], 1.0);
1664 assert_eq!(res.len(), 1);
1665 }
1666
1667 #[test]
1668 fn test_spatial_hash_remove() {
1669 let mut sh = PySpatialHash::new(1.0);
1670 let id = sh.insert([0.0, 0.0, 0.0]);
1671 sh.remove(id);
1672 let res = sh.sphere_query([0.0, 0.0, 0.0], 0.5);
1673 assert!(!res.contains(&id));
1674 }
1675
1676 #[test]
1679 fn test_bvh_build_empty() {
1680 let bvh = PyBvh::build(vec![]);
1681 assert!(bvh.nodes.is_empty());
1682 }
1683
1684 #[test]
1685 fn test_bvh_query_aabb_hit() {
1686 let aabbs = vec![
1687 PyAabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]),
1688 PyAabb::new([5.0, 5.0, 5.0], [6.0, 6.0, 6.0]),
1689 ];
1690 let bvh = PyBvh::build(aabbs);
1691 let q = PyAabb::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
1692 let hits = bvh.query_aabb(&q);
1693 assert!(hits.contains(&0));
1694 assert!(!hits.contains(&1));
1695 }
1696
1697 #[test]
1698 fn test_bvh_raycast() {
1699 let aabbs = vec![PyAabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0])];
1700 let bvh = PyBvh::build(aabbs);
1701 let hits = bvh.raycast([-5.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
1702 assert!(!hits.is_empty());
1703 assert_eq!(hits[0].1, 0);
1704 }
1705
1706 #[test]
1707 fn test_bvh_nearest_neighbor() {
1708 let aabbs = vec![
1709 PyAabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]),
1710 PyAabb::new([5.0, 5.0, 5.0], [6.0, 6.0, 6.0]),
1711 ];
1712 let bvh = PyBvh::build(aabbs);
1713 let nn = bvh.nearest_neighbor([0.5, 0.5, 0.5]);
1714 assert_eq!(nn, Some(0));
1715 }
1716
1717 #[test]
1720 fn test_point_cloud_compute_normals() {
1721 let mut pc = PyPointCloud::from_points(vec![
1722 [1.0, 0.0, 0.0],
1723 [-1.0, 0.0, 0.0],
1724 [0.0, 1.0, 0.0],
1725 [0.0, -1.0, 0.0],
1726 ]);
1727 pc.compute_normals(3);
1728 assert_eq!(pc.normals.len(), 4);
1729 }
1730
1731 #[test]
1732 fn test_point_cloud_simplify() {
1733 let mut pc =
1734 PyPointCloud::from_points((0..100).map(|i| [i as f64 * 0.01, 0.0, 0.0]).collect());
1735 pc.simplify(0.1);
1736 assert!(pc.points.len() < 100);
1737 }
1738
1739 #[test]
1740 fn test_point_cloud_poisson_stub() {
1741 let pc = PyPointCloud::from_points(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
1742 let mesh = pc.poisson_reconstruct();
1743 assert!(mesh.vertices.is_empty());
1744 }
1745
1746 #[test]
1749 fn test_transform_identity() {
1750 let t = PyGeometryTransform::identity();
1751 let p = [1.0, 2.0, 3.0];
1752 let out = t.apply_point(p);
1753 for i in 0..3 {
1754 assert!((out[i] - p[i]).abs() < 1e-10);
1755 }
1756 }
1757
1758 #[test]
1759 fn test_transform_translate() {
1760 let t = PyGeometryTransform::identity().translate([1.0, 2.0, 3.0]);
1761 let out = t.apply_point([0.0, 0.0, 0.0]);
1762 assert!((out[0] - 1.0).abs() < 1e-10);
1763 assert!((out[1] - 2.0).abs() < 1e-10);
1764 assert!((out[2] - 3.0).abs() < 1e-10);
1765 }
1766
1767 #[test]
1768 fn test_transform_scale() {
1769 let t = PyGeometryTransform::identity().with_scale(2.0);
1770 let out = t.apply_point([1.0, 1.0, 1.0]);
1771 for i in 0..3 {
1772 assert!((out[i] - 2.0).abs() < 1e-10);
1773 }
1774 }
1775
1776 #[test]
1777 fn test_transform_rotate_180() {
1778 let t = PyGeometryTransform::identity().rotate([0.0, 0.0, 1.0], PI);
1779 let out = t.apply_point([1.0, 0.0, 0.0]);
1780 assert!((out[0] - (-1.0)).abs() < 1e-10, "x={}", out[0]);
1781 assert!((out[1]).abs() < 1e-10, "y={}", out[1]);
1782 }
1783
1784 #[test]
1785 fn test_transform_apply_to_mesh() {
1786 let mut mesh = unit_box_mesh();
1787 let t = PyGeometryTransform::identity().translate([1.0, 0.0, 0.0]);
1788 t.apply_to_mesh(&mut mesh);
1789 for v in &mesh.vertices {
1791 assert!(v[0] >= 1.0 - 1e-10 && v[0] <= 2.0 + 1e-10);
1792 }
1793 }
1794
1795 #[test]
1798 fn test_mesh_quality_compute() {
1799 let mesh = unit_box_mesh();
1800 let q = PyMeshQuality::compute(&mesh);
1801 assert_eq!(q.aspect_ratios.len(), mesh.triangle_count());
1802 assert_eq!(q.areas.len(), mesh.triangle_count());
1803 }
1804
1805 #[test]
1806 fn test_mesh_quality_areas_positive() {
1807 let mesh = unit_box_mesh();
1808 let q = PyMeshQuality::compute(&mesh);
1809 for &a in &q.areas {
1810 assert!(a >= 0.0);
1811 }
1812 }
1813
1814 #[test]
1815 fn test_mesh_quality_worst_elements() {
1816 let mesh = unit_box_mesh();
1817 let q = PyMeshQuality::compute(&mesh);
1818 let worst = q.worst_elements(3);
1819 assert!(worst.len() <= 3);
1820 }
1821
1822 #[test]
1823 fn test_mesh_quality_mean_aspect_ratio() {
1824 let mesh = unit_box_mesh();
1825 let q = PyMeshQuality::compute(&mesh);
1826 let mar = q.mean_aspect_ratio();
1827 assert!(mar >= 1.0);
1828 }
1829
1830 #[test]
1833 fn test_compute_aabb_empty() {
1834 let (mn, mx) = compute_aabb(&[]);
1835 assert_eq!(mn, [0.0; 3]);
1836 assert_eq!(mx, [0.0; 3]);
1837 }
1838
1839 #[test]
1840 fn test_compute_aabb_points() {
1841 let pts = vec![[-1.0, 2.0, 3.0], [4.0, -5.0, 6.0]];
1842 let (mn, mx) = compute_aabb(&pts);
1843 assert_eq!(mn, [-1.0, -5.0, 3.0]);
1844 assert_eq!(mx, [4.0, 2.0, 6.0]);
1845 }
1846
1847 #[test]
1848 fn test_obj_roundtrip() {
1849 let mesh = unit_box_mesh();
1850 let obj = mesh_to_obj_string(&mesh);
1851 let parsed = obj_string_to_mesh(&obj);
1852 assert_eq!(parsed.vertices.len(), mesh.vertices.len());
1853 assert_eq!(parsed.triangle_count(), mesh.triangle_count());
1854 }
1855
1856 #[test]
1857 fn test_convex_hull_from_mesh() {
1858 let mesh = unit_box_mesh();
1859 let hull = convex_hull_from_mesh(&mesh);
1860 assert!(!hull.vertices.is_empty());
1861 }
1862}