use crate::bounding_volume::Aabb;
use crate::math::{Pose, Vector};
use crate::partitioning::{Bvh, BvhBuildStrategy};
use crate::query::{PointProjection, PointQueryWithLocation};
use crate::shape::composite_shape::CompositeShape;
use crate::shape::{
FeatureId, Segment, SegmentPointLocation, SegmentPseudoNormals, Shape, TypedCompositeShape,
};
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use crate::query::details::NormalConstraints;
#[cfg(feature = "dim2")]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
)]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PolylineFlags(u8);
#[cfg(feature = "dim2")]
bitflags::bitflags! {
impl PolylineFlags: u8 {
const ORIENTED = 1;
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
)]
pub struct Polyline {
bvh: Bvh,
vertices: Vec<Vector>,
indices: Vec<[u32; 2]>,
#[cfg(feature = "dim2")]
pseudo_normals: Option<Vec<Vector>>,
#[cfg(feature = "dim2")]
flags: PolylineFlags,
}
impl Polyline {
pub fn new(vertices: Vec<Vector>, indices: Option<Vec<[u32; 2]>>) -> Self {
let indices =
indices.unwrap_or_else(|| (1..vertices.len() as u32).map(|i| [i - 1, i]).collect());
let leaves = indices.iter().enumerate().map(|(i, idx)| {
let aabb =
Segment::new(vertices[idx[0] as usize], vertices[idx[1] as usize]).local_aabb();
(i, aabb)
});
let bvh = Bvh::from_iter(BvhBuildStrategy::Binned, leaves);
Self {
bvh,
vertices,
indices,
#[cfg(feature = "dim2")]
pseudo_normals: None,
#[cfg(feature = "dim2")]
flags: PolylineFlags::empty(),
}
}
#[cfg(feature = "dim2")]
pub fn with_flags(
vertices: Vec<Vector>,
indices: Option<Vec<[u32; 2]>>,
flags: PolylineFlags,
) -> Self {
let mut result = Self::new(vertices, indices);
result.set_flags(flags);
result
}
#[cfg(feature = "dim2")]
pub fn set_flags(&mut self, flags: PolylineFlags) {
self.flags = flags;
if flags.contains(PolylineFlags::ORIENTED) {
self.compute_pseudo_normals();
} else {
self.pseudo_normals = None;
}
}
#[cfg(feature = "dim2")]
pub fn flags(&self) -> PolylineFlags {
self.flags
}
#[cfg(feature = "dim2")]
fn compute_pseudo_normals(&mut self) {
let mut vertex_normals = Vec::new();
vertex_normals.resize(self.vertices.len(), Vector::ZERO);
for idx in &self.indices {
let a = idx[0] as usize;
let b = idx[1] as usize;
let normal = crate::utils::ccw_face_normal([self.vertices[a], self.vertices[b]])
.unwrap_or(Vector::ZERO);
vertex_normals[a] += normal;
vertex_normals[b] += normal;
}
for normal in &mut vertex_normals {
*normal = normal.normalize_or_zero();
}
self.pseudo_normals = Some(vertex_normals);
}
#[cfg(feature = "dim2")]
pub fn segment_normal_constraints(&self, i: u32) -> Option<SegmentPseudoNormals> {
let pseudo_normals = self.pseudo_normals.as_ref()?;
let idx = self.indices[i as usize];
let a = idx[0] as usize;
let b = idx[1] as usize;
let face = crate::utils::ccw_face_normal([self.vertices[a], self.vertices[b]])?;
Some(SegmentPseudoNormals {
face,
edges: [pseudo_normals[a], pseudo_normals[b]],
})
}
#[cfg(feature = "dim3")]
#[doc(hidden)]
pub fn segment_normal_constraints(&self, _i: u32) -> Option<SegmentPseudoNormals> {
None
}
pub fn aabb(&self, pos: &Pose) -> Aabb {
self.bvh.root_aabb().transform_by(pos)
}
pub fn local_aabb(&self) -> Aabb {
self.bvh.root_aabb()
}
pub fn bvh(&self) -> &Bvh {
&self.bvh
}
pub fn num_segments(&self) -> usize {
self.indices.len()
}
pub fn segments(&self) -> impl ExactSizeIterator<Item = Segment> + '_ {
self.indices.iter().map(move |ids| {
Segment::new(
self.vertices[ids[0] as usize],
self.vertices[ids[1] as usize],
)
})
}
pub fn segment(&self, i: u32) -> Segment {
let idx = self.indices[i as usize];
Segment::new(
self.vertices[idx[0] as usize],
self.vertices[idx[1] as usize],
)
}
pub fn segment_feature_to_polyline_feature(
&self,
segment: u32,
_feature: FeatureId,
) -> FeatureId {
#[cfg(feature = "dim2")]
return FeatureId::Face(segment);
#[cfg(feature = "dim3")]
return FeatureId::Edge(segment);
}
pub fn vertices(&self) -> &[Vector] {
&self.vertices[..]
}
pub fn indices(&self) -> &[[u32; 2]] {
&self.indices
}
pub fn flat_indices(&self) -> &[u32] {
unsafe {
let len = self.indices.len() * 2;
let data = self.indices.as_ptr() as *const u32;
core::slice::from_raw_parts(data, len)
}
}
pub fn scaled(mut self, scale: Vector) -> Self {
self.vertices.iter_mut().for_each(|pt| *pt *= scale);
let mut bvh = self.bvh.clone();
bvh.scale(scale);
#[cfg(feature = "dim2")]
{
let mut result = Self {
bvh,
vertices: self.vertices,
indices: self.indices,
pseudo_normals: None,
flags: PolylineFlags::empty(),
};
result.set_flags(self.flags);
result
}
#[cfg(feature = "dim3")]
{
Self {
bvh,
vertices: self.vertices,
indices: self.indices,
}
}
}
pub fn reverse(&mut self) {
for idx in &mut self.indices {
idx.swap(0, 1);
}
self.indices.reverse();
let leaves = self.segments().map(|seg| seg.local_aabb()).enumerate();
let bvh = Bvh::from_iter(BvhBuildStrategy::Binned, leaves);
self.bvh = bvh;
#[cfg(feature = "dim2")]
if self.flags.contains(PolylineFlags::ORIENTED) {
self.compute_pseudo_normals();
}
}
pub fn extract_connected_components(&self) -> Vec<Polyline> {
let vertices = self.vertices();
let indices = self.indices();
if indices.is_empty() {
Vec::new()
} else {
let mut components = Vec::new();
let mut start_i = 0; let mut start_node = indices[0][0];
let mut component_vertices = Vec::new();
let mut component_indices: Vec<[u32; 2]> = Vec::new();
for (i, idx) in indices.iter().enumerate() {
component_vertices.push(vertices[idx[0] as usize]);
if idx[1] != start_node {
component_indices.push([(i - start_i) as u32, (i - start_i + 1) as u32]);
} else {
component_indices.push([(i - start_i) as u32, 0]);
components.push(Polyline::new(
core::mem::take(&mut component_vertices),
Some(core::mem::take(&mut component_indices)),
));
if i + 1 < indices.len() {
start_node = indices[i + 1][0];
start_i = i + 1;
}
}
}
components
}
}
pub fn project_local_point_assuming_solid_interior_ccw(
&self,
point: Vector,
#[cfg(feature = "dim3")] axis: u8,
) -> (PointProjection, (u32, SegmentPointLocation)) {
let mut proj = self.project_local_point_and_get_location(point, false);
let segment1 = self.segment((proj.1).0);
#[cfg(feature = "dim2")]
let normal1 = segment1.normal();
#[cfg(feature = "dim3")]
let normal1 = segment1.planar_normal(axis);
if let Some(normal1) = normal1 {
proj.0.is_inside = match proj.1 .1 {
SegmentPointLocation::OnVertex(i) => {
let dir2 = if i == 0 {
let adj_seg = if proj.1 .0 == 0 {
self.indices().len() as u32 - 1
} else {
proj.1 .0 - 1
};
assert_eq!(segment1.a, self.segment(adj_seg).b);
-self.segment(adj_seg).scaled_direction()
} else {
assert_eq!(i, 1);
let adj_seg = (proj.1 .0 + 1) % self.indices().len() as u32;
assert_eq!(segment1.b, self.segment(adj_seg).a);
self.segment(adj_seg).scaled_direction()
};
let dot = normal1.dot(dir2);
let threshold = 1.0e-3 * dir2.length();
if dot.abs() > threshold {
dot >= 0.0
} else {
(point - proj.0.point).dot(normal1) <= 0.0
}
}
SegmentPointLocation::OnEdge(_) => (point - proj.0.point).dot(normal1) <= 0.0,
};
}
proj
}
}
impl CompositeShape for Polyline {
fn map_part_at(
&self,
i: u32,
f: &mut dyn FnMut(Option<&Pose>, &dyn Shape, Option<&dyn NormalConstraints>),
) {
let seg = self.segment(i);
let normals = self.segment_normal_constraints(i);
f(
None,
&seg,
normals.as_ref().map(|n| n as &dyn NormalConstraints),
)
}
fn bvh(&self) -> &Bvh {
&self.bvh
}
}
impl TypedCompositeShape for Polyline {
type PartShape = Segment;
type PartNormalConstraints = SegmentPseudoNormals;
#[inline(always)]
fn map_typed_part_at<T>(
&self,
i: u32,
mut f: impl FnMut(Option<&Pose>, &Self::PartShape, Option<&Self::PartNormalConstraints>) -> T,
) -> Option<T> {
let seg = self.segment(i);
let normals = self.segment_normal_constraints(i);
Some(f(None, &seg, normals.as_ref()))
}
#[inline(always)]
fn map_untyped_part_at<T>(
&self,
i: u32,
mut f: impl FnMut(Option<&Pose>, &dyn Shape, Option<&dyn NormalConstraints>) -> T,
) -> Option<T> {
let seg = self.segment(i);
let normals = self.segment_normal_constraints(i);
Some(f(
None,
&seg,
normals.as_ref().map(|n| n as &dyn NormalConstraints),
))
}
}
#[cfg(test)]
#[cfg(all(feature = "dim2", feature = "alloc"))]
mod pseudo_normal_tests {
use crate::math::Vector;
use crate::shape::{Polyline, PolylineFlags};
fn ccw_square() -> Polyline {
let vertices = vec![
Vector::new(-1.0, -1.0),
Vector::new(1.0, -1.0),
Vector::new(1.0, 1.0),
Vector::new(-1.0, 1.0),
];
Polyline::new(vertices, Some(vec![[0, 1], [1, 2], [2, 3], [3, 0]]))
}
#[test]
fn not_oriented_by_default() {
assert!(ccw_square().segment_normal_constraints(0).is_none());
}
#[test]
fn face_and_edge_normals_are_unit_and_outward() {
let mut polyline = ccw_square();
polyline.set_flags(PolylineFlags::ORIENTED);
for i in 0..polyline.num_segments() as u32 {
let segment = polyline.segment(i);
let constraints = polyline.segment_normal_constraints(i).unwrap();
assert!((constraints.face.length() - 1.0).abs() < 1.0e-5);
for edge in constraints.edges {
assert!((edge.length() - 1.0).abs() < 1.0e-5);
}
let midpoint = (segment.a + segment.b) * 0.5;
assert!(constraints.face.dot(midpoint) > 0.0);
}
}
#[test]
fn corner_pseudo_normal_bisects_its_two_faces() {
let mut polyline = ccw_square();
polyline.set_flags(PolylineFlags::ORIENTED);
let bottom = polyline.segment_normal_constraints(0).unwrap();
assert!(bottom.edges[1].abs_diff_eq(Vector::new(1.0, -1.0).normalize(), 1.0e-5));
}
#[test]
fn degenerate_input_yields_no_segments() {
assert_eq!(Polyline::new(vec![], None).num_segments(), 0);
assert_eq!(Polyline::new(vec![Vector::ZERO], None).num_segments(), 0);
}
#[test]
fn oriented_point_query_treats_interior_as_inside() {
use crate::query::{PointQuery, PointQueryWithLocation};
let mut polyline = ccw_square();
polyline.set_flags(PolylineFlags::ORIENTED);
let inside = Vector::new(0.25, -0.5);
let solid = polyline.project_local_point(inside, true);
assert!(solid.is_inside);
assert!(solid.point.abs_diff_eq(inside, 1.0e-5));
let hollow = polyline.project_local_point(inside, false);
assert!(hollow.is_inside);
assert!(!hollow.point.abs_diff_eq(inside, 1.0e-5));
assert!(polyline.contains_local_point(inside));
assert!(
polyline
.project_local_point_and_get_location(inside, false)
.0
.is_inside
);
assert!(
polyline
.project_local_point_and_get_feature(inside)
.0
.is_inside
);
}
#[test]
fn oriented_point_query_leaves_exterior_outside() {
use crate::query::PointQuery;
let mut polyline = ccw_square();
polyline.set_flags(PolylineFlags::ORIENTED);
let outside = Vector::new(2.0, 0.0);
let proj = polyline.project_local_point(outside, true);
assert!(!proj.is_inside);
assert!(proj.point.abs_diff_eq(Vector::new(1.0, 0.0), 1.0e-5));
assert!(!polyline.contains_local_point(outside));
}
#[test]
fn unoriented_polyline_has_no_interior() {
use crate::query::PointQuery;
let polyline = ccw_square();
let center = Vector::ZERO;
assert!(!polyline.project_local_point(center, true).is_inside);
assert!(!polyline.contains_local_point(center));
}
}