Skip to main content

oxihuman_core/
triangular_grid.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Triangular grid with barycentric coordinate lookup.
5
6#![allow(dead_code)]
7
8/// A triangle defined by three 2D vertices.
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy)]
11pub struct Triangle2D {
12    pub a: [f32; 2],
13    pub b: [f32; 2],
14    pub c: [f32; 2],
15}
16
17#[allow(dead_code)]
18impl Triangle2D {
19    pub fn new(a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> Self {
20        Self { a, b, c }
21    }
22
23    /// Compute barycentric coordinates (u, v, w) for point p.
24    /// Returns None if the triangle is degenerate.
25    pub fn barycentric(&self, p: [f32; 2]) -> Option<[f32; 3]> {
26        let v0 = [self.b[0] - self.a[0], self.b[1] - self.a[1]];
27        let v1 = [self.c[0] - self.a[0], self.c[1] - self.a[1]];
28        let v2 = [p[0] - self.a[0], p[1] - self.a[1]];
29        let d00 = dot2(v0, v0);
30        let d01 = dot2(v0, v1);
31        let d11 = dot2(v1, v1);
32        let d20 = dot2(v2, v0);
33        let d21 = dot2(v2, v1);
34        let denom = d00 * d11 - d01 * d01;
35        if denom.abs() < 1e-10 {
36            return None;
37        }
38        let v = (d11 * d20 - d01 * d21) / denom;
39        let w = (d00 * d21 - d01 * d20) / denom;
40        let u = 1.0 - v - w;
41        Some([u, v, w])
42    }
43
44    /// Test if point p is inside the triangle.
45    pub fn contains(&self, p: [f32; 2]) -> bool {
46        match self.barycentric(p) {
47            None => false,
48            Some([u, v, w]) => u >= 0.0 && v >= 0.0 && w >= 0.0,
49        }
50    }
51
52    /// Interpolate a scalar value at barycentric coords (u, v, w).
53    pub fn interpolate(&self, values: [f32; 3], bary: [f32; 3]) -> f32 {
54        values[0] * bary[0] + values[1] * bary[1] + values[2] * bary[2]
55    }
56
57    /// Area of the triangle (positive for CCW).
58    pub fn signed_area(&self) -> f32 {
59        let v0 = [self.b[0] - self.a[0], self.b[1] - self.a[1]];
60        let v1 = [self.c[0] - self.a[0], self.c[1] - self.a[1]];
61        0.5 * (v0[0] * v1[1] - v0[1] * v1[0])
62    }
63
64    pub fn area(&self) -> f32 {
65        self.signed_area().abs()
66    }
67
68    /// Centroid of the triangle.
69    pub fn centroid(&self) -> [f32; 2] {
70        [
71            (self.a[0] + self.b[0] + self.c[0]) / 3.0,
72            (self.a[1] + self.b[1] + self.c[1]) / 3.0,
73        ]
74    }
75}
76
77fn dot2(a: [f32; 2], b: [f32; 2]) -> f32 {
78    a[0] * b[0] + a[1] * b[1]
79}
80
81/// A uniform triangular grid covering a rectangle.
82#[allow(dead_code)]
83#[derive(Debug, Clone)]
84pub struct TriGrid {
85    pub origin: [f32; 2],
86    pub cell_size: f32,
87    pub cols: usize,
88    pub rows: usize,
89}
90
91#[allow(dead_code)]
92impl TriGrid {
93    pub fn new(origin: [f32; 2], cell_size: f32, cols: usize, rows: usize) -> Self {
94        Self {
95            origin,
96            cell_size,
97            cols,
98            rows,
99        }
100    }
101
102    /// Get the two triangles covering cell (col, row).
103    pub fn cell_triangles(&self, col: usize, row: usize) -> [Triangle2D; 2] {
104        let s = self.cell_size;
105        let ox = self.origin[0] + col as f32 * s;
106        let oy = self.origin[1] + row as f32 * s;
107        let a = [ox, oy];
108        let b = [ox + s, oy];
109        let c = [ox + s, oy + s];
110        let d = [ox, oy + s];
111        [Triangle2D::new(a, b, d), Triangle2D::new(b, c, d)]
112    }
113
114    /// Find which triangle in the grid contains point p.
115    pub fn find_triangle(&self, p: [f32; 2]) -> Option<(usize, usize, usize)> {
116        let s = self.cell_size;
117        let col = ((p[0] - self.origin[0]) / s).floor() as i32;
118        let row = ((p[1] - self.origin[1]) / s).floor() as i32;
119        if col < 0 || row < 0 || col as usize >= self.cols || row as usize >= self.rows {
120            return None;
121        }
122        let tris = self.cell_triangles(col as usize, row as usize);
123        for (i, tri) in tris.iter().enumerate() {
124            if tri.contains(p) {
125                return Some((col as usize, row as usize, i));
126            }
127        }
128        None
129    }
130
131    /// Total number of triangles in the grid.
132    pub fn triangle_count(&self) -> usize {
133        self.cols * self.rows * 2
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    fn unit_tri() -> Triangle2D {
142        Triangle2D::new([0.0, 0.0], [1.0, 0.0], [0.0, 1.0])
143    }
144
145    #[test]
146    fn centroid_inside() {
147        let t = unit_tri();
148        let c = t.centroid();
149        assert!(t.contains(c));
150    }
151
152    #[test]
153    fn barycentric_vertex_a() {
154        let t = unit_tri();
155        let [u, v, w] = t.barycentric([0.0, 0.0]).expect("should succeed");
156        assert!((u - 1.0).abs() < 1e-5);
157        assert!(v.abs() < 1e-5);
158        assert!(w.abs() < 1e-5);
159    }
160
161    #[test]
162    fn point_outside_not_contained() {
163        let t = unit_tri();
164        assert!(!t.contains([1.0, 1.0]));
165    }
166
167    #[test]
168    fn area_unit_tri() {
169        let t = unit_tri();
170        assert!((t.area() - 0.5).abs() < 1e-5);
171    }
172
173    #[test]
174    fn interpolate_at_vertex_a() {
175        let t = unit_tri();
176        let bary = t.barycentric([0.0, 0.0]).expect("should succeed");
177        let val = t.interpolate([3.0, 0.0, 0.0], bary);
178        assert!((val - 3.0).abs() < 1e-4);
179    }
180
181    #[test]
182    fn grid_triangle_count() {
183        let g = TriGrid::new([0.0, 0.0], 1.0, 4, 3);
184        assert_eq!(g.triangle_count(), 24);
185    }
186
187    #[test]
188    fn find_triangle_origin() {
189        let g = TriGrid::new([0.0, 0.0], 1.0, 4, 4);
190        assert!(g.find_triangle([0.1, 0.1]).is_some());
191    }
192
193    #[test]
194    fn find_triangle_outside_returns_none() {
195        let g = TriGrid::new([0.0, 0.0], 1.0, 4, 4);
196        assert!(g.find_triangle([-0.1, 0.5]).is_none());
197    }
198
199    #[test]
200    fn degenerate_barycentric_is_none() {
201        let t = Triangle2D::new([0.0, 0.0], [1.0, 0.0], [2.0, 0.0]);
202        assert!(t.barycentric([0.5, 0.0]).is_none());
203    }
204
205    #[test]
206    fn signed_area_ccw_positive() {
207        let t = unit_tri();
208        assert!(t.signed_area() > 0.0);
209    }
210}