engeom 0.2.9__tar.gz → 0.2.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. {engeom-0.2.9 → engeom-0.2.11}/Cargo.lock +2 -2
  2. {engeom-0.2.9 → engeom-0.2.11}/Cargo.toml +1 -1
  3. {engeom-0.2.9 → engeom-0.2.11}/PKG-INFO +1 -1
  4. {engeom-0.2.9 → engeom-0.2.11}/engeom/Cargo.lock +1 -1
  5. {engeom-0.2.9 → engeom-0.2.11}/engeom/Cargo.toml +1 -1
  6. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/surface_point.rs +27 -0
  7. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/curve3.rs +1 -0
  8. engeom-0.2.11/engeom/src/geom3/iso3.rs +375 -0
  9. engeom-0.2.11/engeom/src/geom3/mesh/collisions.rs +194 -0
  10. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh.rs +82 -3
  11. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/plane3.rs +11 -0
  12. engeom-0.2.11/engeom/src/geom3/xyzwpr.rs +141 -0
  13. engeom-0.2.11/engeom/src/geom3.rs +72 -0
  14. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/lib.rs +5 -0
  15. engeom-0.2.11/engeom/src/sensor.rs +156 -0
  16. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/_plot/matplotlib.py +62 -15
  17. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/_plot/pyvista.py +26 -5
  18. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/geom2.pyi +40 -0
  19. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/geom3.pyi +385 -0
  20. engeom-0.2.11/python/engeom/sensor/__init__.py +6 -0
  21. engeom-0.2.11/python/engeom/sensor.pyi +58 -0
  22. {engeom-0.2.9 → engeom-0.2.11}/src/geom2.rs +17 -0
  23. {engeom-0.2.9 → engeom-0.2.11}/src/geom3.rs +213 -6
  24. {engeom-0.2.9 → engeom-0.2.11}/src/lib.rs +14 -0
  25. {engeom-0.2.9 → engeom-0.2.11}/src/mesh.rs +139 -6
  26. {engeom-0.2.9 → engeom-0.2.11}/src/ray_casting.rs +0 -3
  27. engeom-0.2.11/src/sensor.rs +117 -0
  28. engeom-0.2.9/engeom/src/geom3.rs +0 -142
  29. {engeom-0.2.9 → engeom-0.2.11}/.github/workflows/CI.yml +0 -0
  30. {engeom-0.2.9 → engeom-0.2.11}/.gitignore +0 -0
  31. {engeom-0.2.9 → engeom-0.2.11}/.gitmodules +0 -0
  32. {engeom-0.2.9 → engeom-0.2.11}/README.md +0 -0
  33. {engeom-0.2.9 → engeom-0.2.11}/docs/airfoils/intro.md +0 -0
  34. {engeom-0.2.9 → engeom-0.2.11}/docs/api/airfoil.md +0 -0
  35. {engeom-0.2.9 → engeom-0.2.11}/docs/api/engeom.md +0 -0
  36. {engeom-0.2.9 → engeom-0.2.11}/docs/api/geom2.md +0 -0
  37. {engeom-0.2.9 → engeom-0.2.11}/docs/api/geom3.md +0 -0
  38. {engeom-0.2.9 → engeom-0.2.11}/docs/api/metrology.md +0 -0
  39. {engeom-0.2.9 → engeom-0.2.11}/docs/api/plot.md +0 -0
  40. {engeom-0.2.9 → engeom-0.2.11}/docs/bounding_volumes.md +0 -0
  41. {engeom-0.2.9 → engeom-0.2.11}/docs/curves.md +0 -0
  42. {engeom-0.2.9 → engeom-0.2.11}/docs/images/surface_point_meas.svg +0 -0
  43. {engeom-0.2.9 → engeom-0.2.11}/docs/index.md +0 -0
  44. {engeom-0.2.9 → engeom-0.2.11}/docs/isometries.md +0 -0
  45. {engeom-0.2.9 → engeom-0.2.11}/docs/meshes.md +0 -0
  46. {engeom-0.2.9 → engeom-0.2.11}/docs/metrology.md +0 -0
  47. {engeom-0.2.9 → engeom-0.2.11}/docs/numpy.md +0 -0
  48. {engeom-0.2.9 → engeom-0.2.11}/docs/planes_circles_lines.md +0 -0
  49. {engeom-0.2.9 → engeom-0.2.11}/docs/points_vectors.md +0 -0
  50. {engeom-0.2.9 → engeom-0.2.11}/docs/surf_points.md +0 -0
  51. {engeom-0.2.9 → engeom-0.2.11}/docs/svd_basis.md +0 -0
  52. {engeom-0.2.9 → engeom-0.2.11}/engeom/.gitignore +0 -0
  53. {engeom-0.2.9 → engeom-0.2.11}/engeom/README.md +0 -0
  54. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/airfoils/camber.md +0 -0
  55. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/airfoils/overview.md +0 -0
  56. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/common/angles.md +0 -0
  57. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/common/core_space.md +0 -0
  58. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/common/discrete_domain.md +0 -0
  59. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/common/images/surface_point_meas.svg +0 -0
  60. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/common/svd_basis.md +0 -0
  61. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/geom2/alignment.md +0 -0
  62. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/geom2/curve.md +0 -0
  63. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/geom2/point_collections.md +0 -0
  64. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/geom2/shapes.md +0 -0
  65. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/index.md +0 -0
  66. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/javascripts/mathjax.js +0 -0
  67. {engeom-0.2.9 → engeom-0.2.11}/engeom/docs/python_rust.md +0 -0
  68. {engeom-0.2.9 → engeom-0.2.11}/engeom/mkdocs.yml +0 -0
  69. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/airfoil/camber.rs +0 -0
  70. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/airfoil/edges.rs +0 -0
  71. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/airfoil/helpers.rs +0 -0
  72. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/airfoil/inscribed_circle.rs +0 -0
  73. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/airfoil/orientation.rs +0 -0
  74. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/airfoil.rs +0 -0
  75. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/align.rs +0 -0
  76. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/angles.rs +0 -0
  77. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/convert_2d_3d.rs +0 -0
  78. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/discrete_domain.rs +0 -0
  79. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/indices.rs +0 -0
  80. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/interval.rs +0 -0
  81. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/kd_tree.rs +0 -0
  82. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/points.rs +0 -0
  83. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/poisson_disk.rs +0 -0
  84. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/svd_basis.rs +0 -0
  85. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common/vec_f64.rs +0 -0
  86. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/common.rs +0 -0
  87. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/errors.rs +0 -0
  88. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/func1/common_functions.rs +0 -0
  89. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/func1/polynomial.rs +0 -0
  90. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/func1/series1.rs +0 -0
  91. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/func1.rs +0 -0
  92. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/aabb2.rs +0 -0
  93. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/align2/jacobian.rs +0 -0
  94. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/align2/points_to_curve.rs +0 -0
  95. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/align2/rc_params2.rs +0 -0
  96. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/align2.rs +0 -0
  97. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/angles2.rs +0 -0
  98. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/circle2.rs +0 -0
  99. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/curve2.rs +0 -0
  100. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/hull.rs +0 -0
  101. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/line2.rs +0 -0
  102. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2/polyline2.rs +0 -0
  103. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom2.rs +0 -0
  104. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/align3/jacobian.rs +0 -0
  105. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/align3/multi_param.rs +0 -0
  106. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/align3/points_to_mesh.rs +0 -0
  107. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/align3/rotations.rs +0 -0
  108. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/align3.rs +0 -0
  109. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/conformal.rs +0 -0
  110. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/edges.rs +0 -0
  111. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/faces.rs +0 -0
  112. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/filtering.rs +0 -0
  113. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/measurement.rs +0 -0
  114. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/outline.rs +0 -0
  115. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/patches.rs +0 -0
  116. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/queries.rs +0 -0
  117. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/sampling.rs +0 -0
  118. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/mesh/uv_mapping.rs +0 -0
  119. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/geom3/point_cloud.rs +0 -0
  120. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/io.rs +0 -0
  121. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/metrology/dimension.rs +0 -0
  122. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/metrology/line_profiles.rs +0 -0
  123. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/metrology/surface_deviation.rs +0 -0
  124. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/metrology/tolerance.rs +0 -0
  125. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/metrology/tolerance_map.rs +0 -0
  126. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/metrology.rs +0 -0
  127. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/raster3.rs +0 -0
  128. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/stats.rs +0 -0
  129. {engeom-0.2.9 → engeom-0.2.11}/engeom/src/utility.rs +0 -0
  130. {engeom-0.2.9 → engeom-0.2.11}/mkdocs.yml +0 -0
  131. {engeom-0.2.9 → engeom-0.2.11}/pyproject.toml +0 -0
  132. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/__init__.py +0 -0
  133. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/_plot/__init__.py +0 -0
  134. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/_plot/common.py +0 -0
  135. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/airfoil/__init__.py +0 -0
  136. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/airfoil.pyi +0 -0
  137. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/align/__init__.py +0 -0
  138. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/align.pyi +0 -0
  139. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/engeom.pyi +0 -0
  140. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/geom2/__init__.py +0 -0
  141. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/geom3/__init__.py +0 -0
  142. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/metrology/__init__.py +0 -0
  143. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/metrology.pyi +0 -0
  144. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/plot.py +0 -0
  145. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/raster3/__init__.py +0 -0
  146. {engeom-0.2.9 → engeom-0.2.11}/python/engeom/raster3.pyi +0 -0
  147. {engeom-0.2.9 → engeom-0.2.11}/python/tests/test_all.py +0 -0
  148. {engeom-0.2.9 → engeom-0.2.11}/python/tests/test_geom2_simple.py +0 -0
  149. {engeom-0.2.9 → engeom-0.2.11}/python/tests/test_geom3_simple.py +0 -0
  150. {engeom-0.2.9 → engeom-0.2.11}/src/airfoil.rs +0 -0
  151. {engeom-0.2.9 → engeom-0.2.11}/src/alignments.rs +0 -0
  152. {engeom-0.2.9 → engeom-0.2.11}/src/bounding.rs +0 -0
  153. {engeom-0.2.9 → engeom-0.2.11}/src/common.rs +0 -0
  154. {engeom-0.2.9 → engeom-0.2.11}/src/conversions.rs +0 -0
  155. {engeom-0.2.9 → engeom-0.2.11}/src/metrology.rs +0 -0
  156. {engeom-0.2.9 → engeom-0.2.11}/src/raster.rs +0 -0
  157. {engeom-0.2.9 → engeom-0.2.11}/src/svd_basis.rs +0 -0
@@ -231,7 +231,7 @@ dependencies = [
231
231
 
232
232
  [[package]]
233
233
  name = "engeom"
234
- version = "0.2.0"
234
+ version = "0.2.1"
235
235
  dependencies = [
236
236
  "faer",
237
237
  "itertools 0.14.0",
@@ -1025,7 +1025,7 @@ dependencies = [
1025
1025
 
1026
1026
  [[package]]
1027
1027
  name = "py-engeom"
1028
- version = "0.2.9"
1028
+ version = "0.2.11"
1029
1029
  dependencies = [
1030
1030
  "engeom",
1031
1031
  "numpy",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "py-engeom"
3
- version = "0.2.9"
3
+ version = "0.2.11"
4
4
  edition = "2021"
5
5
 
6
6
  # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engeom
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -358,7 +358,7 @@ dependencies = [
358
358
 
359
359
  [[package]]
360
360
  name = "engeom"
361
- version = "0.2.0"
361
+ version = "0.2.1"
362
362
  dependencies = [
363
363
  "approx",
364
364
  "criterion",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "engeom"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -80,6 +80,33 @@ impl<const D: usize> SurfacePoint<D> {
80
80
  let projection = self.projection(other);
81
81
  (projection - other).norm()
82
82
  }
83
+
84
+ /// Returns a new surface point shifted from the original surface point by the given distance
85
+ /// along the normal. This is useful for creating a new surface point that is a certain distance
86
+ /// away from the original surface point, in the direction of the normal.
87
+ ///
88
+ /// # Arguments
89
+ ///
90
+ /// * `shift`: the distance to offset the surface point along the normal
91
+ ///
92
+ /// returns: SurfacePoint<{ D }>
93
+ ///
94
+ /// # Examples
95
+ ///
96
+ /// ```
97
+ /// use engeom::{Point2, SurfacePoint2, Vector2};
98
+ /// use approx::assert_relative_eq;
99
+ ///
100
+ /// let sp = SurfacePoint2::new_normalize(Point2::new(0.0, 0.0), Vector2::new(0.0, 1.0));
101
+ ///
102
+ /// let shifted = sp.shift(2.0);
103
+ /// assert_relative_eq!(shifted.point, Point2::new(0.0, 2.0), epsilon = 1e-6);
104
+ /// assert_relative_eq!(shifted.normal.into_inner(), Vector2::new(0.0, 1.0), epsilon = 1e-6);
105
+ /// ```
106
+ pub fn shift(&self, offset: f64) -> Self {
107
+ let new_point = self.point + self.normal.as_ref() * offset;
108
+ Self::new(new_point, self.normal)
109
+ }
83
110
  }
84
111
 
85
112
  /// Created a vector of `SurfacePoint` instances from a vector of points and a vector of normals.
@@ -255,6 +255,7 @@ impl Curve3 {
255
255
  let new_points = ramer_douglas_peucker(self.line.vertices(), tol);
256
256
  Self::from_points(&new_points, tol).unwrap()
257
257
  }
258
+
258
259
  }
259
260
 
260
261
  fn resample_by_max_spacing(curve: &Curve3, max_spacing: f64) -> Curve3 {
@@ -0,0 +1,375 @@
1
+ //! This module has additional tools and functions for working with 3D isometries
2
+
3
+ use crate::{Iso3, Point3, Result, UnitVec3, Vector3};
4
+ use parry3d_f64::na::{try_convert, Matrix4, UnitQuaternion};
5
+ use parry3d_f64::na::{Matrix3, Translation3};
6
+
7
+ pub trait IsoExtensions3 {
8
+ fn flip_around_x(&self) -> Iso3;
9
+ fn flip_around_y(&self) -> Iso3;
10
+ fn flip_around_z(&self) -> Iso3;
11
+ fn try_from_array(array: &[f64; 16]) -> Result<Iso3>;
12
+
13
+ fn try_from_basis_xy(e0: &Vector3, e1: &Vector3, origin: Option<Point3>) -> Result<Iso3>;
14
+ fn try_from_basis_xz(e0: &Vector3, e2: &Vector3, origin: Option<Point3>) -> Result<Iso3>;
15
+ fn try_from_basis_yz(e1: &Vector3, e2: &Vector3, origin: Option<Point3>) -> Result<Iso3>;
16
+ fn try_from_basis_yx(e1: &Vector3, e0: &Vector3, origin: Option<Point3>) -> Result<Iso3>;
17
+ fn try_from_basis_zx(e2: &Vector3, e0: &Vector3, origin: Option<Point3>) -> Result<Iso3>;
18
+ fn try_from_basis_zy(e2: &Vector3, e1: &Vector3, origin: Option<Point3>) -> Result<Iso3>;
19
+
20
+ fn from_rx(angle: f64) -> Iso3;
21
+ fn from_ry(angle: f64) -> Iso3;
22
+ fn from_rz(angle: f64) -> Iso3;
23
+ }
24
+
25
+ impl IsoExtensions3 for Iso3 {
26
+ /// Rotate the isometry in place by 180 degrees around the x-axis. The location of the origin
27
+ /// is not changed, but the y and z directions are reversed.
28
+ fn flip_around_x(&self) -> Self {
29
+ let r = Iso3::rotation(Vector3::x() * std::f64::consts::PI);
30
+ self.translation * r * self.rotation
31
+ }
32
+
33
+ /// Rotate the isometry in place by 180 degrees around the y-axis. The location of the origin
34
+ /// is not changed, but the x and z directions are reversed.
35
+ fn flip_around_y(&self) -> Self {
36
+ let r = Iso3::rotation(Vector3::y() * std::f64::consts::PI);
37
+ self.translation * r * self.rotation
38
+ }
39
+
40
+ /// Rotate the isometry in place by 180 degrees around the z-axis. The location of the origin
41
+ /// is not changed, but the x and y directions are reversed.
42
+ fn flip_around_z(&self) -> Self {
43
+ let r = Iso3::rotation(Vector3::z() * std::f64::consts::PI);
44
+ self.translation * r * self.rotation
45
+ }
46
+
47
+ /// Try to convert a 16 element array into an Iso3. The array is expected to be in row-major
48
+ /// order.
49
+ fn try_from_array(array: &[f64; 16]) -> Result<Self> {
50
+ try_convert(Matrix4::from_row_slice(array)).ok_or("Could not convert to Iso3".into())
51
+ }
52
+
53
+ /// Try to create an isometry from two basis vectors and an optional origin. The primary basis
54
+ /// vector will become the x-axis in the isometry, the secondary basis vector will be projected
55
+ /// onto the primary and the remaining component will be the y-axis. The final axis will be
56
+ /// computed by cross product for a right-handed coordinate system.
57
+ ///
58
+ /// The isometry produced by this method will move a point in the basis coordinate system to
59
+ /// where it would be located in the world coordinate system.
60
+ ///
61
+ /// If you want to take features in the world coordinate system and move them into the basis
62
+ /// coordinate system, you need to use the inverse of the isometry.
63
+ ///
64
+ /// # Arguments
65
+ ///
66
+ /// * `e0`: A vector in the world coordinate system that will become the x-axis in the basis
67
+ /// coordinate system, will be normalized to unit length automatically.
68
+ /// * `e1`: A vector in the world coordinate system whose component linearly independent of `e0`
69
+ /// will become the y-axis in the basis coordinate system, will be normalized to unit length.
70
+ /// * `origin`: An optional point in the world coordinate system that will be the origin of the
71
+ /// basis coordinate system. If not provided, the origin of the basis coordinate system will
72
+ /// be coincident with the origin of the world coordinate system.
73
+ ///
74
+ /// returns: Result<Isometry<f64, Unit<Quaternion<f64>>, 3>, Box<dyn Error, Global>>
75
+ fn try_from_basis_xy(e0: &Vector3, e1: &Vector3, origin: Option<Point3>) -> Result<Iso3> {
76
+ let e0 = e0.try_normalize(1e-10).ok_or("Could not normalize e0")?;
77
+ let e2 = e0.cross(e1).try_normalize(1e-10).ok_or("Could not normalize e2")?;
78
+ let e1 = e2.cross(&e0).try_normalize(1e-10).ok_or("Could not normalize e1")?;
79
+
80
+ from_bases(e0, e1, e2, origin)
81
+ }
82
+
83
+ /// Try to create an isometry from two basis vectors and an optional origin. The primary basis
84
+ /// vector will become the x-axis in the isometry, the secondary basis vector will be projected
85
+ /// onto the primary and the remaining component will be the z-axis. The final axis will be
86
+ /// computed by cross product for a right-handed coordinate system.
87
+ ///
88
+ /// The isometry produced by this method will move a point in the basis coordinate system to
89
+ /// where it would be located in the world coordinate system.
90
+ ///
91
+ /// If you want to take features in the world coordinate system and move them into the basis
92
+ /// coordinate system, you need to use the inverse of the isometry.
93
+ ///
94
+ /// # Arguments
95
+ ///
96
+ /// * `e0`: A vector in the world coordinate system that will become the x-axis in the basis
97
+ /// coordinate system, will be normalized to unit length automatically.
98
+ /// * `e2`: A vector in the world coordinate system whose component linearly independent of `e0`
99
+ /// will become the z-axis in the basis coordinate system, will be normalized to unit length.
100
+ /// * `origin`: An optional point in the world coordinate system that will be the origin of the
101
+ /// basis coordinate system. If not provided, the origin of the basis coordinate system will
102
+ /// be coincident with the origin of the world coordinate system.
103
+ ///
104
+ /// returns: Result<Isometry<f64, Unit<Quaternion<f64>>, 3>, Box<dyn Error, Global>>
105
+ fn try_from_basis_xz(e0: &Vector3, e2: &Vector3, origin: Option<Point3>) -> Result<Iso3> {
106
+ let e0 = e0.try_normalize(1e-10).ok_or("Could not normalize e0")?;
107
+ let e1 = e2.cross(&e0).try_normalize(1e-10).ok_or("Could not normalize e1")?;
108
+ let e2 = e0.cross(&e1).try_normalize(1e-10).ok_or("Could not normalize e2")?;
109
+ from_bases(e0, e1, e2, origin)
110
+ }
111
+
112
+ /// Try to create an isometry from two basis vectors and an optional origin. The primary basis
113
+ /// vector will become the y-axis in the isometry, the secondary basis vector will be projected
114
+ /// onto the primary and the remaining component will be the z-axis. The final axis will be
115
+ /// computed by cross product for a right-handed coordinate system.
116
+ ///
117
+ /// The isometry produced by this method will move a point in the basis coordinate system to
118
+ /// where it would be located in the world coordinate system.
119
+ ///
120
+ /// If you want to take features in the world coordinate system and move them into the basis
121
+ /// coordinate system, you need to use the inverse of the isometry.
122
+ ///
123
+ /// # Arguments
124
+ ///
125
+ /// * `e1`: A vector in the world coordinate system that will become the y-axis in the basis
126
+ /// coordinate system, will be normalized to unit length automatically.
127
+ /// * `e2`: A vector in the world coordinate system whose component linearly independent of `e1`
128
+ /// will become the z-axis in the basis coordinate system, will be normalized to unit length.
129
+ /// * `origin`: An optional point in the world coordinate system that will be the origin of the
130
+ /// basis coordinate system. If not provided, the origin of the basis coordinate system will
131
+ /// be coincident with the origin of the world coordinate system.
132
+ ///
133
+ /// returns: Result<Isometry<f64, Unit<Quaternion<f64>>, 3>, Box<dyn Error, Global>>
134
+ fn try_from_basis_yz(e1: &Vector3, e2: &Vector3, origin: Option<Point3>) -> Result<Iso3> {
135
+ let e1 = e1.try_normalize(1e-10).ok_or("Could not normalize e1")?;
136
+ let e0 = e1.cross(&e2).try_normalize(1e-10).ok_or("Could not normalize e0")?;
137
+ let e2 = e0.cross(&e1).try_normalize(1e-10).ok_or("Could not normalize e2")?;
138
+ from_bases(e0, e1, e2, origin)
139
+ }
140
+
141
+ /// Try to create an isometry from two basis vectors and an optional origin. The primary basis
142
+ /// vector will become the y-axis in the isometry, the secondary basis vector will be projected
143
+ /// onto the primary and the remaining component will be the x-axis. The final axis will be
144
+ /// computed by cross product for a right-handed coordinate system.
145
+ ///
146
+ /// The isometry produced by this method will move a point in the basis coordinate system to
147
+ /// where it would be located in the world coordinate system.
148
+ ///
149
+ /// If you want to take features in the world coordinate system and move them into the basis
150
+ /// coordinate system, you need to use the inverse of the isometry.
151
+ ///
152
+ /// # Arguments
153
+ ///
154
+ /// * `e1`: A vector in the world coordinate system that will become the y-axis in the basis
155
+ /// coordinate system, will be normalized to unit length automatically.
156
+ /// * `e0`: A vector in the world coordinate system whose component linearly independent of `e1`
157
+ /// will become the x-axis in the basis coordinate system, will be normalized to unit length.
158
+ /// * `origin`: An optional point in the world coordinate system that will be the origin of the
159
+ /// basis coordinate system. If not provided, the origin of the basis coordinate system will
160
+ /// be coincident with the origin of the world coordinate system.
161
+ ///
162
+ /// returns: Result<Isometry<f64, Unit<Quaternion<f64>>, 3>, Box<dyn Error, Global>>
163
+ fn try_from_basis_yx(e1: &Vector3, e0: &Vector3, origin: Option<Point3>) -> Result<Iso3> {
164
+ let e1 = e1.try_normalize(1e-10).ok_or("Could not normalize e1")?;
165
+ let e2 = e0.cross(&e1).try_normalize(1e-10).ok_or("Could not normalize e2")?;
166
+ let e0 = e1.cross(&e2).try_normalize(1e-10).ok_or("Could not normalize e0")?;
167
+ from_bases(e0, e1, e2, origin)
168
+ }
169
+
170
+ /// Try to create an isometry from two basis vectors and an optional origin. The primary basis
171
+ /// vector will become the z-axis in the isometry, the secondary basis vector will be projected
172
+ /// onto the primary and the remaining component will be the x-axis. The final axis will be
173
+ /// computed by cross product for a right-handed coordinate system.
174
+ ///
175
+ /// The isometry produced by this method will move a point in the basis coordinate system to
176
+ /// where it would be located in the world coordinate system.
177
+ ///
178
+ /// If you want to take features in the world coordinate system and move them into the basis
179
+ /// coordinate system, you need to use the inverse of the isometry.
180
+ ///
181
+ /// # Arguments
182
+ ///
183
+ /// * `e2`: A vector in the world coordinate system that will become the z-axis in the basis
184
+ /// coordinate system, will be normalized to unit length automatically.
185
+ /// * `e0`: A vector in the world coordinate system whose component linearly independent of `e2`
186
+ /// will become the x-axis in the basis coordinate system, will be normalized to unit length.
187
+ /// * `origin`: An optional point in the world coordinate system that will be the origin of the
188
+ /// basis coordinate system. If not provided, the origin of the basis coordinate system will
189
+ /// be coincident with the origin of the world coordinate system.
190
+ ///
191
+ /// returns: Result<Isometry<f64, Unit<Quaternion<f64>>, 3>, Box<dyn Error, Global>>
192
+ fn try_from_basis_zx(e2: &Vector3, e0: &Vector3, origin: Option<Point3>) -> Result<Iso3> {
193
+ let e2 = e2.try_normalize(1e-10).ok_or("Could not normalize e2")?;
194
+ let e1 = e2.cross(&e0).try_normalize(1e-10).ok_or("Could not normalize e2")?;
195
+ let e0 = e1.cross(&e2).try_normalize(1e-10).ok_or("Could not normalize e0")?;
196
+ from_bases(e0, e1, e2, origin)
197
+ }
198
+
199
+ /// Try to create an isometry from two basis vectors and an optional origin. The primary basis
200
+ /// vector will become the z-axis in the isometry, the secondary basis vector will be projected
201
+ /// onto the primary and the remaining component will be the y-axis. The final axis will be
202
+ /// computed by cross product for a right-handed coordinate system.
203
+ ///
204
+ /// The isometry produced by this method will move a point in the basis coordinate system to
205
+ /// where it would be located in the world coordinate system.
206
+ ///
207
+ /// If you want to take features in the world coordinate system and move them into the basis
208
+ /// coordinate system, you need to use the inverse of the isometry.
209
+ ///
210
+ /// # Arguments
211
+ ///
212
+ /// * `e2`: A vector in the world coordinate system that will become the z-axis in the basis
213
+ /// coordinate system, will be normalized to unit length automatically.
214
+ /// * `e1`: A vector in the world coordinate system whose component linearly independent of `e2`
215
+ /// will become the y-axis in the basis coordinate system, will be normalized to unit length.
216
+ /// * `origin`: An optional point in the world coordinate system that will be the origin of the
217
+ /// basis coordinate system. If not provided, the origin of the basis coordinate system will
218
+ /// be coincident with the origin of the world coordinate system.
219
+ ///
220
+ /// returns: Result<Isometry<f64, Unit<Quaternion<f64>>, 3>, Box<dyn Error, Global>>
221
+ fn try_from_basis_zy(e2: &Vector3, e1: &Vector3, origin: Option<Point3>) -> Result<Iso3> {
222
+ let e2 = e2.try_normalize(1e-10).ok_or("Could not normalize e2")?;
223
+ let e0 = e1.cross(&e2).try_normalize(1e-10).ok_or("Could not normalize e0")?;
224
+ let e1 = e2.cross(&e0).try_normalize(1e-10).ok_or("Could not normalize e2")?;
225
+ from_bases(e0, e1, e2, origin)
226
+ }
227
+
228
+ fn from_rx(angle: f64) -> Iso3 {
229
+ Iso3::rotation(Vector3::x() * angle)
230
+ }
231
+
232
+ fn from_ry(angle: f64) -> Iso3 {
233
+ Iso3::rotation(Vector3::y() * angle)
234
+ }
235
+
236
+ fn from_rz(angle: f64) -> Iso3 {
237
+ Iso3::rotation(Vector3::z() * angle)
238
+ }
239
+ }
240
+
241
+ fn from_bases(e0: Vector3, e1: Vector3, e2: Vector3, origin: Option<Point3>) -> Result<Iso3> {
242
+ let rot_m = Matrix3::from_columns(&[e0, e1, e2]);
243
+ let r = UnitQuaternion::from_matrix(&rot_m);
244
+ let t = if let Some(o) = origin {
245
+ Translation3::from(o.coords)
246
+ } else {
247
+ Translation3::identity()
248
+ };
249
+
250
+ Ok(Iso3::from_parts(t, r))
251
+ }
252
+
253
+ #[cfg(test)]
254
+ mod tests {
255
+ use std::f64::consts::PI;
256
+ use super::*;
257
+ use crate::Point3;
258
+ use approx::assert_relative_eq;
259
+
260
+ struct BasisCheck {
261
+ o: Point3,
262
+ e0: Vector3,
263
+ e1: Vector3,
264
+ e2: Vector3,
265
+ fwd: Iso3,
266
+ }
267
+
268
+ impl BasisCheck {
269
+ fn new() -> Self {
270
+ let o = Point3::new(1.0, 2.0, 3.0);
271
+ let angle = UnitVec3::new_normalize(Vector3::new(1.0, 1.0, 1.0));
272
+ let fwd = Iso3::from_parts(
273
+ Translation3::from(o.coords),
274
+ UnitQuaternion::new(PI / 4.0 * angle.into_inner()),
275
+ );
276
+ let e0 = fwd * Vector3::x();
277
+ let e1 = fwd * Vector3::y();
278
+ let e2 = fwd * Vector3::z();
279
+
280
+ Self { o, e0, e1, e2, fwd }
281
+ }
282
+ }
283
+
284
+ #[test]
285
+ fn iso3_try_from_basis_xy() -> Result<()> {
286
+ let check = BasisCheck::new();
287
+ let iso = Iso3::try_from_basis_xy(&check.e0, &check.e1, Some(check.o))?;
288
+ assert_relative_eq!(iso, check.fwd, epsilon = 1e-6);
289
+ Ok(())
290
+ }
291
+
292
+ #[test]
293
+ fn iso3_try_from_basis_xz() -> Result<()> {
294
+ let check = BasisCheck::new();
295
+ let iso = Iso3::try_from_basis_xz(&check.e0, &check.e2, Some(check.o))?;
296
+ assert_relative_eq!(iso, check.fwd, epsilon = 1e-6);
297
+ Ok(())
298
+ }
299
+
300
+ #[test]
301
+ fn iso3_try_from_basis_yx() -> Result<()> {
302
+ let check = BasisCheck::new();
303
+ let iso = Iso3::try_from_basis_yx(&check.e1, &check.e0, Some(check.o))?;
304
+ assert_relative_eq!(iso, check.fwd, epsilon = 1e-6);
305
+ Ok(())
306
+ }
307
+
308
+ #[test]
309
+ fn iso3_try_from_basis_yz() -> Result<()> {
310
+ let check = BasisCheck::new();
311
+ let iso = Iso3::try_from_basis_yz(&check.e1, &check.e2, Some(check.o))?;
312
+ assert_relative_eq!(iso, check.fwd, epsilon = 1e-6);
313
+ Ok(())
314
+ }
315
+
316
+ #[test]
317
+ fn iso3_try_from_basis_zx() -> Result<()> {
318
+ let check = BasisCheck::new();
319
+ let iso = Iso3::try_from_basis_zx(&check.e2, &check.e0, Some(check.o))?;
320
+ assert_relative_eq!(iso, check.fwd, epsilon = 1e-6);
321
+ Ok(())
322
+ }
323
+
324
+ #[test]
325
+ fn iso3_try_from_basis_zy() -> Result<()> {
326
+ let check = BasisCheck::new();
327
+ let iso = Iso3::try_from_basis_zy(&check.e2, &check.e1, Some(check.o))?;
328
+ assert_relative_eq!(iso, check.fwd, epsilon = 1e-6);
329
+ Ok(())
330
+ }
331
+
332
+ #[test]
333
+ fn iso3_try_from_basis_xy_manual() {
334
+ let o = Point3::new(1.0, 2.0, 3.0);
335
+ let e0 = UnitVec3::new_normalize(Vector3::new(1.0, 1.0, 0.0));
336
+ let e1 = Vector3::new(0.0, 1.0, 1.0);
337
+
338
+ let iso = Iso3::try_from_basis_xy(&e0.into_inner(), &e1, Some(o)).unwrap();
339
+
340
+ assert_relative_eq!(iso * Point3::origin(), o, epsilon = 1e-6);
341
+ assert_relative_eq!(
342
+ iso * Point3::new(1.0, 0.0, 0.0),
343
+ o + e0.into_inner() * 1.0,
344
+ epsilon = 1e-6
345
+ );
346
+ }
347
+
348
+ #[test]
349
+ fn iso3_try_from_array_simple() {
350
+ let array = [
351
+ 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 2.0, 0.0, 0.0, 1.0, 3.0, 0.0, 0.0, 0.0, 1.0,
352
+ ];
353
+ let iso = Iso3::try_from_array(&array).unwrap();
354
+ let m = iso.to_matrix();
355
+ let expected = Matrix4::new(
356
+ 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 2.0, 0.0, 0.0, 1.0, 3.0, 0.0, 0.0, 0.0, 1.0,
357
+ );
358
+ assert_relative_eq!(m, expected);
359
+ }
360
+
361
+ #[test]
362
+ fn iso3_flip_x() {
363
+ let iso = Iso3::new(Vector3::new(1.0, 2.0, 3.0), Vector3::new(0.0, 0.0, 0.0));
364
+ let flipped = iso.flip_around_x();
365
+
366
+ let p = Point3::new(0.0, 0.0, 0.0);
367
+ assert_relative_eq!(flipped * p, Point3::new(1.0, 2.0, 3.0));
368
+
369
+ let p1 = Point3::new(1.0, 0.0, 0.0);
370
+ assert_relative_eq!(flipped * p1, Point3::new(2.0, 2.0, 3.0));
371
+
372
+ let p2 = Point3::new(0.0, 1.0, 0.0);
373
+ assert_relative_eq!(flipped * p2, Point3::new(1.0, 1.0, 3.0));
374
+ }
375
+ }
@@ -0,0 +1,194 @@
1
+ //! Mesh collision detection and distance checks
2
+
3
+ use std::cmp::PartialEq;
4
+ use crate::{Iso3, Mesh, Result};
5
+ use std::collections::{HashMap, HashSet};
6
+ use parry3d_f64::query::intersection_test;
7
+
8
+ #[derive(Debug, Clone, PartialEq)]
9
+ enum MeshType {
10
+ Stationary,
11
+ Moving,
12
+ }
13
+
14
+ struct MeshItem {
15
+ mesh: Mesh,
16
+ mesh_type: MeshType,
17
+ }
18
+
19
+ pub struct MeshCollisionSet {
20
+ meshes: HashMap<usize, MeshItem>,
21
+ exceptions: HashSet<(usize, usize)>,
22
+ }
23
+
24
+ impl MeshCollisionSet {
25
+ pub fn new() -> Self {
26
+ Self {
27
+ meshes: HashMap::new(),
28
+ exceptions: HashSet::new(),
29
+ }
30
+ }
31
+
32
+ pub fn add_exception(&mut self, id1: usize, id2: usize) {
33
+ let lower = id1.min(id2);
34
+ let upper = id1.max(id2);
35
+ self.exceptions.insert((lower, upper));
36
+ }
37
+
38
+ fn skip_collision(&self, id1: usize, id2: usize) -> bool {
39
+ let lower = id1.min(id2);
40
+ let upper = id1.max(id2);
41
+ self.exceptions.contains(&(lower, upper))
42
+ }
43
+
44
+ fn add_mesh(&mut self, mesh: Mesh, mesh_type: MeshType) -> usize {
45
+ let id = self.meshes.len();
46
+ self.meshes.insert(id, MeshItem { mesh, mesh_type });
47
+
48
+ id
49
+ }
50
+
51
+ pub fn add_stationary(&mut self, mesh: Mesh) -> usize {
52
+ self.add_mesh(mesh, MeshType::Stationary)
53
+ }
54
+
55
+ pub fn add_moving(&mut self, mesh: Mesh) -> usize {
56
+ self.add_mesh(mesh, MeshType::Moving)
57
+ }
58
+
59
+ /// This function will check for all collisions between the meshes in the set, according to the
60
+ /// following rules:
61
+ ///
62
+ /// - Moving meshes will be checked against all meshes that don't contain an exception,
63
+ /// including both stationary and other moving meshes
64
+ /// - Stationary meshes will not be checked against any other meshes, and so a collision will
65
+ /// only be reported if it is with a stationary mesh
66
+ ///
67
+ /// # Arguments
68
+ ///
69
+ /// * `transforms`: transforms for the moving meshes
70
+ /// * `stop_at_first`: If true, the function will stop at the first collision found for each
71
+ /// moving mesh. If false, it will check all collisions.
72
+ ///
73
+ /// returns: Vec<(usize, usize), Global>
74
+ ///
75
+ /// # Examples
76
+ ///
77
+ /// ```
78
+ ///
79
+ /// ```
80
+ pub fn check_all(
81
+ &self,
82
+ transforms: &[(usize, Iso3)],
83
+ stop_at_first: bool,
84
+ ) -> Result<Vec<(usize, usize)>> {
85
+ // Create the fast isometry lookup:
86
+ let lookups = self.quick_lookups(transforms)?;
87
+
88
+ // We'll iterate through all the moving meshes (this can be parallelized later).
89
+ // For each moving mesh, we'll iterate through all stationary meshes and then all moving
90
+ // meshes. Whether a collision check occurs depends on the following:
91
+ // - Is the current mesh id lower than the other mesh id?
92
+ // - Is there an exception for the current pair of meshes?
93
+ let mut pairs = Vec::new();
94
+
95
+ for (&id1, mesh1) in self.meshes.iter() {
96
+ if mesh1.mesh_type == MeshType::Stationary {
97
+ continue;
98
+ }
99
+
100
+ let iso1 = &lookups[id1];
101
+
102
+ for (&id2, mesh2) in self.meshes.iter() {
103
+ if mesh2.mesh_type == MeshType::Moving && id1 >= id2 {
104
+ continue;
105
+ }
106
+
107
+ if self.skip_collision(id1, id2) {
108
+ continue;
109
+ }
110
+
111
+ let iso2 = &lookups[id2];
112
+
113
+ // Check for collision
114
+ if let Ok(check) = intersection_test(iso1, mesh1.mesh.tri_mesh(), iso2, mesh2.mesh.tri_mesh()) {
115
+ if check {
116
+ pairs.push((id1, id2));
117
+ if stop_at_first {
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ Ok(pairs)
126
+ }
127
+
128
+ fn quick_lookups(&self, transforms: &[(usize, Iso3)]) -> Result<Vec<Iso3>> {
129
+ let mut lookups = vec![Iso3::identity(); self.meshes.len()];
130
+ for &(id, iso) in transforms.iter() {
131
+ if id >= self.meshes.len() {
132
+ return Err(format!("Transform id {} out of bounds", id).into());
133
+ }
134
+
135
+ lookups[id] = iso.clone();
136
+ }
137
+
138
+ Ok(lookups)
139
+ }
140
+ }
141
+
142
+
143
+ #[cfg(test)]
144
+ mod tests {
145
+ use super::*;
146
+ use crate::geom3::Mesh;
147
+ use crate::Iso3;
148
+
149
+ #[test]
150
+ fn collision_set() {
151
+ let mut set = MeshCollisionSet::new();
152
+ let mesh1 = Mesh::create_box(1.0, 1.0, 1.0, true);
153
+ let mesh2 = Mesh::create_box(1.0, 1.0, 1.0, true);
154
+
155
+ let id1 = set.add_stationary(mesh1);
156
+ let id2 = set.add_moving(mesh2);
157
+
158
+ let transforms = vec![(id2, Iso3::translation(0.5, 0.5, 0.5))];
159
+
160
+ let pairs = set.check_all(&transforms, false).unwrap();
161
+ assert_eq!(pairs.len(), 1);
162
+ }
163
+
164
+ #[test]
165
+ fn collision_set_exception_skips() {
166
+ let mut set = MeshCollisionSet::new();
167
+ let mesh1 = Mesh::create_box(1.0, 1.0, 1.0, true);
168
+ let mesh2 = Mesh::create_box(1.0, 1.0, 1.0, true);
169
+
170
+ let id1 = set.add_stationary(mesh1);
171
+ let id2 = set.add_moving(mesh2);
172
+ set.add_exception(id2, id1);
173
+
174
+ let transforms = vec![(id2, Iso3::translation(0.5, 0.5, 0.5))];
175
+
176
+ let pairs = set.check_all(&transforms, false).unwrap();
177
+ assert_eq!(pairs.len(), 0);
178
+ }
179
+
180
+ #[test]
181
+ fn collision_set_misses() {
182
+ let mut set = MeshCollisionSet::new();
183
+ let mesh1 = Mesh::create_box(1.0, 1.0, 1.0, true);
184
+ let mesh2 = Mesh::create_box(1.0, 1.0, 1.0, true);
185
+
186
+ let id1 = set.add_stationary(mesh1);
187
+ let id2 = set.add_moving(mesh2);
188
+
189
+ let transforms = vec![(id2, Iso3::translation(2.5, 0.5, 0.5))];
190
+
191
+ let pairs = set.check_all(&transforms, false).unwrap();
192
+ assert_eq!(pairs.len(), 0);
193
+ }
194
+ }