parry3d/shape/polyline.rs
1use crate::bounding_volume::Aabb;
2use crate::math::{Isometry, Point, Real, Vector};
3use crate::partitioning::{Bvh, BvhBuildStrategy};
4use crate::query::{PointProjection, PointQueryWithLocation};
5use crate::shape::composite_shape::CompositeShape;
6use crate::shape::{FeatureId, Segment, SegmentPointLocation, Shape, TypedCompositeShape};
7#[cfg(feature = "alloc")]
8use alloc::vec::Vec;
9
10use crate::query::details::NormalConstraints;
11
12#[derive(Clone, Debug)]
13#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
14#[cfg_attr(
15 feature = "rkyv",
16 derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize),
17 archive(check_bytes)
18)]
19/// A polyline.
20pub struct Polyline {
21 bvh: Bvh,
22 vertices: Vec<Point<Real>>,
23 indices: Vec<[u32; 2]>,
24}
25
26impl Polyline {
27 /// Creates a new polyline from a vertex buffer and an index buffer.
28 pub fn new(vertices: Vec<Point<Real>>, indices: Option<Vec<[u32; 2]>>) -> Self {
29 let indices =
30 indices.unwrap_or_else(|| (0..vertices.len() as u32 - 1).map(|i| [i, i + 1]).collect());
31 let leaves = indices.iter().enumerate().map(|(i, idx)| {
32 let aabb =
33 Segment::new(vertices[idx[0] as usize], vertices[idx[1] as usize]).local_aabb();
34 (i, aabb)
35 });
36
37 // NOTE: we apply no dilation factor because we won't
38 // update this tree dynamically.
39 let bvh = Bvh::from_iter(BvhBuildStrategy::Binned, leaves);
40
41 Self {
42 bvh,
43 vertices,
44 indices,
45 }
46 }
47
48 /// Compute the axis-aligned bounding box of this polyline.
49 pub fn aabb(&self, pos: &Isometry<Real>) -> Aabb {
50 self.bvh.root_aabb().transform_by(pos)
51 }
52
53 /// Gets the local axis-aligned bounding box of this polyline.
54 pub fn local_aabb(&self) -> Aabb {
55 self.bvh.root_aabb()
56 }
57
58 /// The BVH acceleration structure for this polyline.
59 pub fn bvh(&self) -> &Bvh {
60 &self.bvh
61 }
62
63 /// The number of segments forming this polyline.
64 pub fn num_segments(&self) -> usize {
65 self.indices.len()
66 }
67
68 /// An iterator through all the segments of this mesh.
69 pub fn segments(&self) -> impl ExactSizeIterator<Item = Segment> + '_ {
70 self.indices.iter().map(move |ids| {
71 Segment::new(
72 self.vertices[ids[0] as usize],
73 self.vertices[ids[1] as usize],
74 )
75 })
76 }
77
78 /// Get the `i`-th segment of this mesh.
79 pub fn segment(&self, i: u32) -> Segment {
80 let idx = self.indices[i as usize];
81 Segment::new(
82 self.vertices[idx[0] as usize],
83 self.vertices[idx[1] as usize],
84 )
85 }
86
87 /// Transforms the feature-id of a segment to the feature-id of this polyline.
88 pub fn segment_feature_to_polyline_feature(
89 &self,
90 segment: u32,
91 _feature: FeatureId,
92 ) -> FeatureId {
93 // TODO: return a vertex feature when it makes sense.
94 #[cfg(feature = "dim2")]
95 return FeatureId::Face(segment);
96 #[cfg(feature = "dim3")]
97 return FeatureId::Edge(segment);
98 }
99
100 /// The vertex buffer of this mesh.
101 pub fn vertices(&self) -> &[Point<Real>] {
102 &self.vertices[..]
103 }
104
105 /// The index buffer of this mesh.
106 pub fn indices(&self) -> &[[u32; 2]] {
107 &self.indices
108 }
109
110 /// A flat view of the index buffer of this mesh.
111 pub fn flat_indices(&self) -> &[u32] {
112 unsafe {
113 let len = self.indices.len() * 2;
114 let data = self.indices.as_ptr() as *const u32;
115 core::slice::from_raw_parts(data, len)
116 }
117 }
118
119 /// Computes a scaled version of this polyline.
120 pub fn scaled(mut self, scale: &Vector<Real>) -> Self {
121 self.vertices
122 .iter_mut()
123 .for_each(|pt| pt.coords.component_mul_assign(scale));
124 let mut bvh = self.bvh.clone();
125 bvh.scale(scale);
126 Self {
127 bvh,
128 vertices: self.vertices,
129 indices: self.indices,
130 }
131 }
132
133 /// Reverse the orientation of this polyline by swapping the indices of all
134 /// its segments and reverting its index buffer.
135 pub fn reverse(&mut self) {
136 for idx in &mut self.indices {
137 idx.swap(0, 1);
138 }
139
140 self.indices.reverse();
141
142 // Rebuild the bvh since the segment indices no longer map to the correct element.
143 // TODO PERF: should the Bvh have a function for efficient leaf index remapping?
144 // Probably not worth it unless this function starts showing up as a
145 // bottleneck for someone.
146 let leaves = self.segments().map(|seg| seg.local_aabb()).enumerate();
147 let bvh = Bvh::from_iter(BvhBuildStrategy::Binned, leaves);
148 self.bvh = bvh;
149 }
150
151 /// Extracts the connected components of this polyline, consuming `self`.
152 ///
153 /// This method is currently quite restrictive on the kind of allowed input. The polyline
154 /// represented by `self` must already have an index buffer sorted such that:
155 /// - Each connected component appears in the index buffer one after the other, i.e., a
156 /// connected component of this polyline must be a contiguous range of this polyline’s
157 /// index buffer.
158 /// - Each connected component is closed, i.e., each range of this polyline index buffer
159 /// `self.indices[i_start..=i_end]` forming a complete connected component, we must have
160 /// `self.indices[i_start][0] == self.indices[i_end][1]`.
161 /// - The indices for each component must already be in order, i.e., if the segments
162 /// `self.indices[i]` and `self.indices[i + 1]` are part of the same connected component then
163 /// we must have `self.indices[i][1] == self.indices[i + 1][0]`.
164 ///
165 /// # Output
166 /// Returns the set of polylines. If the inputs fulfill the constraints mentioned above, each
167 /// polyline will be a closed loop with consistent edge orientations, i.e., for all indices `i`,
168 /// we have `polyline.indices[i][1] == polyline.indices[i + 1][0]`.
169 ///
170 /// The orientation of each closed loop (clockwise or counterclockwise) are identical to their
171 /// original orientation in `self`.
172 pub fn extract_connected_components(&self) -> Vec<Polyline> {
173 let vertices = self.vertices();
174 let indices = self.indices();
175
176 if indices.is_empty() {
177 // Polyline is empty, return empty Vec
178 Vec::new()
179 } else {
180 let mut components = Vec::new();
181
182 let mut start_i = 0; // Start position of component
183 let mut start_node = indices[0][0]; // Start vertex index of component
184
185 let mut component_vertices = Vec::new();
186 let mut component_indices: Vec<[u32; 2]> = Vec::new();
187
188 // Iterate over indices, building polylines as we go
189 for (i, idx) in indices.iter().enumerate() {
190 component_vertices.push(vertices[idx[0] as usize]);
191
192 if idx[1] != start_node {
193 // Keep scanning and adding data
194 component_indices.push([(i - start_i) as u32, (i - start_i + 1) as u32]);
195 } else {
196 // Start node reached: build polyline and start next component
197 component_indices.push([(i - start_i) as u32, 0]);
198 components.push(Polyline::new(
199 core::mem::take(&mut component_vertices),
200 Some(core::mem::take(&mut component_indices)),
201 ));
202
203 if i + 1 < indices.len() {
204 // More components to find
205 start_node = indices[i + 1][0];
206 start_i = i + 1;
207 }
208 }
209 }
210
211 components
212 }
213 }
214
215 /// Perform a point projection assuming a solid interior based on a counter-clock-wise orientation.
216 ///
217 /// This is similar to `self.project_local_point_and_get_location` except that the resulting
218 /// `PointProjection::is_inside` will be set to true if the point is inside of the area delimited
219 /// by this polyline, assuming that:
220 /// - This polyline isn’t self-crossing.
221 /// - This polyline is closed with `self.indices[i][1] == self.indices[(i + 1) % num_indices][0]` where
222 /// `num_indices == self.indices.len()`.
223 /// - This polyline is oriented counter-clockwise.
224 /// - In 3D, the polyline is assumed to be fully coplanar, on a plane with normal given by
225 /// `axis`.
226 ///
227 /// These properties are not checked.
228 pub fn project_local_point_assuming_solid_interior_ccw(
229 &self,
230 point: Point<Real>,
231 #[cfg(feature = "dim3")] axis: u8,
232 ) -> (PointProjection, (u32, SegmentPointLocation)) {
233 let mut proj = self.project_local_point_and_get_location(&point, false);
234 let segment1 = self.segment((proj.1).0);
235
236 #[cfg(feature = "dim2")]
237 let normal1 = segment1.normal();
238 #[cfg(feature = "dim3")]
239 let normal1 = segment1.planar_normal(axis);
240
241 if let Some(normal1) = normal1 {
242 proj.0.is_inside = match proj.1 .1 {
243 SegmentPointLocation::OnVertex(i) => {
244 let dir2 = if i == 0 {
245 let adj_seg = if proj.1 .0 == 0 {
246 self.indices().len() as u32 - 1
247 } else {
248 proj.1 .0 - 1
249 };
250
251 assert_eq!(segment1.a, self.segment(adj_seg).b);
252 -self.segment(adj_seg).scaled_direction()
253 } else {
254 assert_eq!(i, 1);
255 let adj_seg = (proj.1 .0 + 1) % self.indices().len() as u32;
256 assert_eq!(segment1.b, self.segment(adj_seg).a);
257
258 self.segment(adj_seg).scaled_direction()
259 };
260
261 let dot = normal1.dot(&dir2);
262 // TODO: is this threshold too big? This corresponds to an angle equal to
263 // abs(acos(1.0e-3)) = (90 - 0.057) degrees.
264 // We did encounter some cases where this was needed, but perhaps the
265 // actual problem was an issue with the SegmentPointLocation (which should
266 // perhaps have been Edge instead of Vertex)?
267 let threshold = 1.0e-3 * dir2.norm();
268 if dot.abs() > threshold {
269 // If the vertex is a reentrant vertex, then the point is
270 // inside. Otherwise, it is outside.
271 dot >= 0.0
272 } else {
273 // If the two edges are collinear, we can’t classify the vertex.
274 // So check against the edge’s normal instead.
275 (point - proj.0.point).dot(&normal1) <= 0.0
276 }
277 }
278 SegmentPointLocation::OnEdge(_) => (point - proj.0.point).dot(&normal1) <= 0.0,
279 };
280 }
281
282 proj
283 }
284}
285
286impl CompositeShape for Polyline {
287 fn map_part_at(
288 &self,
289 i: u32,
290 f: &mut dyn FnMut(Option<&Isometry<Real>>, &dyn Shape, Option<&dyn NormalConstraints>),
291 ) {
292 let tri = self.segment(i);
293 f(None, &tri, None)
294 }
295
296 fn bvh(&self) -> &Bvh {
297 &self.bvh
298 }
299}
300
301impl TypedCompositeShape for Polyline {
302 type PartShape = Segment;
303 type PartNormalConstraints = ();
304
305 #[inline(always)]
306 fn map_typed_part_at<T>(
307 &self,
308 i: u32,
309 mut f: impl FnMut(
310 Option<&Isometry<Real>>,
311 &Self::PartShape,
312 Option<&Self::PartNormalConstraints>,
313 ) -> T,
314 ) -> Option<T> {
315 let seg = self.segment(i);
316 Some(f(None, &seg, None))
317 }
318
319 #[inline(always)]
320 fn map_untyped_part_at<T>(
321 &self,
322 i: u32,
323 mut f: impl FnMut(Option<&Isometry<Real>>, &dyn Shape, Option<&dyn NormalConstraints>) -> T,
324 ) -> Option<T> {
325 let seg = self.segment(i);
326 Some(f(None, &seg, None))
327 }
328}