Skip to main content

oxiphysics_python/world_api/
geometry.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Geometry Queries: AABB, Sphere, ConvexHull.
5
6#![allow(missing_docs)]
7
8// ===========================================================================
9// Geometry Queries
10// ===========================================================================
11
12/// Axis-aligned bounding box (AABB) in 3-D.
13#[derive(Debug, Clone, Copy)]
14#[allow(dead_code)]
15pub struct PyAabb {
16    /// Minimum corner `[xmin, ymin, zmin]`.
17    pub min: [f64; 3],
18    /// Maximum corner `[xmax, ymax, zmax]`.
19    pub max: [f64; 3],
20}
21
22impl PyAabb {
23    /// Create from min/max corner points.
24    pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
25        Self { min, max }
26    }
27
28    /// Create a unit cube centred at the origin.
29    pub fn unit() -> Self {
30        Self::new([-0.5; 3], [0.5; 3])
31    }
32
33    /// Create from centre and half-extents.
34    pub fn from_center_half_extents(center: [f64; 3], he: [f64; 3]) -> Self {
35        Self {
36            min: [center[0] - he[0], center[1] - he[1], center[2] - he[2]],
37            max: [center[0] + he[0], center[1] + he[1], center[2] + he[2]],
38        }
39    }
40
41    /// Centre point of the AABB.
42    pub fn center(&self) -> [f64; 3] {
43        [
44            (self.min[0] + self.max[0]) * 0.5,
45            (self.min[1] + self.max[1]) * 0.5,
46            (self.min[2] + self.max[2]) * 0.5,
47        ]
48    }
49
50    /// Half-extents in each dimension.
51    pub fn half_extents(&self) -> [f64; 3] {
52        [
53            (self.max[0] - self.min[0]) * 0.5,
54            (self.max[1] - self.min[1]) * 0.5,
55            (self.max[2] - self.min[2]) * 0.5,
56        ]
57    }
58
59    /// Surface area of the AABB.
60    pub fn surface_area(&self) -> f64 {
61        let dx = self.max[0] - self.min[0];
62        let dy = self.max[1] - self.min[1];
63        let dz = self.max[2] - self.min[2];
64        2.0 * (dx * dy + dy * dz + dz * dx)
65    }
66
67    /// Volume of the AABB.
68    pub fn volume(&self) -> f64 {
69        let dx = (self.max[0] - self.min[0]).max(0.0);
70        let dy = (self.max[1] - self.min[1]).max(0.0);
71        let dz = (self.max[2] - self.min[2]).max(0.0);
72        dx * dy * dz
73    }
74
75    /// Whether a point is contained within (or on the boundary of) this AABB.
76    pub fn contains_point(&self, p: [f64; 3]) -> bool {
77        p[0] >= self.min[0]
78            && p[0] <= self.max[0]
79            && p[1] >= self.min[1]
80            && p[1] <= self.max[1]
81            && p[2] >= self.min[2]
82            && p[2] <= self.max[2]
83    }
84
85    /// Whether this AABB intersects another AABB.
86    pub fn intersects(&self, other: &PyAabb) -> bool {
87        self.min[0] <= other.max[0]
88            && self.max[0] >= other.min[0]
89            && self.min[1] <= other.max[1]
90            && self.max[1] >= other.min[1]
91            && self.min[2] <= other.max[2]
92            && self.max[2] >= other.min[2]
93    }
94
95    /// Merge with another AABB, producing the smallest enclosing AABB.
96    pub fn merged(&self, other: &PyAabb) -> PyAabb {
97        PyAabb {
98            min: [
99                self.min[0].min(other.min[0]),
100                self.min[1].min(other.min[1]),
101                self.min[2].min(other.min[2]),
102            ],
103            max: [
104                self.max[0].max(other.max[0]),
105                self.max[1].max(other.max[1]),
106                self.max[2].max(other.max[2]),
107            ],
108        }
109    }
110}
111
112/// Sphere geometry query helper.
113#[derive(Debug, Clone, Copy)]
114#[allow(dead_code)]
115pub struct PySphere {
116    /// Centre of the sphere.
117    pub center: [f64; 3],
118    /// Radius.
119    pub radius: f64,
120}
121
122impl PySphere {
123    /// Create a sphere from centre and radius.
124    pub fn new(center: [f64; 3], radius: f64) -> Self {
125        Self {
126            center,
127            radius: radius.max(0.0),
128        }
129    }
130
131    /// Unit sphere at origin.
132    pub fn unit() -> Self {
133        Self::new([0.0; 3], 1.0)
134    }
135
136    /// Surface area 4π r².
137    pub fn surface_area(&self) -> f64 {
138        4.0 * std::f64::consts::PI * self.radius * self.radius
139    }
140
141    /// Volume 4/3 π r³.
142    pub fn volume(&self) -> f64 {
143        (4.0 / 3.0) * std::f64::consts::PI * self.radius * self.radius * self.radius
144    }
145
146    /// Whether a point is inside (or on the surface of) this sphere.
147    pub fn contains_point(&self, p: [f64; 3]) -> bool {
148        let dx = p[0] - self.center[0];
149        let dy = p[1] - self.center[1];
150        let dz = p[2] - self.center[2];
151        dx * dx + dy * dy + dz * dz <= self.radius * self.radius
152    }
153
154    /// Signed distance from the sphere surface to point `p`.
155    ///
156    /// Negative inside, positive outside.
157    pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
158        let dx = p[0] - self.center[0];
159        let dy = p[1] - self.center[1];
160        let dz = p[2] - self.center[2];
161        (dx * dx + dy * dy + dz * dz).sqrt() - self.radius
162    }
163
164    /// Whether this sphere overlaps another sphere.
165    pub fn overlaps(&self, other: &PySphere) -> bool {
166        let dx = self.center[0] - other.center[0];
167        let dy = self.center[1] - other.center[1];
168        let dz = self.center[2] - other.center[2];
169        let dist2 = dx * dx + dy * dy + dz * dz;
170        let sum_r = self.radius + other.radius;
171        dist2 <= sum_r * sum_r
172    }
173
174    /// Bounding AABB of this sphere.
175    pub fn aabb(&self) -> PyAabb {
176        PyAabb::from_center_half_extents(self.center, [self.radius; 3])
177    }
178}
179
180/// A convex hull stored as a list of vertices.
181///
182/// The hull is not computed internally; the caller is responsible for
183/// providing convex vertices. Methods here are geometry helpers.
184#[derive(Debug, Clone)]
185#[allow(dead_code)]
186pub struct PyConvexHull {
187    /// Vertices of the convex hull.
188    pub vertices: Vec<[f64; 3]>,
189}
190
191impl PyConvexHull {
192    /// Create a convex hull from a list of vertices.
193    pub fn new(vertices: Vec<[f64; 3]>) -> Self {
194        Self { vertices }
195    }
196
197    /// Create a convex hull approximating a unit cube.
198    pub fn unit_cube() -> Self {
199        let verts: Vec<[f64; 3]> = [
200            [-0.5, -0.5, -0.5],
201            [0.5, -0.5, -0.5],
202            [0.5, 0.5, -0.5],
203            [-0.5, 0.5, -0.5],
204            [-0.5, -0.5, 0.5],
205            [0.5, -0.5, 0.5],
206            [0.5, 0.5, 0.5],
207            [-0.5, 0.5, 0.5],
208        ]
209        .to_vec();
210        Self::new(verts)
211    }
212
213    /// Number of vertices in the hull.
214    pub fn vertex_count(&self) -> usize {
215        self.vertices.len()
216    }
217
218    /// Compute the axis-aligned bounding box of all vertices.
219    pub fn aabb(&self) -> Option<PyAabb> {
220        if self.vertices.is_empty() {
221            return None;
222        }
223        let mut mn = self.vertices[0];
224        let mut mx = self.vertices[0];
225        for &v in &self.vertices {
226            for k in 0..3 {
227                mn[k] = mn[k].min(v[k]);
228                mx[k] = mx[k].max(v[k]);
229            }
230        }
231        Some(PyAabb::new(mn, mx))
232    }
233
234    /// Centroid (arithmetic mean) of all vertices.
235    pub fn centroid(&self) -> Option<[f64; 3]> {
236        if self.vertices.is_empty() {
237            return None;
238        }
239        let n = self.vertices.len() as f64;
240        let mut c = [0.0f64; 3];
241        for v in &self.vertices {
242            c[0] += v[0];
243            c[1] += v[1];
244            c[2] += v[2];
245        }
246        Some([c[0] / n, c[1] / n, c[2] / n])
247    }
248
249    /// Naively check if a point is "inside" the hull by checking it is
250    /// within the AABB (a conservative, approximate test).
251    pub fn may_contain_point(&self, p: [f64; 3]) -> bool {
252        match self.aabb() {
253            Some(aabb) => aabb.contains_point(p),
254            None => false,
255        }
256    }
257
258    /// Support function: find vertex furthest along direction `d`.
259    pub fn support(&self, d: [f64; 3]) -> Option<[f64; 3]> {
260        self.vertices
261            .iter()
262            .max_by(|a, b| {
263                let da = a[0] * d[0] + a[1] * d[1] + a[2] * d[2];
264                let db = b[0] * d[0] + b[1] * d[1] + b[2] * d[2];
265                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
266            })
267            .copied()
268    }
269}