use super::super::mapping::{
effective_align, mate, ConstraintFailure, MappedConstraint, ResolvedElement,
};
use super::{elements_field, id_field, schema_entry, ConstraintTypeDef};
use crate::feature_pipeline::assembly::ConstraintEntry;
use crate::feature_pipeline::SelectionProbe;
use crate::{MateAxis, MateKind, MatePlane, SelectionGeometry, Vec3};
pub(super) const DEF: ConstraintTypeDef = ConstraintTypeDef {
type_id: "center",
label: "Center",
short_name: "CNTR",
icon: "\u{25EB}",
long_name: "\u{25EB} Center",
min_elements: 3,
max_elements: 4,
duplicate_family: true,
applicable,
};
fn applicable(probe: &SelectionProbe) -> bool {
probe.all_component
&& probe.components == 2
&& probe.faces >= 2
&& (3..=4).contains(&(probe.faces + probe.edges))
}
pub(super) fn schema() -> serde_json::Value {
schema_entry(&DEF, serde_json::json!({
"id": id_field(),
"elements": elements_field(&["FACE", "EDGE", "VERTEX"], 3, 4,
"Two parallel planar faces on one component (the width) and, on another component, one element or two like elements to centre between them (the tab)"),
"facing": super::super::mapping::facing_field("Planar tab: Aligned or Opposed to the width's mid-plane. Unset, a new constraint takes the facing it is placed with"),
}))
}
const PARALLEL_SIN_TOL: f64 = 1e-4;
fn parallel(a: Vec3, b: Vec3) -> bool {
a.cross(b).length() <= PARALLEL_SIN_TOL
}
#[derive(Clone, Copy)]
struct Plane {
origin: Vec3,
normal: Vec3,
}
impl Plane {
fn of(geometry: &SelectionGeometry) -> Option<Self> {
match *geometry {
SelectionGeometry::Plane { origin, normal } => Some(Self { origin, normal }),
_ => None,
}
}
fn mid(self, other: Plane) -> Plane {
let separation = other.origin.sub(self.origin).dot(self.normal);
Plane {
origin: self.origin.add(self.normal.scale(separation * 0.5)),
normal: self.normal,
}
}
fn mate(self) -> MatePlane {
MatePlane {
origin: [self.origin.x, self.origin.y, self.origin.z],
normal: [self.normal.x, self.normal.y, self.normal.z],
}
}
fn height(self, point: Vec3) -> f64 {
point.sub(self.origin).dot(self.normal)
}
}
#[derive(Clone, Copy)]
struct Line {
origin: Vec3,
direction: Vec3,
}
impl Line {
fn of(geometry: &SelectionGeometry) -> Option<Self> {
match *geometry {
SelectionGeometry::Line { origin, direction }
| SelectionGeometry::Axis {
origin, direction, ..
} => Some(Self { origin, direction }),
_ => None,
}
}
fn mid(self, other: Line) -> Line {
let along = self.origin.sub(other.origin).dot(other.direction);
let foot = other.origin.add(other.direction.scale(along));
Line {
origin: self.origin.add(foot).scale(0.5),
direction: self.direction,
}
}
fn mate(self) -> MateAxis {
MateAxis {
origin: [self.origin.x, self.origin.y, self.origin.z],
direction: [self.direction.x, self.direction.y, self.direction.z],
}
}
}
#[derive(Clone, Copy)]
enum Centre {
Plane(Plane),
Line(Line),
Point(Vec3),
}
impl Centre {
fn of(geometry: &SelectionGeometry) -> Self {
if let Some(plane) = Plane::of(geometry) {
Self::Plane(plane)
} else if let Some(line) = Line::of(geometry) {
Self::Line(line)
} else {
Self::Point(geometry.representative_point())
}
}
fn mid(lead: Self, other: Self, lead_name: &str, other_name: &str) -> Result<Self, ConstraintFailure> {
match (lead, other) {
(Self::Plane(a), Self::Plane(b)) => {
if !parallel(a.normal, b.normal) {
return Err(ConstraintFailure::unsupported(format!(
"center: the two tab faces '{lead_name}' and '{other_name}' must be parallel"
)));
}
Ok(Self::Plane(a.mid(b)))
}
(Self::Line(a), Self::Line(b)) => {
if !parallel(a.direction, b.direction) {
return Err(ConstraintFailure::unsupported(format!(
"center: the two tab axes '{lead_name}' and '{other_name}' must be parallel"
)));
}
Ok(Self::Line(a.mid(b)))
}
(Self::Point(a), Self::Point(b)) => Ok(Self::Point(a.add(b).scale(0.5))),
_ => Err(ConstraintFailure::unsupported(format!(
"center: the two tab elements '{lead_name}' and '{other_name}' must be alike \
(two planar faces, two axes, or two points)"
))),
}
}
fn anchor(self) -> Vec3 {
match self {
Self::Plane(plane) => plane.origin,
Self::Line(line) => line.origin,
Self::Point(point) => point,
}
}
}
struct Group<'a> {
indexes: Vec<usize>,
elements: Vec<&'a ResolvedElement>,
}
impl<'a> Group<'a> {
fn lead(&self) -> &'a ResolvedElement {
self.elements[0]
}
fn names(&self) -> Vec<&str> {
self.elements.iter().map(|element| element.name.as_str()).collect()
}
fn width(&self) -> Option<(Plane, Plane)> {
let [lead, other] = self.elements.as_slice() else {
return None;
};
let (lw, ow) = (Plane::of(&lead.world)?, Plane::of(&other.world)?);
if !parallel(lw.normal, ow.normal) {
return None;
}
let (ll, ol) = (Plane::of(&lead.local)?, Plane::of(&other.local)?);
Some((lw.mid(ow), ll.mid(ol)))
}
fn centre(&self) -> Result<(Centre, Centre), ConstraintFailure> {
match self.elements.as_slice() {
[single] => Ok((Centre::of(&single.world), Centre::of(&single.local))),
[lead, other] => Ok((
Centre::mid(Centre::of(&lead.world), Centre::of(&other.world), &lead.name, &other.name)?,
Centre::mid(Centre::of(&lead.local), Centre::of(&other.local), &lead.name, &other.name)?,
)),
_ => Err(ConstraintFailure::unsupported(
"center: the tab is one element or two like elements",
)),
}
}
}
fn groups(resolved: &[ResolvedElement]) -> Result<[Group<'_>; 2], ConstraintFailure> {
let mut by_component: Vec<(&str, Vec<usize>)> = Vec::new();
for (index, element) in resolved.iter().enumerate() {
match by_component
.iter_mut()
.find(|(component, _)| *component == element.component)
{
Some((_, members)) => members.push(index),
None => by_component.push((element.component.as_str(), vec![index])),
}
}
if by_component.len() != 2 {
return Err(ConstraintFailure::unsupported(format!(
"center acts between two components: two parallel planar faces on one (the width) \
and one or two elements on another (the tab) — the selection spans {} components",
by_component.len()
)));
}
let build = |mut indexes: Vec<usize>| {
indexes.sort_by(|&a, &b| resolved[a].name.cmp(&resolved[b].name));
let elements = indexes.iter().map(|&index| &resolved[index]).collect();
Group { indexes, elements }
};
let mut groups = by_component.into_iter().map(|(_, indexes)| build(indexes));
Ok([groups.next().expect("two groups"), groups.next().expect("two groups")])
}
pub(in crate::feature_pipeline::assembly) fn map(
entry: &mut ConstraintEntry,
resolved: &[ResolvedElement],
) -> Result<MappedConstraint, ConstraintFailure> {
let [first, second] = groups(resolved)?;
let (width, tab, mid_world, mid_local) = match (first.width(), second.width()) {
(Some(_), Some(_)) if second.lead().name < first.lead().name => {
let (world, local) = second.width().expect("checked");
(second, first, world, local)
}
(Some((world, local)), _) => (first, second, world, local),
(None, Some((world, local))) => (second, first, world, local),
(None, None) => {
return Err(ConstraintFailure::unsupported(format!(
"center needs two parallel planar faces on one component (the width) and one or two \
elements on another (the tab); '{}' and '{}' hold no such face pair",
first.names().join("', '"),
second.names().join("', '"),
)));
}
};
let (centre_world, centre_local) = tab.centre()?;
let width_names = width.names();
let note = format!(
"{} centred between {} and {}",
tab.names().join(" + "),
width_names[0],
width_names[1]
);
let groups = vec![width.indexes.clone(), tab.indexes.clone()];
let offset = mid_world.height(centre_world.anchor());
let (w, t) = (width.lead(), tab.lead());
let mates = match (centre_world, centre_local) {
(Centre::Plane(plane_world), Centre::Plane(plane_local)) => {
let align = effective_align(entry, mid_world.normal, plane_world.normal, "reverse");
vec![mate(w, t, MateKind::CoincidentPlanePlane {
plane_a: mid_local.mate(),
plane_b: plane_local.mate(),
align,
})]
}
(_, Centre::Line(line_local)) => vec![
mate(w, t, MateKind::Perpendicular {
direction_a: mid_local.mate().normal,
direction_b: line_local.mate().direction,
}),
mate(t, w, MateKind::CoincidentPointPlane {
point_a: line_local.mate().origin,
plane_b: mid_local.mate(),
}),
],
(_, Centre::Point(point_local)) => vec![mate(t, w, MateKind::CoincidentPointPlane {
point_a: [point_local.x, point_local.y, point_local.z],
plane_b: mid_local.mate(),
})],
(_, Centre::Plane(_)) => unreachable!("world and local centres share a kind"),
};
Ok(MappedConstraint {
mates,
measured: Some((offset, "mm")),
groups,
note: Some(note),
..Default::default()
})
}