use serde::{Deserialize, Serialize};
use crate::arrangement::Vec2;
use crate::classification::{parameter_point_in_face, PolygonClass};
use crate::feature_pipeline::features::datum::rotate_euler_xyz;
use crate::projection::{project_point_to_curve, project_point_to_surface};
use crate::topology::{EdgeRecord, FaceRecord};
use crate::{BrepSolid, KernelTolerances, NurbsCurve, Vec3};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct PmiPose {
pub center: [f64; 3],
pub translate: [f64; 3],
#[serde(rename = "rotateDeg")]
pub rotate_deg: [f64; 3],
pub scale: [f64; 3],
}
impl PmiPose {
fn radians(&self) -> [f64; 3] {
self.rotate_deg.map(f64::to_radians)
}
pub fn apply(&self, point: [f64; 3]) -> [f64; 3] {
let c = self.center;
let local = Vec3::new(
(point[0] - c[0]) * self.scale[0],
(point[1] - c[1]) * self.scale[1],
(point[2] - c[2]) * self.scale[2],
);
let rotated = rotate_euler_xyz(local, self.radians());
[
rotated.x + c[0] + self.translate[0],
rotated.y + c[1] + self.translate[1],
rotated.z + c[2] + self.translate[2],
]
}
pub fn unapply(&self, point: [f64; 3]) -> [f64; 3] {
let c = self.center;
let moved = Vec3::new(
point[0] - c[0] - self.translate[0],
point[1] - c[1] - self.translate[1],
point[2] - c[2] - self.translate[2],
);
let e = self.radians();
let rx = rotate_euler_xyz(Vec3::new(1.0, 0.0, 0.0), e);
let ry = rotate_euler_xyz(Vec3::new(0.0, 1.0, 0.0), e);
let rz = rotate_euler_xyz(Vec3::new(0.0, 0.0, 1.0), e);
let local = Vec3::new(rx.dot(moved), ry.dot(moved), rz.dot(moved));
let unscale = |v: f64, s: f64| if s.abs() > 1e-300 { v / s } else { v };
[
unscale(local.x, self.scale[0]) + c[0],
unscale(local.y, self.scale[1]) + c[1],
unscale(local.z, self.scale[2]) + c[2],
]
}
pub fn is_rigid(&self) -> bool {
self.scale.iter().all(|s| (s - 1.0).abs() < 1e-12)
}
}
pub fn pose_apply(stages: &[PmiPose], point: [f64; 3]) -> [f64; 3] {
stages.iter().fold(point, |p, stage| stage.apply(p))
}
pub fn pose_unapply(stages: &[PmiPose], point: [f64; 3]) -> [f64; 3] {
stages.iter().rev().fold(point, |p, stage| stage.unapply(p))
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SolidPoses {
pub solid: String,
pub stages: Vec<PmiPose>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct HalfSpace {
pub point: [f64; 3],
pub normal: [f64; 3],
}
impl HalfSpace {
pub fn signed(&self, p: [f64; 3]) -> f64 {
(0..3).map(|i| (p[i] - self.point[i]) * self.normal[i]).sum()
}
pub fn keeps(&self, p: [f64; 3], tolerance: f64) -> bool {
self.signed(p) >= -tolerance
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "camelCase")]
pub enum BalloonAnchorMode {
Surface,
Edge {
solid: String,
#[serde(rename = "edgeId")]
edge_id: u64,
},
Point,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BalloonAnchor {
pub solids: Vec<String>,
#[serde(flatten)]
pub mode: BalloonAnchorMode,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub poses: Vec<SolidPoses>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub section: Option<HalfSpace>,
}
impl BalloonAnchor {
pub fn stages(&self, solid: &str) -> &[PmiPose] {
self.poses
.iter()
.find(|poses| poses.solid == solid)
.map_or(&[], |poses| poses.stages.as_slice())
}
pub fn scope(&self, solid: &str) -> HeadScope<'_> {
HeadScope {
stages: self.stages(solid),
section: self.section.as_ref(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct HeadScope<'a> {
pub stages: &'a [PmiPose],
pub section: Option<&'a HalfSpace>,
}
impl HeadScope<'_> {
pub const PLAIN: HeadScope<'static> = HeadScope {
stages: &[],
section: None,
};
fn display(&self, modeling: Vec3) -> [f64; 3] {
pose_apply(self.stages, [modeling.x, modeling.y, modeling.z])
}
fn modeling(&self, display: [f64; 3]) -> Vec3 {
let p = pose_unapply(self.stages, display);
Vec3::new(p[0], p[1], p[2])
}
fn keeps(&self, display: [f64; 3], tolerance: f64) -> bool {
self.section.is_none_or(|section| section.keeps(display, tolerance))
}
fn is_rigid(&self) -> bool {
self.stages.iter().all(PmiPose::is_rigid)
}
}
struct Best {
display: [f64; 3],
d2: f64,
}
fn offer(best: &mut Option<Best>, scope: &HeadScope<'_>, bubble: [f64; 3], tolerance: f64, modeling: Vec3) {
let display = scope.display(modeling);
if !scope.keeps(display, tolerance) {
return;
}
let d2 = (0..3).map(|i| (display[i] - bubble[i]).powi(2)).sum::<f64>();
if best.as_ref().is_none_or(|b| d2 < b.d2) {
*best = Some(Best { display, d2 });
}
}
pub fn nearest_displayed_point(solid: &BrepSolid, scope: HeadScope<'_>, bubble: [f64; 3]) -> Result<Option<[f64; 3]>, String> {
let spatial = KernelTolerances::for_solid(solid, 1e-7).spatial();
let query = scope.modeling(bubble);
let mut best: Option<Best> = None;
for vertex in &solid.vertices {
offer(&mut best, &scope, bubble, spatial, vertex.point);
}
for edge in &solid.edges {
if edge.degenerate {
continue;
}
offer(&mut best, &scope, bubble, spatial, nearest_point_on_edge(edge, query)?);
if let Some(section) = scope.section {
for crossing in edge_plane_crossings(edge, &scope, section)? {
offer(&mut best, &scope, bubble, spatial, crossing);
}
}
}
let rigid = scope.is_rigid();
for shell in &solid.shells {
for face in &shell.faces {
if rigid {
if let (Some(best), Some(hull_d2)) = (best.as_ref(), hull_distance_squared(face, query)?) {
if hull_d2 > best.d2 {
continue;
}
}
}
let Ok(projection) = project_point_to_surface(&face.surface, query) else {
continue;
};
if rigid && best.as_ref().is_some_and(|b| projection.distance * projection.distance >= b.d2) {
continue;
}
let uv_band = match face.surface.derivatives(projection.u, projection.v, 1) {
Ok(derivatives) => crate::tolerance::surface_uv_tolerance(
spatial,
derivatives[1][0].length(),
derivatives[0][1].length(),
),
Err(_) => spatial,
};
let Ok(class) = parameter_point_in_face(
face,
Vec2 {
x: projection.u,
y: projection.v,
},
uv_band,
) else {
continue;
};
if matches!(class, PolygonClass::Inside | PolygonClass::Boundary) {
offer(&mut best, &scope, bubble, spatial, projection.point);
}
}
}
Ok(best.map(|b| b.display))
}
pub fn nearest_point_on_solid(solid: &BrepSolid, point: Vec3) -> Result<Vec3, String> {
nearest_displayed_point(solid, HeadScope::PLAIN, [point.x, point.y, point.z])?
.map(|p| Vec3::new(p[0], p[1], p[2]))
.ok_or_else(|| "solid has no geometry".to_string())
}
fn hull_distance_squared(face: &FaceRecord, point: Vec3) -> Result<Option<f64>, String> {
let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
let mut any = false;
for row in &face.surface.control_points {
for control in row {
let p = control.point()?;
low = Vec3::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
high = Vec3::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
any = true;
}
}
if !any {
return Ok(None);
}
let axis = |v: f64, lo: f64, hi: f64| if v < lo { lo - v } else if v > hi { v - hi } else { 0.0 };
let dx = axis(point.x, low.x, high.x);
let dy = axis(point.y, low.y, high.y);
let dz = axis(point.z, low.z, high.z);
Ok(Some(dx * dx + dy * dy + dz * dz))
}
pub fn nearest_point_on_edge(edge: &EdgeRecord, point: Vec3) -> Result<Vec3, String> {
if edge.degenerate {
return edge.curve.evaluate(edge.t0);
}
let span = span_curve(&edge.curve, edge.t0.min(edge.t1), edge.t0.max(edge.t1))?;
let projection = project_point_to_curve(&span, point)?;
let mut best = (projection.distance, projection.point);
for parameter in [edge.t0, edge.t1] {
let end = edge.curve.evaluate(parameter)?;
let distance = end.sub(point).length();
if distance < best.0 {
best = (distance, end);
}
}
Ok(best.1)
}
pub fn nearest_displayed_point_on_edge(edge: &EdgeRecord, scope: HeadScope<'_>, bubble: [f64; 3]) -> Result<Option<[f64; 3]>, String> {
let tolerance = 1e-7;
let mut best: Option<Best> = None;
offer(&mut best, &scope, bubble, tolerance, nearest_point_on_edge(edge, scope.modeling(bubble))?);
if let Some(section) = scope.section {
if best.is_none() && !edge.degenerate {
for crossing in edge_plane_crossings(edge, &scope, section)? {
offer(&mut best, &scope, bubble, tolerance, crossing);
}
}
}
Ok(best.map(|b| b.display))
}
fn edge_plane_crossings(edge: &EdgeRecord, scope: &HeadScope<'_>, section: &HalfSpace) -> Result<Vec<Vec3>, String> {
const SAMPLES: usize = 64;
let (t0, t1) = (edge.t0.min(edge.t1), edge.t0.max(edge.t1));
if !(t1 > t0) {
return Ok(Vec::new());
}
let signed_at = |t: f64| -> Result<f64, String> { Ok(section.signed(scope.display(edge.curve.evaluate(t)?))) };
let mut out = Vec::new();
let mut previous = (t0, signed_at(t0)?);
for k in 1..=SAMPLES {
let t = t0 + (t1 - t0) * k as f64 / SAMPLES as f64;
let s = signed_at(t)?;
if previous.1 == 0.0 {
out.push(edge.curve.evaluate(previous.0)?);
} else if (previous.1 < 0.0) != (s < 0.0) && s != 0.0 {
let (mut a, mut sa, mut b) = (previous.0, previous.1, t);
for _ in 0..60 {
let m = (a + b) * 0.5;
let sm = signed_at(m)?;
if (sm < 0.0) == (sa < 0.0) {
a = m;
sa = sm;
} else {
b = m;
}
}
out.push(edge.curve.evaluate((a + b) * 0.5)?);
}
previous = (t, s);
}
if previous.1 == 0.0 {
out.push(edge.curve.evaluate(previous.0)?);
}
Ok(out)
}
fn span_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, String> {
let [start, end] = curve.domain()?;
let eps = (end - start).abs() * 1e-9;
let mut span = curve.clone();
if t0 > start + eps && t0 < end - eps {
span = span.split(t0)?.1;
}
let [_, span_end] = span.domain()?;
if t1 > start + eps && t1 < span_end - eps {
span = span.split(t1)?.0;
}
Ok(span)
}
pub fn balloon_head<'a>(
anchor: &BalloonAnchor,
bubble: [f64; 3],
mut lookup: impl FnMut(&str) -> Option<&'a BrepSolid>,
missing: &mut Vec<String>,
) -> Result<Option<[f64; 3]>, String> {
match &anchor.mode {
BalloonAnchorMode::Point => Ok(None),
BalloonAnchorMode::Edge { solid, edge_id } => {
let Some(brep) = lookup(solid) else {
missing.push(solid.clone());
return Ok(None);
};
let edge = brep
.edges
.iter()
.find(|edge| edge.id == *edge_id)
.ok_or_else(|| format!("anchor edge {edge_id} is not on '{solid}'"))?;
nearest_displayed_point_on_edge(edge, anchor.scope(solid), bubble)?
.map(Some)
.ok_or_else(|| format!("anchor edge {edge_id} of '{solid}' is sectioned away in this view"))
}
BalloonAnchorMode::Surface => {
let mut best: Option<Best> = None;
for name in &anchor.solids {
let Some(brep) = lookup(name) else {
missing.push(name.clone());
continue;
};
if let Some(display) = nearest_displayed_point(brep, anchor.scope(name), bubble)? {
let d2 = (0..3).map(|i| (display[i] - bubble[i]).powi(2)).sum::<f64>();
if best.as_ref().is_none_or(|b| d2 < b.d2) {
best = Some(Best { display, d2 });
}
}
}
if !missing.is_empty() {
return Ok(None);
}
best.map(|b| Some(b.display))
.ok_or_else(|| "the balloon's occurrence has no displayed geometry in this view".to_string())
}
}
}