use crate::kinematics::{Chain, KinematicsOptions};
use crate::model::{Point3, RigidTransform};
use crate::multiaxis::sim::tridexel::TriDexel;
use crate::kinematics::post::ResolvedMotion;
use crate::{CamError, Runner};
use serde::{Deserialize, Serialize};
pub use crate::model::{Link, LinkShape, LinkSolid, MachineModel};
pub trait LinkShapeGeometry {
fn contains_local(&self, p: Point3) -> bool;
fn surface_samples(&self, spacing: f64) -> Vec<Point3>;
}
impl LinkShapeGeometry for LinkShape {
fn contains_local(&self, p: Point3) -> bool {
match self {
LinkShape::Box { min, max } => p.x >= min.x && p.x <= max.x && p.y >= min.y && p.y <= max.y && p.z >= min.z && p.z <= max.z,
LinkShape::Cylinder { base, axis, radius, height } => {
let a = axis.normalized().unwrap_or(Point3::Z);
let d = p.sub(*base);
let along = d.dot(a);
along >= 0.0 && along <= *height && d.sub(a.scale(along)).length() <= *radius
}
}
}
fn surface_samples(&self, spacing: f64) -> Vec<Point3> {
let steps = |len: f64| ((len / spacing).ceil() as usize).max(1);
let mut out = Vec::new();
match self {
LinkShape::Box { min, max } => {
let n = [steps(max.x - min.x), steps(max.y - min.y), steps(max.z - min.z)];
for i in 0..=n[0] {
for j in 0..=n[1] {
for k in 0..=n[2] {
let on_face = i == 0 || i == n[0] || j == 0 || j == n[1] || k == 0 || k == n[2];
if !on_face {
continue;
}
out.push(Point3::new(
min.x + (max.x - min.x) * i as f64 / n[0] as f64,
min.y + (max.y - min.y) * j as f64 / n[1] as f64,
min.z + (max.z - min.z) * k as f64 / n[2] as f64,
));
}
}
}
}
LinkShape::Cylinder { base, axis, radius, height } => {
let a = axis.normalized().unwrap_or(Point3::Z);
let e1 = crate::geom::perpendicular(a);
let e2 = a.cross(e1);
let nh = steps(*height);
let na = steps(std::f64::consts::TAU * radius).max(8);
let nr = steps(*radius);
for k in 0..=nh {
let h = height * k as f64 / nh as f64;
for i in 0..na {
let phi = std::f64::consts::TAU * i as f64 / na as f64;
out.push(base.add(a.scale(h)).add(e1.scale(radius * phi.cos())).add(e2.scale(radius * phi.sin())));
}
}
for h in [0.0, *height] {
for r in 0..nr {
let rr = radius * r as f64 / nr as f64;
let m = if r == 0 { 1 } else { na };
for i in 0..m {
let phi = std::f64::consts::TAU * i as f64 / m as f64;
out.push(base.add(a.scale(h)).add(e1.scale(rr * phi.cos())).add(e2.scale(rr * phi.sin())));
}
}
}
}
}
out
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct MachineCheckOptions {
pub surface_spacing: f64,
pub max_step_deg: f64,
pub max_step_linear: f64,
pub max_work: u64,
}
impl Default for MachineCheckOptions {
fn default() -> Self {
Self { surface_spacing: 0.5, max_step_deg: 2.0, max_step_linear: 5.0, max_work: 2_000_000_000 }
}
}
impl MachineCheckOptions {
pub fn validate(&self) -> Result<(), CamError> {
let bad = |m: &str| Err(CamError::InvalidInput(format!("machine check: {m}")));
if !self.surface_spacing.is_finite() || self.surface_spacing <= 0.0 {
return bad("surface spacing must be positive");
}
if !self.max_step_deg.is_finite() || self.max_step_deg <= 0.0 || !self.max_step_linear.is_finite() || self.max_step_linear <= 0.0 {
return bad("steps must be positive");
}
if self.max_work == 0 {
return bad("work budget must be positive");
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineCollision {
pub block: usize,
pub fraction: f64,
pub angles_deg: Vec<f64>,
pub linear: Point3,
pub body: String,
pub against: String,
pub point: Point3,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineCheckReport {
pub first: Option<MachineCollision>,
pub samples: u64,
pub work: u64,
pub bound: f64,
}
struct Placed<'a> {
solid: &'a LinkSolid,
to_world: RigidTransform,
to_local: RigidTransform,
}
fn is_tool_side(chain: &Chain, link: Link) -> bool {
match link {
Link::Tool => true,
Link::Joint { index } => chain.joints.get(index).map_or(false, |j| j.carrier == crate::kinematics::Carrier::Head),
Link::Base | Link::Part => false,
}
}
pub fn check_machine(
chain: &Chain,
model: &MachineModel,
blocks: &[ResolvedMotion],
stock: Option<&TriDexel>,
opts: &MachineCheckOptions,
runner: &mut Runner<'_>,
) -> Result<MachineCheckReport, CamError> {
opts.validate()?;
for s in &model.solids {
s.shape.validate().map_err(CamError::InvalidInput)?;
if let Link::Joint { index } = s.link {
if index >= chain.joints.len() {
return Err(CamError::InvalidInput(format!("machine solid {} sits on joint {index}, which the chain does not have", s.name)));
}
}
}
let samples: Vec<Vec<Point3>> = model.solids.iter().map(|s| s.shape.surface_samples(opts.surface_spacing)).collect();
let tool_side: Vec<usize> = (0..model.solids.len()).filter(|&i| is_tool_side(chain, model.solids[i].link)).collect();
let part_side: Vec<usize> = (0..model.solids.len()).filter(|&i| !is_tool_side(chain, model.solids[i].link)).collect();
let mut report = MachineCheckReport { bound: opts.surface_spacing, ..Default::default() };
let mut work_left = opts.max_work;
let n_rot = chain.joints.len();
let mut prev: Option<(Point3, Vec<f64>)> = None;
let mut tip: Option<Point3> = None;
let off = KinematicsOptions { tcp: false, ..Default::default() };
runner.stage("machine-check", "Machine collision check", 0, blocks.len() as u64)?;
for (bi, b) in blocks.iter().enumerate() {
runner.work(1, "machine-check", bi as u64, blocks.len() as u64)?;
let angles: Vec<f64> = b.rotary.iter().map(|w| w.angle_deg).collect();
if angles.len() != n_rot {
return Err(CamError::InvalidInput(format!("block {bi} carries {} rotary words, the chain has {n_rot} joints", angles.len())));
}
if let Some(t) = b.motion.end() {
tip = Some(t);
}
let Some(t) = tip else { continue };
let linear = chain.linear_for(t, &angles, &off);
let (steps, from) = match &prev {
None => (1usize, (linear, angles.clone())),
Some((pl, pa)) => {
let rot = pa.iter().zip(&angles).map(|(a, c)| (c - a).abs()).fold(0.0, f64::max);
let lin = pl.distance(linear);
let n = ((rot / opts.max_step_deg).ceil().max((lin / opts.max_step_linear).ceil()) as usize).max(1);
(n, (*pl, pa.clone()))
}
};
let start_s = if prev.is_none() { 0 } else { 1 };
for s in start_s..=steps {
let f = s as f64 / steps as f64;
let lin = from.0.lerp(linear, f);
let ang: Vec<f64> = from.1.iter().zip(&angles).map(|(a, c)| a + (c - a) * f).collect();
report.samples += 1;
let placed: Vec<Placed<'_>> = model
.solids
.iter()
.map(|sld| {
let t = chain.link_transform(sld.link, &ang, lin)?;
Ok(Placed { solid: sld, to_local: t.inverse(), to_world: t })
})
.collect::<Result<_, crate::kinematics::KinematicsError>>()?;
let mut hit: Option<MachineCollision> = None;
'pairs: for &ti in &tool_side {
for &pi in &part_side {
for (a, bb) in [(ti, pi), (pi, ti)] {
let cost = samples[a].len() as u64;
if work_left < cost {
return Err(CamError::InvalidInput("machine check: work budget exceeded".into()));
}
work_left -= cost;
report.work += cost;
for p in &samples[a] {
let w = placed[a].to_world.apply_point(*p);
let q = placed[bb].to_local.apply_point(w);
if placed[bb].solid.shape.contains_local(q) {
hit = Some(MachineCollision {
block: bi,
fraction: f,
angles_deg: ang.clone(),
linear: lin,
body: model.solids[ti].name.clone(),
against: model.solids[pi].name.clone(),
point: w,
});
break 'pairs;
}
}
}
}
if let Some(stock) = stock {
let cost = samples[ti].len() as u64;
if work_left < cost {
return Err(CamError::InvalidInput("machine check: work budget exceeded".into()));
}
work_left -= cost;
report.work += cost;
for p in &samples[ti] {
let w = placed[ti].to_world.apply_point(*p);
let in_part = chain.world_to_part(&ang, w);
if stock.contains(in_part) {
hit = Some(MachineCollision {
block: bi,
fraction: f,
angles_deg: ang.clone(),
linear: lin,
body: model.solids[ti].name.clone(),
against: "stock".into(),
point: w,
});
break 'pairs;
}
}
}
}
if hit.is_some() {
report.first = hit;
return Ok(report);
}
}
prev = Some((linear, angles));
}
Ok(report)
}