use crate::math::{Real, Vector};
#[cfg(feature = "alloc")]
use crate::query::details::NormalConstraints;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[cfg(all(feature = "alloc", feature = "dim2"))]
pub(crate) fn turns_clockwise(from: Vector, to: Vector) -> bool {
from.x * to.y - from.y * to.x < 0.0
}
#[cfg(all(feature = "alloc", feature = "dim2"))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, Debug)]
pub struct CompoundEdgeCone {
pub face: Vector,
pub clockwise_limit: Vector,
pub counter_clockwise_limit: Vector,
}
#[cfg(all(feature = "alloc", feature = "dim2"))]
impl CompoundEdgeCone {
fn contains(&self, dir: Vector) -> bool {
dir.dot(self.face) > 0.0
&& !turns_clockwise(self.clockwise_limit, dir)
&& !turns_clockwise(dir, self.counter_clockwise_limit)
}
fn clamped(&self, dir: Vector) -> Vector {
if self.contains(dir) {
dir
} else if dir.dot(self.clockwise_limit) >= dir.dot(self.counter_clockwise_limit) {
self.clockwise_limit
} else {
self.counter_clockwise_limit
}
}
}
#[cfg(all(feature = "alloc", feature = "dim2"))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, Default)]
pub struct CompoundPseudoNormals {
pub boundary_edges: Vec<CompoundEdgeCone>,
}
#[cfg(all(feature = "alloc", feature = "dim2"))]
impl NormalConstraints for CompoundPseudoNormals {
fn project_local_normal_mut(&self, dir: &mut Vector) -> bool {
let mut nearest: Option<(Vector, Real)> = None;
for edge in &self.boundary_edges {
let candidate = edge.clamped(*dir);
let alignment = dir.dot(candidate);
match nearest {
Some((_, best)) if alignment <= best => {}
_ => nearest = Some((candidate, alignment)),
}
}
match nearest {
Some((direction, alignment)) if alignment > 0.0 => {
*dir = direction;
true
}
_ => false,
}
}
}
#[cfg(all(feature = "alloc", feature = "dim3"))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug)]
pub struct CompoundFaceCone {
pub face: Vector,
pub edge_pseudo_normals: Vec<Vector>,
}
#[cfg(all(feature = "alloc", feature = "dim3"))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, Default)]
pub struct CompoundPseudoNormals {
pub boundary_faces: Vec<CompoundFaceCone>,
}
#[cfg(all(feature = "alloc", feature = "dim3"))]
impl NormalConstraints for CompoundPseudoNormals {
fn project_local_normal_mut(&self, dir: &mut Vector) -> bool {
let mut best: Option<(&CompoundFaceCone, Real)> = None;
for cone in &self.boundary_faces {
let alignment = dir.dot(cone.face);
match best {
Some((_, best_alignment)) if alignment <= best_alignment => {}
_ => best = Some((cone, alignment)),
}
}
let Some((cone, _)) = best else {
return false;
};
let closest_edge = cone
.edge_pseudo_normals
.iter()
.copied()
.max_by(|a, b| dir.dot(*a).total_cmp(&dir.dot(*b)))
.unwrap_or(cone.face);
crate::shape::pseudo_normals::project_into_cone(cone.face, closest_edge, dir)
}
}
#[cfg(test)]
#[cfg(all(feature = "dim2", feature = "alloc"))]
mod test {
use super::{CompoundEdgeCone, CompoundPseudoNormals, NormalConstraints};
use crate::math::Vector;
use alloc::vec;
fn cone(face: Vector, clockwise: Vector, counter_clockwise: Vector) -> CompoundEdgeCone {
CompoundEdgeCone {
face,
clockwise_limit: clockwise,
counter_clockwise_limit: counter_clockwise,
}
}
fn pinned(face: Vector) -> CompoundEdgeCone {
cone(face, face, face)
}
#[test]
fn buried_part_rejects_everything() {
let normals = CompoundPseudoNormals::default();
assert!(normals.project_local_normal(Vector::Y).is_none());
assert!(normals.project_local_normal(-Vector::Y).is_none());
}
#[test]
fn a_direction_already_on_the_outline_is_left_alone() {
let normals = CompoundPseudoNormals {
boundary_edges: vec![cone(
Vector::Y,
Vector::new(1.0, 1.0).normalize(),
Vector::new(-1.0, 1.0).normalize(),
)],
};
assert_eq!(normals.project_local_normal(Vector::Y), Some(Vector::Y));
let inside_the_cone = Vector::new(0.3, 1.0).normalize();
assert_eq!(
normals.project_local_normal(inside_the_cone),
Some(inside_the_cone)
);
}
#[test]
fn a_direction_past_the_corner_is_pulled_back_to_it() {
let corner = Vector::new(1.0, 1.0).normalize();
let normals = CompoundPseudoNormals {
boundary_edges: vec![cone(Vector::Y, corner, Vector::new(-1.0, 1.0).normalize())],
};
assert_eq!(normals.project_local_normal(Vector::X), Some(corner));
}
#[test]
fn picks_the_edge_the_normal_points_out_of() {
let normals = CompoundPseudoNormals {
boundary_edges: vec![pinned(Vector::Y), pinned(Vector::X)],
};
assert_eq!(normals.project_local_normal(Vector::Y), Some(Vector::Y));
assert_eq!(normals.project_local_normal(Vector::X), Some(Vector::X));
}
#[test]
fn a_direction_reaching_the_part_from_behind_is_dropped() {
let normals = CompoundPseudoNormals {
boundary_edges: vec![pinned(Vector::Y)],
};
assert!(normals.project_local_normal(-Vector::Y).is_none());
}
#[test]
fn clamps_a_wedge_tip_normal_onto_the_boundary_edge() {
let ramp = Vector::new(-0.6, 0.8).normalize();
let normals = CompoundPseudoNormals {
boundary_edges: vec![pinned(ramp)],
};
let clamped = normals.project_local_normal(-Vector::X).unwrap();
assert!((clamped - ramp).length() < 1.0e-5);
}
}