| 1 | // pathfinder/geometry/src/basic/transform3d.rs |
| 2 | // |
| 3 | // Copyright © 2019 The Pathfinder Project Developers. |
| 4 | // |
| 5 | // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
| 6 | // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| 7 | // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
| 8 | // option. This file may not be copied, modified, or distributed |
| 9 | // except according to those terms. |
| 10 | |
| 11 | //! 3D transforms that can be applied to paths. |
| 12 | |
| 13 | use crate::rect::RectF; |
| 14 | use crate::transform2d::Matrix2x2F; |
| 15 | use crate::vector::{Vector2F, Vector2I, Vector3F, Vector4F}; |
| 16 | use pathfinder_simd::default::F32x4; |
| 17 | use std::ops::{Add, Mul, MulAssign, Neg}; |
| 18 | |
| 19 | /// An transform, optimized with SIMD. |
| 20 | /// |
| 21 | /// In column-major order. |
| 22 | #[derive (Clone, Copy, Debug, PartialEq)] |
| 23 | #[repr (C)] |
| 24 | pub struct Transform4F { |
| 25 | pub c0: F32x4, |
| 26 | pub c1: F32x4, |
| 27 | pub c2: F32x4, |
| 28 | pub c3: F32x4, |
| 29 | } |
| 30 | |
| 31 | impl Default for Transform4F { |
| 32 | #[inline ] |
| 33 | fn default() -> Transform4F { |
| 34 | Transform4F { |
| 35 | c0: F32x4::new(a:1.0, b:0.0, c:0.0, d:0.0), |
| 36 | c1: F32x4::new(a:0.0, b:1.0, c:0.0, d:0.0), |
| 37 | c2: F32x4::new(a:0.0, b:0.0, c:1.0, d:0.0), |
| 38 | c3: F32x4::new(a:0.0, b:0.0, c:0.0, d:1.0), |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | impl Transform4F { |
| 44 | #[inline ] |
| 45 | pub fn row_major( |
| 46 | m00: f32, |
| 47 | m01: f32, |
| 48 | m02: f32, |
| 49 | m03: f32, |
| 50 | m10: f32, |
| 51 | m11: f32, |
| 52 | m12: f32, |
| 53 | m13: f32, |
| 54 | m20: f32, |
| 55 | m21: f32, |
| 56 | m22: f32, |
| 57 | m23: f32, |
| 58 | m30: f32, |
| 59 | m31: f32, |
| 60 | m32: f32, |
| 61 | m33: f32, |
| 62 | ) -> Transform4F { |
| 63 | Transform4F { |
| 64 | c0: F32x4::new(m00, m10, m20, m30), |
| 65 | c1: F32x4::new(m01, m11, m21, m31), |
| 66 | c2: F32x4::new(m02, m12, m22, m32), |
| 67 | c3: F32x4::new(m03, m13, m23, m33), |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | #[inline ] |
| 72 | pub fn from_scale(scale: Vector4F) -> Transform4F { |
| 73 | Transform4F { |
| 74 | c0: F32x4::new(scale.x(), 0.0, 0.0, 0.0), |
| 75 | c1: F32x4::new(0.0, scale.y(), 0.0, 0.0), |
| 76 | c2: F32x4::new(0.0, 0.0, scale.z(), 0.0), |
| 77 | c3: F32x4::new(0.0, 0.0, 0.0, 1.0), |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | #[inline ] |
| 82 | pub fn from_uniform_scale(factor: f32) -> Transform4F { |
| 83 | Transform4F::from_scale(Vector4F::splat(factor)) |
| 84 | } |
| 85 | |
| 86 | #[inline ] |
| 87 | pub fn from_translation(mut translation: Vector4F) -> Transform4F { |
| 88 | translation.set_w(1.0); |
| 89 | Transform4F { c3: translation.0, ..Transform4F::default() } |
| 90 | } |
| 91 | |
| 92 | // TODO(pcwalton): Optimize. |
| 93 | pub fn from_rotation(yaw: f32, pitch: f32, roll: f32) -> Transform4F { |
| 94 | let (cos_b, sin_b) = (yaw.cos(), yaw.sin()); |
| 95 | let (cos_c, sin_c) = (pitch.cos(), pitch.sin()); |
| 96 | let (cos_a, sin_a) = (roll.cos(), roll.sin()); |
| 97 | let m00 = cos_a * cos_b; |
| 98 | let m01 = cos_a * sin_b * sin_c - sin_a * cos_c; |
| 99 | let m02 = cos_a * sin_b * cos_c + sin_a * sin_c; |
| 100 | let m10 = sin_a * cos_b; |
| 101 | let m11 = sin_a * sin_b * sin_c + cos_a * cos_c; |
| 102 | let m12 = sin_a * sin_b * cos_c - cos_a * sin_c; |
| 103 | let m20 = -sin_b; |
| 104 | let m21 = cos_b * sin_c; |
| 105 | let m22 = cos_b * cos_c; |
| 106 | Transform4F::row_major( |
| 107 | m00, m01, m02, 0.0, m10, m11, m12, 0.0, m20, m21, m22, 0.0, 0.0, 0.0, 0.0, 1.0, |
| 108 | ) |
| 109 | } |
| 110 | |
| 111 | /// Creates a rotation matrix from the given quaternion. |
| 112 | /// |
| 113 | /// The quaternion is expected to be packed into a SIMD type (x, y, z, w) corresponding to |
| 114 | /// x + yi + zj + wk. |
| 115 | pub fn from_rotation_quaternion(q: F32x4) -> Transform4F { |
| 116 | // TODO(pcwalton): Optimize better with more shuffles. |
| 117 | let (mut sq, mut w, mut xy_xz_yz) = (q * q, q.wwww() * q, q.xxyy() * q.yzzy()); |
| 118 | sq += sq; |
| 119 | w += w; |
| 120 | xy_xz_yz += xy_xz_yz; |
| 121 | let diag = F32x4::splat(1.0) - (sq.yxxy() + sq.zzyy()); |
| 122 | let (wx2, wy2, wz2) = (w.x(), w.y(), w.z()); |
| 123 | let (xy2, xz2, yz2) = (xy_xz_yz.x(), xy_xz_yz.y(), xy_xz_yz.z()); |
| 124 | Transform4F::row_major( |
| 125 | diag.x(), |
| 126 | xy2 - wz2, |
| 127 | xz2 + wy2, |
| 128 | 0.0, |
| 129 | xy2 + wz2, |
| 130 | diag.y(), |
| 131 | yz2 - wx2, |
| 132 | 0.0, |
| 133 | xz2 - wy2, |
| 134 | yz2 + wx2, |
| 135 | diag.z(), |
| 136 | 0.0, |
| 137 | 0.0, |
| 138 | 0.0, |
| 139 | 0.0, |
| 140 | 1.0, |
| 141 | ) |
| 142 | } |
| 143 | |
| 144 | /// Just like `glOrtho()`. |
| 145 | #[inline ] |
| 146 | pub fn from_ortho( |
| 147 | left: f32, |
| 148 | right: f32, |
| 149 | bottom: f32, |
| 150 | top: f32, |
| 151 | near_val: f32, |
| 152 | far_val: f32, |
| 153 | ) -> Transform4F { |
| 154 | let x_inv = 1.0 / (right - left); |
| 155 | let y_inv = 1.0 / (top - bottom); |
| 156 | let z_inv = 1.0 / (far_val - near_val); |
| 157 | let tx = -(right + left) * x_inv; |
| 158 | let ty = -(top + bottom) * y_inv; |
| 159 | let tz = -(far_val + near_val) * z_inv; |
| 160 | Transform4F::row_major( |
| 161 | 2.0 * x_inv, |
| 162 | 0.0, |
| 163 | 0.0, |
| 164 | tx, |
| 165 | 0.0, |
| 166 | 2.0 * y_inv, |
| 167 | 0.0, |
| 168 | ty, |
| 169 | 0.0, |
| 170 | 0.0, |
| 171 | -2.0 * z_inv, |
| 172 | tz, |
| 173 | 0.0, |
| 174 | 0.0, |
| 175 | 0.0, |
| 176 | 1.0, |
| 177 | ) |
| 178 | } |
| 179 | |
| 180 | /// Linearly interpolate between transforms |
| 181 | pub fn lerp(&self, weight: f32, other: &Transform4F) -> Transform4F { |
| 182 | let c0 = self.c0 * F32x4::splat(weight) + other.c0 * F32x4::splat(1.0 - weight); |
| 183 | let c1 = self.c1 * F32x4::splat(weight) + other.c1 * F32x4::splat(1.0 - weight); |
| 184 | let c2 = self.c2 * F32x4::splat(weight) + other.c2 * F32x4::splat(1.0 - weight); |
| 185 | let c3 = self.c3 * F32x4::splat(weight) + other.c3 * F32x4::splat(1.0 - weight); |
| 186 | Transform4F { c0, c1, c2, c3 } |
| 187 | } |
| 188 | |
| 189 | /// Just like `gluPerspective()`. |
| 190 | #[inline ] |
| 191 | pub fn from_perspective(fov_y: f32, aspect: f32, z_near: f32, z_far: f32) -> Transform4F { |
| 192 | let f = 1.0 / (fov_y * 0.5).tan(); |
| 193 | let z_denom = 1.0 / (z_near - z_far); |
| 194 | let m00 = f / aspect; |
| 195 | let m11 = f; |
| 196 | let m22 = (z_far + z_near) * z_denom; |
| 197 | let m23 = 2.0 * z_far * z_near * z_denom; |
| 198 | let m32 = -1.0; |
| 199 | Transform4F::row_major( |
| 200 | m00, 0.0, 0.0, 0.0, 0.0, m11, 0.0, 0.0, 0.0, 0.0, m22, m23, 0.0, 0.0, m32, 0.0, |
| 201 | ) |
| 202 | } |
| 203 | |
| 204 | /// Just like `gluLookAt()`. |
| 205 | #[inline ] |
| 206 | pub fn looking_at(eye: Vector3F, center: Vector3F, mut up: Vector3F) -> Transform4F { |
| 207 | let f = (center - eye).normalize(); |
| 208 | up = up.normalize(); |
| 209 | let s = f.cross(up); |
| 210 | let u = s.normalize().cross(f); |
| 211 | let minus_f = -f; |
| 212 | |
| 213 | // TODO(pcwalton): Use SIMD. This needs a matrix transpose: |
| 214 | // https://fgiesen.wordpress.com/2013/07/09/simd-transposes-1/ |
| 215 | let transform = Transform4F::row_major(s.x(), s.y(), s.z(), 0.0, |
| 216 | u.x(), u.y(), u.z(), 0.0, |
| 217 | minus_f.x(), minus_f.y(), minus_f.z(), 0.0, |
| 218 | 0.0, 0.0, 0.0, 1.0) * |
| 219 | Transform4F::from_translation((-eye).to_4d()); |
| 220 | transform |
| 221 | } |
| 222 | |
| 223 | // +- -+ |
| 224 | // | A B | |
| 225 | // | C D | |
| 226 | // +- -+ |
| 227 | #[inline ] |
| 228 | pub fn from_submatrices( |
| 229 | a: Matrix2x2F, |
| 230 | b: Matrix2x2F, |
| 231 | c: Matrix2x2F, |
| 232 | d: Matrix2x2F, |
| 233 | ) -> Transform4F { |
| 234 | Transform4F { |
| 235 | c0: a.0.concat_xy_xy(c.0), |
| 236 | c1: a.0.concat_zw_zw(c.0), |
| 237 | c2: b.0.concat_xy_xy(d.0), |
| 238 | c3: b.0.concat_zw_zw(d.0), |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | #[inline ] |
| 243 | pub fn rotate(&self, yaw: f32, pitch: f32, roll: f32) -> Transform4F { |
| 244 | Transform4F::from_rotation(yaw, pitch, roll) * *self |
| 245 | } |
| 246 | |
| 247 | #[inline ] |
| 248 | pub fn scale(&self, scale: Vector4F) -> Transform4F { |
| 249 | Transform4F::from_scale(scale) * *self |
| 250 | } |
| 251 | |
| 252 | #[inline ] |
| 253 | pub fn uniform_scale(&self, scale: f32) -> Transform4F { |
| 254 | Transform4F::from_uniform_scale(scale) * *self |
| 255 | } |
| 256 | |
| 257 | #[inline ] |
| 258 | pub fn translate(&self, translation: Vector4F) -> Transform4F { |
| 259 | Transform4F::from_translation(translation) * *self |
| 260 | } |
| 261 | |
| 262 | #[inline ] |
| 263 | pub fn upper_left(&self) -> Matrix2x2F { |
| 264 | Matrix2x2F(self.c0.concat_xy_xy(self.c1)) |
| 265 | } |
| 266 | |
| 267 | #[inline ] |
| 268 | pub fn upper_right(&self) -> Matrix2x2F { |
| 269 | Matrix2x2F(self.c2.concat_xy_xy(self.c3)) |
| 270 | } |
| 271 | |
| 272 | #[inline ] |
| 273 | pub fn lower_left(&self) -> Matrix2x2F { |
| 274 | Matrix2x2F(self.c0.concat_zw_zw(self.c1)) |
| 275 | } |
| 276 | |
| 277 | #[inline ] |
| 278 | pub fn lower_right(&self) -> Matrix2x2F { |
| 279 | Matrix2x2F(self.c2.concat_zw_zw(self.c3)) |
| 280 | } |
| 281 | |
| 282 | // https://en.wikipedia.org/wiki/Invertible_matrix#Blockwise_inversion |
| 283 | // |
| 284 | // If A is the upper left submatrix of this matrix, this method assumes that A and the Schur |
| 285 | // complement of A are invertible. |
| 286 | pub fn inverse(&self) -> Transform4F { |
| 287 | // Extract submatrices. |
| 288 | let (a, b) = (self.upper_left(), self.upper_right()); |
| 289 | let (c, d) = (self.lower_left(), self.lower_right()); |
| 290 | |
| 291 | // Compute temporary matrices. |
| 292 | let a_inv = a.inverse(); |
| 293 | let x = c * a_inv; |
| 294 | let y = (d - x * b).inverse(); |
| 295 | let z = a_inv * b; |
| 296 | |
| 297 | // Compute new submatrices. |
| 298 | let (a_new, b_new) = (a_inv + z * y * x, -z * y); |
| 299 | let (c_new, d_new) = (-y * x, y); |
| 300 | |
| 301 | // Construct inverse. |
| 302 | Transform4F::from_submatrices(a_new, b_new, c_new, d_new) |
| 303 | } |
| 304 | |
| 305 | pub fn approx_eq(&self, other: &Transform4F, epsilon: f32) -> bool { |
| 306 | self.c0.approx_eq(other.c0, epsilon) |
| 307 | && self.c1.approx_eq(other.c1, epsilon) |
| 308 | && self.c2.approx_eq(other.c2, epsilon) |
| 309 | && self.c3.approx_eq(other.c3, epsilon) |
| 310 | } |
| 311 | |
| 312 | #[inline ] |
| 313 | pub fn as_ptr(&self) -> *const f32 { |
| 314 | (&self.c0) as *const F32x4 as *const f32 |
| 315 | } |
| 316 | |
| 317 | #[inline ] |
| 318 | pub fn to_columns(&self) -> [F32x4; 4] { |
| 319 | [self.c0, self.c1, self.c2, self.c3] |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | impl Mul<Transform4F> for Transform4F { |
| 324 | type Output = Transform4F; |
| 325 | |
| 326 | // https://stackoverflow.com/a/18508113 |
| 327 | #[inline ] |
| 328 | fn mul(self, other: Transform4F) -> Transform4F { |
| 329 | return Transform4F { |
| 330 | c0: mul_col(&self, b_col:other.c0), |
| 331 | c1: mul_col(&self, b_col:other.c1), |
| 332 | c2: mul_col(&self, b_col:other.c2), |
| 333 | c3: mul_col(&self, b_col:other.c3), |
| 334 | }; |
| 335 | |
| 336 | #[inline ] |
| 337 | fn mul_col(a: &Transform4F, b_col: F32x4) -> F32x4 { |
| 338 | a.c0 * b_col.xxxx() + a.c1 * b_col.yyyy() + a.c2 * b_col.zzzz() + a.c3 * b_col.wwww() |
| 339 | } |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | impl Mul<Vector4F> for Transform4F { |
| 344 | type Output = Vector4F; |
| 345 | |
| 346 | #[inline ] |
| 347 | fn mul(self, vector: Vector4F) -> Vector4F { |
| 348 | let term0: F32x4 = self.c0 * F32x4::splat(vector.x()); |
| 349 | let term1: F32x4 = self.c1 * F32x4::splat(vector.y()); |
| 350 | let term2: F32x4 = self.c2 * F32x4::splat(vector.z()); |
| 351 | let term3: F32x4 = self.c3 * F32x4::splat(vector.w()); |
| 352 | Vector4F(term0 + term1 + term2 + term3) |
| 353 | } |
| 354 | } |
| 355 | |
| 356 | impl MulAssign<Transform4F> for Transform4F { |
| 357 | fn mul_assign(&mut self, other: Transform4F) { |
| 358 | *self = *self * other |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | impl Add<Matrix2x2F> for Matrix2x2F { |
| 363 | type Output = Matrix2x2F; |
| 364 | #[inline ] |
| 365 | fn add(self, other: Matrix2x2F) -> Matrix2x2F { |
| 366 | Matrix2x2F(self.0 + other.0) |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | impl Neg for Matrix2x2F { |
| 371 | type Output = Matrix2x2F; |
| 372 | #[inline ] |
| 373 | fn neg(self) -> Matrix2x2F { |
| 374 | Matrix2x2F(-self.0) |
| 375 | } |
| 376 | } |
| 377 | |
| 378 | #[derive (Clone, Copy, Debug)] |
| 379 | pub struct Perspective { |
| 380 | pub transform: Transform4F, |
| 381 | pub window_size: Vector2I, |
| 382 | } |
| 383 | |
| 384 | impl Perspective { |
| 385 | #[inline ] |
| 386 | pub fn new(transform: &Transform4F, window_size: Vector2I) -> Perspective { |
| 387 | Perspective { |
| 388 | transform: *transform, |
| 389 | window_size, |
| 390 | } |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | impl Mul<Transform4F> for Perspective { |
| 395 | type Output = Perspective; |
| 396 | #[inline ] |
| 397 | fn mul(self, other: Transform4F) -> Perspective { |
| 398 | Perspective { |
| 399 | transform: self.transform * other, |
| 400 | window_size: self.window_size, |
| 401 | } |
| 402 | } |
| 403 | } |
| 404 | |
| 405 | impl Mul<Vector2F> for Perspective { |
| 406 | type Output = Vector2F; |
| 407 | #[inline ] |
| 408 | fn mul(self, vector: Vector2F) -> Vector2F { |
| 409 | let point: Vector2F = (self.transform * vector.to_4d()).to_2d() * Vector2F::new(x:1.0, y:-1.0); |
| 410 | (point + 1.0) * self.window_size.to_f32() * 0.5 |
| 411 | } |
| 412 | } |
| 413 | |
| 414 | impl Mul<RectF> for Perspective { |
| 415 | type Output = RectF; |
| 416 | #[inline ] |
| 417 | fn mul(self, rect: RectF) -> RectF { |
| 418 | let (upper_left: Vector2F, upper_right: Vector2F) = (self * rect.origin(), self * rect.upper_right()); |
| 419 | let (lower_left: Vector2F, lower_right: Vector2F) = (self * rect.lower_left(), self * rect.lower_right()); |
| 420 | let min_point: Vector2F = upper_left.min(upper_right).min(lower_left).min(lower_right); |
| 421 | let max_point: Vector2F = upper_left.max(upper_right).max(lower_left).max(lower_right); |
| 422 | RectF::from_points(origin:min_point, lower_right:max_point) |
| 423 | } |
| 424 | } |
| 425 | |
| 426 | #[cfg (test)] |
| 427 | mod test { |
| 428 | use crate::vector::Vector4F; |
| 429 | use crate::transform3d::Transform4F; |
| 430 | |
| 431 | #[test ] |
| 432 | fn test_post_mul() { |
| 433 | let a = Transform4F::row_major( |
| 434 | 3.0, 1.0, 4.0, 5.0, 9.0, 2.0, 6.0, 5.0, 3.0, 5.0, 8.0, 9.0, 7.0, 9.0, 3.0, 2.0, |
| 435 | ); |
| 436 | let b = Transform4F::row_major( |
| 437 | 3.0, 8.0, 4.0, 6.0, 2.0, 6.0, 4.0, 3.0, 3.0, 8.0, 3.0, 2.0, 7.0, 9.0, 5.0, 0.0, |
| 438 | ); |
| 439 | let c = Transform4F::row_major( |
| 440 | 58.0, 107.0, 53.0, 29.0, 84.0, 177.0, 87.0, 72.0, 106.0, 199.0, 101.0, 49.0, 62.0, |
| 441 | 152.0, 83.0, 75.0, |
| 442 | ); |
| 443 | assert_eq!(a * b, c); |
| 444 | } |
| 445 | |
| 446 | #[test ] |
| 447 | fn test_pre_mul() { |
| 448 | let a = Transform4F::row_major( |
| 449 | 3.0, 1.0, 4.0, 5.0, 9.0, 2.0, 6.0, 5.0, 3.0, 5.0, 8.0, 9.0, 7.0, 9.0, 3.0, 2.0, |
| 450 | ); |
| 451 | let b = Transform4F::row_major( |
| 452 | 3.0, 8.0, 4.0, 6.0, 2.0, 6.0, 4.0, 3.0, 3.0, 8.0, 3.0, 2.0, 7.0, 9.0, 5.0, 0.0, |
| 453 | ); |
| 454 | let c = Transform4F::row_major( |
| 455 | 135.0, 93.0, 110.0, 103.0, 93.0, 61.0, 85.0, 82.0, 104.0, 52.0, 90.0, 86.0, 117.0, |
| 456 | 50.0, 122.0, 125.0, |
| 457 | ); |
| 458 | assert_eq!(b * a, c); |
| 459 | } |
| 460 | |
| 461 | #[test ] |
| 462 | fn test_transform_point() { |
| 463 | let a = Transform4F::row_major( |
| 464 | 3.0, 1.0, 4.0, 5.0, 9.0, 2.0, 6.0, 5.0, 3.0, 5.0, 8.0, 9.0, 7.0, 9.0, 3.0, 2.0, |
| 465 | ); |
| 466 | let p = Vector4F::new(3.0, 8.0, 4.0, 6.0); |
| 467 | let q = Vector4F::new(63.0, 97.0, 135.0, 117.0); |
| 468 | assert_eq!(a * p, q); |
| 469 | } |
| 470 | |
| 471 | #[test ] |
| 472 | fn test_inverse() { |
| 473 | // Random matrix. |
| 474 | let m = Transform4F::row_major( |
| 475 | 0.86277982, 0.15986552, 0.90739898, 0.60066808, 0.17386167, 0.016353, 0.8535783, |
| 476 | 0.12969608, 0.0946466, 0.43248631, 0.63480505, 0.08154603, 0.50305436, 0.48359687, |
| 477 | 0.51057162, 0.24812012, |
| 478 | ); |
| 479 | let p0 = Vector4F::new(0.95536648, 0.80633691, 0.16357357, 0.5477598); |
| 480 | let p1 = m * p0; |
| 481 | let m_inv = m.inverse(); |
| 482 | let m_inv_exp = Transform4F::row_major( |
| 483 | -2.47290136, |
| 484 | 3.48865688, |
| 485 | -6.12298336, |
| 486 | 6.17536696, |
| 487 | 0.00124033357, |
| 488 | -1.72561993, |
| 489 | 2.16876606, |
| 490 | 0.186227748, |
| 491 | -0.375021729, |
| 492 | 1.53883017, |
| 493 | -0.0558194403, |
| 494 | 0.121857058, |
| 495 | 5.78300323, |
| 496 | -6.87635769, |
| 497 | 8.30196620, |
| 498 | -9.10374060, |
| 499 | ); |
| 500 | assert!(m_inv.approx_eq(&m_inv_exp, 0.0001)); |
| 501 | let p2 = m_inv * p1; |
| 502 | assert!(p0.approx_eq(p2, 0.0001)); |
| 503 | } |
| 504 | } |
| 505 | |