1use crate::kinematics::{Chain, KinematicsOptions};
21use crate::model::{Point3, RigidTransform};
22use crate::multiaxis::sim::tridexel::TriDexel;
23use crate::kinematics::post::ResolvedMotion;
24use crate::{CamError, Runner};
25use serde::{Deserialize, Serialize};
26
27pub use crate::model::{Link, LinkShape, LinkSolid, MachineModel};
28
29pub trait LinkShapeGeometry {
31 fn contains_local(&self, p: Point3) -> bool;
33 fn surface_samples(&self, spacing: f64) -> Vec<Point3>;
36}
37
38impl LinkShapeGeometry for LinkShape {
39 fn contains_local(&self, p: Point3) -> bool {
41 match self {
42 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,
43 LinkShape::Cylinder { base, axis, radius, height } => {
44 let a = axis.normalized().unwrap_or(Point3::Z);
45 let d = p.sub(*base);
46 let along = d.dot(a);
47 along >= 0.0 && along <= *height && d.sub(a.scale(along)).length() <= *radius
48 }
49 }
50 }
51
52 fn surface_samples(&self, spacing: f64) -> Vec<Point3> {
55 let steps = |len: f64| ((len / spacing).ceil() as usize).max(1);
56 let mut out = Vec::new();
57 match self {
58 LinkShape::Box { min, max } => {
59 let n = [steps(max.x - min.x), steps(max.y - min.y), steps(max.z - min.z)];
60 for i in 0..=n[0] {
61 for j in 0..=n[1] {
62 for k in 0..=n[2] {
63 let on_face = i == 0 || i == n[0] || j == 0 || j == n[1] || k == 0 || k == n[2];
64 if !on_face {
65 continue;
66 }
67 out.push(Point3::new(
68 min.x + (max.x - min.x) * i as f64 / n[0] as f64,
69 min.y + (max.y - min.y) * j as f64 / n[1] as f64,
70 min.z + (max.z - min.z) * k as f64 / n[2] as f64,
71 ));
72 }
73 }
74 }
75 }
76 LinkShape::Cylinder { base, axis, radius, height } => {
77 let a = axis.normalized().unwrap_or(Point3::Z);
78 let e1 = crate::geom::perpendicular(a);
79 let e2 = a.cross(e1);
80 let nh = steps(*height);
81 let na = steps(std::f64::consts::TAU * radius).max(8);
82 let nr = steps(*radius);
83 for k in 0..=nh {
84 let h = height * k as f64 / nh as f64;
85 for i in 0..na {
86 let phi = std::f64::consts::TAU * i as f64 / na as f64;
87 out.push(base.add(a.scale(h)).add(e1.scale(radius * phi.cos())).add(e2.scale(radius * phi.sin())));
88 }
89 }
90 for h in [0.0, *height] {
91 for r in 0..nr {
92 let rr = radius * r as f64 / nr as f64;
93 let m = if r == 0 { 1 } else { na };
94 for i in 0..m {
95 let phi = std::f64::consts::TAU * i as f64 / m as f64;
96 out.push(base.add(a.scale(h)).add(e1.scale(rr * phi.cos())).add(e2.scale(rr * phi.sin())));
97 }
98 }
99 }
100 }
101 }
102 out
103 }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108#[serde(default)]
109pub struct MachineCheckOptions {
110 pub surface_spacing: f64,
112 pub max_step_deg: f64,
114 pub max_step_linear: f64,
116 pub max_work: u64,
118}
119
120impl Default for MachineCheckOptions {
121 fn default() -> Self {
122 Self { surface_spacing: 0.5, max_step_deg: 2.0, max_step_linear: 5.0, max_work: 2_000_000_000 }
123 }
124}
125
126impl MachineCheckOptions {
127 pub fn validate(&self) -> Result<(), CamError> {
128 let bad = |m: &str| Err(CamError::InvalidInput(format!("machine check: {m}")));
129 if !self.surface_spacing.is_finite() || self.surface_spacing <= 0.0 {
130 return bad("surface spacing must be positive");
131 }
132 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 {
133 return bad("steps must be positive");
134 }
135 if self.max_work == 0 {
136 return bad("work budget must be positive");
137 }
138 Ok(())
139 }
140}
141
142#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct MachineCollision {
146 pub block: usize,
149 pub fraction: f64,
151 pub angles_deg: Vec<f64>,
152 pub linear: Point3,
153 pub body: String,
155 pub against: String,
157 pub point: Point3,
159}
160
161#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct MachineCheckReport {
164 pub first: Option<MachineCollision>,
165 pub samples: u64,
166 pub work: u64,
168 pub bound: f64,
170}
171
172struct Placed<'a> {
173 solid: &'a LinkSolid,
174 to_world: RigidTransform,
175 to_local: RigidTransform,
176}
177
178fn is_tool_side(chain: &Chain, link: Link) -> bool {
179 match link {
180 Link::Tool => true,
181 Link::Joint { index } => chain.joints.get(index).map_or(false, |j| j.carrier == crate::kinematics::Carrier::Head),
182 Link::Base | Link::Part => false,
183 }
184}
185
186pub fn check_machine(
188 chain: &Chain,
189 model: &MachineModel,
190 blocks: &[ResolvedMotion],
191 stock: Option<&TriDexel>,
192 opts: &MachineCheckOptions,
193 runner: &mut Runner<'_>,
194) -> Result<MachineCheckReport, CamError> {
195 opts.validate()?;
196 for s in &model.solids {
197 s.shape.validate().map_err(CamError::InvalidInput)?;
198 if let Link::Joint { index } = s.link {
199 if index >= chain.joints.len() {
200 return Err(CamError::InvalidInput(format!("machine solid {} sits on joint {index}, which the chain does not have", s.name)));
201 }
202 }
203 }
204 let samples: Vec<Vec<Point3>> = model.solids.iter().map(|s| s.shape.surface_samples(opts.surface_spacing)).collect();
205 let tool_side: Vec<usize> = (0..model.solids.len()).filter(|&i| is_tool_side(chain, model.solids[i].link)).collect();
206 let part_side: Vec<usize> = (0..model.solids.len()).filter(|&i| !is_tool_side(chain, model.solids[i].link)).collect();
207 let mut report = MachineCheckReport { bound: opts.surface_spacing, ..Default::default() };
208 let mut work_left = opts.max_work;
209 let n_rot = chain.joints.len();
210 let mut prev: Option<(Point3, Vec<f64>)> = None;
211 let mut tip: Option<Point3> = None;
212 let off = KinematicsOptions { tcp: false, ..Default::default() };
213 runner.stage("machine-check", "Machine collision check", 0, blocks.len() as u64)?;
214 for (bi, b) in blocks.iter().enumerate() {
215 runner.work(1, "machine-check", bi as u64, blocks.len() as u64)?;
216 let angles: Vec<f64> = b.rotary.iter().map(|w| w.angle_deg).collect();
217 if angles.len() != n_rot {
218 return Err(CamError::InvalidInput(format!("block {bi} carries {} rotary words, the chain has {n_rot} joints", angles.len())));
219 }
220 if let Some(t) = b.motion.end() {
224 tip = Some(t);
225 }
226 let Some(t) = tip else { continue };
227 let linear = chain.linear_for(t, &angles, &off);
228 let (steps, from) = match &prev {
229 None => (1usize, (linear, angles.clone())),
230 Some((pl, pa)) => {
231 let rot = pa.iter().zip(&angles).map(|(a, c)| (c - a).abs()).fold(0.0, f64::max);
232 let lin = pl.distance(linear);
233 let n = ((rot / opts.max_step_deg).ceil().max((lin / opts.max_step_linear).ceil()) as usize).max(1);
234 (n, (*pl, pa.clone()))
235 }
236 };
237 let start_s = if prev.is_none() { 0 } else { 1 };
238 for s in start_s..=steps {
239 let f = s as f64 / steps as f64;
240 let lin = from.0.lerp(linear, f);
241 let ang: Vec<f64> = from.1.iter().zip(&angles).map(|(a, c)| a + (c - a) * f).collect();
242 report.samples += 1;
243 let placed: Vec<Placed<'_>> = model
244 .solids
245 .iter()
246 .map(|sld| {
247 let t = chain.link_transform(sld.link, &ang, lin)?;
248 Ok(Placed { solid: sld, to_local: t.inverse(), to_world: t })
249 })
250 .collect::<Result<_, crate::kinematics::KinematicsError>>()?;
251 let mut hit: Option<MachineCollision> = None;
252 'pairs: for &ti in &tool_side {
253 for &pi in &part_side {
254 for (a, bb) in [(ti, pi), (pi, ti)] {
255 let cost = samples[a].len() as u64;
256 if work_left < cost {
257 return Err(CamError::InvalidInput("machine check: work budget exceeded".into()));
258 }
259 work_left -= cost;
260 report.work += cost;
261 for p in &samples[a] {
262 let w = placed[a].to_world.apply_point(*p);
263 let q = placed[bb].to_local.apply_point(w);
264 if placed[bb].solid.shape.contains_local(q) {
265 hit = Some(MachineCollision {
266 block: bi,
267 fraction: f,
268 angles_deg: ang.clone(),
269 linear: lin,
270 body: model.solids[ti].name.clone(),
271 against: model.solids[pi].name.clone(),
272 point: w,
273 });
274 break 'pairs;
275 }
276 }
277 }
278 }
279 if let Some(stock) = stock {
280 let cost = samples[ti].len() as u64;
281 if work_left < cost {
282 return Err(CamError::InvalidInput("machine check: work budget exceeded".into()));
283 }
284 work_left -= cost;
285 report.work += cost;
286 for p in &samples[ti] {
287 let w = placed[ti].to_world.apply_point(*p);
288 let in_part = chain.world_to_part(&ang, w);
289 if stock.contains(in_part) {
290 hit = Some(MachineCollision {
291 block: bi,
292 fraction: f,
293 angles_deg: ang.clone(),
294 linear: lin,
295 body: model.solids[ti].name.clone(),
296 against: "stock".into(),
297 point: w,
298 });
299 break 'pairs;
300 }
301 }
302 }
303 }
304 if hit.is_some() {
305 report.first = hit;
306 return Ok(report);
307 }
308 }
309 prev = Some((linear, angles));
310 }
311 Ok(report)
312}