1#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum Sign {
27 Negative,
29 Zero,
31 Positive,
33}
34
35impl Sign {
36 #[must_use]
39 pub fn of(value: f64) -> Self {
40 if value > 0.0 {
41 Self::Positive
42 } else if value < 0.0 {
43 Self::Negative
44 } else {
45 Self::Zero
46 }
47 }
48
49 #[must_use]
51 pub const fn is_zero(self) -> bool {
52 matches!(self, Self::Zero)
53 }
54
55 #[must_use]
57 pub const fn reversed(self) -> Self {
58 match self {
59 Self::Negative => Self::Positive,
60 Self::Zero => Self::Zero,
61 Self::Positive => Self::Negative,
62 }
63 }
64}
65
66pub type P2 = [f64; 2];
68pub type P3 = [f64; 3];
70
71pub trait Predicates {
76 fn orient2d(a: P2, b: P2, c: P2) -> Sign;
81
82 fn orient3d(a: P3, b: P3, c: P3, d: P3) -> Sign;
88
89 fn incircle(a: P2, b: P2, c: P2, d: P2) -> Sign;
94
95 fn insphere(a: P3, b: P3, c: P3, d: P3, e: P3) -> Sign;
100
101 fn is_left_of(a: P2, b: P2, c: P2) -> bool {
103 Self::orient2d(a, b, c) == Sign::Positive
104 }
105
106 fn are_collinear(a: P2, b: P2, c: P2) -> bool {
108 Self::orient2d(a, b, c).is_zero()
109 }
110
111 fn are_coplanar(a: P3, b: P3, c: P3, d: P3) -> bool {
113 Self::orient3d(a, b, c, d).is_zero()
114 }
115}
116
117#[derive(Debug, Clone, Copy, Default)]
126pub struct Exact;
127
128#[derive(Debug, Clone, Copy, Default)]
138pub struct Fast;
139
140fn c2(p: P2) -> robust::Coord<f64> {
141 robust::Coord { x: p[0], y: p[1] }
142}
143
144fn c3(p: P3) -> robust::Coord3D<f64> {
145 robust::Coord3D {
146 x: p[0],
147 y: p[1],
148 z: p[2],
149 }
150}
151
152impl Predicates for Exact {
153 fn orient2d(a: P2, b: P2, c: P2) -> Sign {
154 Sign::of(robust::orient2d(c2(a), c2(b), c2(c)))
155 }
156
157 fn orient3d(a: P3, b: P3, c: P3, d: P3) -> Sign {
158 Sign::of(robust::orient3d(c3(a), c3(b), c3(c), c3(d)))
159 }
160
161 fn incircle(a: P2, b: P2, c: P2, d: P2) -> Sign {
162 Sign::of(robust::incircle(c2(a), c2(b), c2(c), c2(d)))
163 }
164
165 fn insphere(a: P3, b: P3, c: P3, d: P3, e: P3) -> Sign {
166 Sign::of(robust::insphere(c3(a), c3(b), c3(c), c3(d), c3(e)))
167 }
168}
169
170impl Predicates for Fast {
171 fn orient2d(a: P2, b: P2, c: P2) -> Sign {
172 Sign::of((a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) * (b[0] - c[0]))
173 }
174
175 fn orient3d(a: P3, b: P3, c: P3, d: P3) -> Sign {
176 let ad = [a[0] - d[0], a[1] - d[1], a[2] - d[2]];
177 let bd = [b[0] - d[0], b[1] - d[1], b[2] - d[2]];
178 let cd = [c[0] - d[0], c[1] - d[1], c[2] - d[2]];
179 let det = ad[0] * (bd[1] * cd[2] - bd[2] * cd[1]) - bd[0] * (ad[1] * cd[2] - ad[2] * cd[1])
180 + cd[0] * (ad[1] * bd[2] - ad[2] * bd[1]);
181 Sign::of(det)
182 }
183
184 fn incircle(a: P2, b: P2, c: P2, d: P2) -> Sign {
185 let ad = [a[0] - d[0], a[1] - d[1]];
186 let bd = [b[0] - d[0], b[1] - d[1]];
187 let cd = [c[0] - d[0], c[1] - d[1]];
188 let alift = ad[0].mul_add(ad[0], ad[1] * ad[1]);
189 let blift = bd[0].mul_add(bd[0], bd[1] * bd[1]);
190 let clift = cd[0].mul_add(cd[0], cd[1] * cd[1]);
191 let det = alift * (bd[0] * cd[1] - cd[0] * bd[1]) - blift * (ad[0] * cd[1] - cd[0] * ad[1])
192 + clift * (ad[0] * bd[1] - bd[0] * ad[1]);
193 Sign::of(det)
194 }
195
196 fn insphere(a: P3, b: P3, c: P3, d: P3, e: P3) -> Sign {
197 let lift = |p: P3| {
198 let v = [p[0] - e[0], p[1] - e[1], p[2] - e[2]];
199 (v, v[0].mul_add(v[0], v[1].mul_add(v[1], v[2] * v[2])))
200 };
201 let (ae, al) = lift(a);
202 let (be, bl) = lift(b);
203 let (ce, cl) = lift(c);
204 let (de, dl) = lift(d);
205
206 let det3 = |p: [f64; 3], q: [f64; 3], r: [f64; 3]| {
207 p[0] * (q[1] * r[2] - q[2] * r[1]) - p[1] * (q[0] * r[2] - q[2] * r[0])
208 + p[2] * (q[0] * r[1] - q[1] * r[0])
209 };
210 let det = -al * det3(be, ce, de) + bl * det3(ae, ce, de) - cl * det3(ae, be, de)
211 + dl * det3(ae, be, ce);
212 Sign::of(det)
213 }
214}
215
216#[cfg(test)]
217#[allow(clippy::unwrap_used)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn orient2d_sign_convention() {
223 let a = [0.0, 0.0];
224 let b = [1.0, 0.0];
225 assert_eq!(Exact::orient2d(a, b, [0.0, 1.0]), Sign::Positive);
226 assert_eq!(Exact::orient2d(a, b, [0.0, -1.0]), Sign::Negative);
227 assert_eq!(Exact::orient2d(a, b, [2.0, 0.0]), Sign::Zero);
228 assert!(Exact::is_left_of(a, b, [0.5, 0.5]));
229 assert!(Exact::are_collinear(a, b, [7.0, 0.0]));
230 }
231
232 #[test]
233 fn orient3d_sign_convention_and_coplanarity() {
234 let a = [0.0, 0.0, 0.0];
235 let b = [1.0, 0.0, 0.0];
236 let c = [0.0, 1.0, 0.0];
237 let above = Exact::orient3d(a, b, c, [0.0, 0.0, 1.0]);
240 let below = Exact::orient3d(a, b, c, [0.0, 0.0, -1.0]);
241 assert_eq!(above, below.reversed());
242 assert!(!above.is_zero());
243 assert!(Exact::are_coplanar(a, b, c, [3.0, -4.0, 0.0]));
244 }
245
246 #[test]
247 fn exact_and_fast_agree_when_well_separated() {
248 let pts = [
249 ([0.0, 0.0], [3.0, 1.0], [1.0, 4.0]),
250 ([-2.0, 5.0], [7.0, -1.0], [0.25, 0.5]),
251 ([1e6, 1e6], [-1e6, 2e6], [0.0, 0.0]),
252 ];
253 for (a, b, c) in pts {
254 assert_eq!(Exact::orient2d(a, b, c), Fast::orient2d(a, b, c));
255 }
256 }
257
258 #[test]
259 fn exact_predicates_survive_a_case_naive_arithmetic_gets_wrong() {
260 let a = [0.5, 0.5];
264 let b = [12.0, 12.0];
265 let c = [24.000_000_000_000_004, 24.0];
266
267 assert_eq!(Exact::orient2d(a, b, c), Sign::Negative);
268 assert!(!Exact::orient2d(a, b, c).is_zero());
271 }
272
273 #[test]
274 fn incircle_sign_convention() {
275 let a = [-1.0, -1.0];
277 let b = [1.0, -1.0];
278 let c = [1.0, 1.0];
279 assert_eq!(
280 Exact::orient2d(a, b, c),
281 Sign::Positive,
282 "test setup must be CCW"
283 );
284 assert_eq!(Exact::incircle(a, b, c, [0.0, 0.0]), Sign::Positive);
285 assert_eq!(Exact::incircle(a, b, c, [5.0, 5.0]), Sign::Negative);
286 assert_eq!(
287 Exact::incircle(a, b, c, [-1.0, 1.0]),
288 Sign::Zero,
289 "cocircular"
290 );
291 }
292
293 #[test]
294 fn insphere_sign_convention() {
295 let a = [0.0, 0.0, 0.0];
296 let b = [1.0, 0.0, 0.0];
297 let c = [0.0, 1.0, 0.0];
298 let d = [0.0, 0.0, 1.0];
299 let (a, b, c, d) = if Exact::orient3d(a, b, c, d) == Sign::Positive {
301 (a, b, c, d)
302 } else {
303 (a, c, b, d)
304 };
305 let inside = Exact::insphere(a, b, c, d, [0.25, 0.25, 0.25]);
306 let outside = Exact::insphere(a, b, c, d, [10.0, 10.0, 10.0]);
307 assert_eq!(inside, Sign::Positive);
308 assert_eq!(outside, Sign::Negative);
309 }
310
311 #[test]
312 fn nan_is_treated_as_degenerate_not_propagated() {
313 assert_eq!(Sign::of(f64::NAN), Sign::Zero);
314 assert_eq!(Sign::of(0.0), Sign::Zero);
315 assert_eq!(Sign::of(-0.0), Sign::Zero);
316 }
317
318 #[test]
319 fn reversed_is_an_involution() {
320 for s in [Sign::Negative, Sign::Zero, Sign::Positive] {
321 assert_eq!(s.reversed().reversed(), s);
322 }
323 }
324}