use super::{Bounds3, MachineId, Point3, RigidTransform, SetupId, TargetRef};
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Setup {
pub id: SetupId,
#[serde(default)]
pub name: String,
#[serde(default)]
pub model_to_machine: RigidTransform,
#[serde(default)]
pub wcs: WorkOffset,
#[serde(default)]
pub stock: Stock,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turning_stick_out: Option<f64>,
#[serde(default)]
pub heights: Heights,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub machine: Option<MachineId>,
#[serde(default)]
pub targets: Vec<TargetRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub protected: Vec<TargetRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub program: Option<ProgramInfo>,
}
impl Setup {
pub fn new(id: impl Into<SetupId>, name: impl Into<String>) -> Self {
Self { id: id.into(), name: name.into(), ..Default::default() }
}
pub fn machine_to_model(&self) -> RigidTransform {
self.model_to_machine.inverse()
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct WorkOffset {
pub number: u32,
pub origin: Point3,
}
impl Default for WorkOffset {
fn default() -> Self {
Self { number: 1, origin: Point3::ZERO }
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgramInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<u32>,
#[serde(default)]
pub comment: String,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
#[non_exhaustive]
pub enum Stock {
BoundsOffset {
#[serde(default)]
x: f64,
#[serde(default)]
y: f64,
#[serde(default)]
top: f64,
#[serde(default)]
bottom: f64,
},
Box { min: Point3, max: Point3 },
Cylinder { base: Point3, axis: Point3, radius: f64, height: f64 },
FromSolid { solid: String },
FromRest { setup: SetupId },
}
impl Default for Stock {
fn default() -> Self {
Stock::BoundsOffset { x: 0.0, y: 0.0, top: 0.0, bottom: 0.0 }
}
}
impl Stock {
pub fn model_bounds(&self, target_bounds: &Bounds3) -> Option<Bounds3> {
match self {
Stock::BoundsOffset { x, y, top, bottom } => {
if target_bounds.is_empty() {
return None;
}
Some(Bounds3::new(
Point3::new(target_bounds.min.x - x, target_bounds.min.y - y, target_bounds.min.z - bottom),
Point3::new(target_bounds.max.x + x, target_bounds.max.y + y, target_bounds.max.z + top),
))
}
Stock::Box { min, max } => Some(Bounds3::new(*min, *max)),
Stock::Cylinder { base, axis, radius, height } => {
let a = axis.normalized()?;
let top = base.add(a.scale(*height));
let ext = Point3::new(
radius * (1.0 - a.x * a.x).max(0.0).sqrt(),
radius * (1.0 - a.y * a.y).max(0.0).sqrt(),
radius * (1.0 - a.z * a.z).max(0.0).sqrt(),
);
Some(Bounds3::new(base.min(top).sub(ext), base.max(top).add(ext)))
}
Stock::FromSolid { .. } | Stock::FromRest { .. } => None,
}
}
pub fn validate(&self) -> Result<(), String> {
match self {
Stock::BoundsOffset { x, y, top, bottom } => {
for (name, v) in [("x", x), ("y", y), ("top", top), ("bottom", bottom)] {
if !v.is_finite() {
return Err(format!("stock offset {name} is not finite"));
}
}
Ok(())
}
Stock::Box { min, max } => {
if !min.is_finite() || !max.is_finite() {
return Err("stock box has a non-finite corner".into());
}
if min.x >= max.x || min.y >= max.y || min.z >= max.z {
return Err("stock box must have positive size on every axis".into());
}
Ok(())
}
Stock::Cylinder { base, axis, radius, height } => {
if !base.is_finite() || !axis.is_finite() || !radius.is_finite() || !height.is_finite() {
return Err("stock cylinder has a non-finite parameter".into());
}
if axis.normalized().is_none() {
return Err("stock cylinder axis must be non-zero".into());
}
if *radius <= 0.0 || *height <= 0.0 {
return Err("stock cylinder radius and height must be positive".into());
}
Ok(())
}
Stock::FromSolid { solid } => {
if solid.is_empty() {
Err("stock body name is empty".into())
} else {
Ok(())
}
}
Stock::FromRest { setup } => {
if setup.as_str().is_empty() {
Err("rest stock setup id is empty".into())
} else {
Ok(())
}
}
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct Heights {
pub clearance: f64,
pub retract: f64,
pub feed: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub top: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bottom: Option<f64>,
}
impl Default for Heights {
fn default() -> Self {
Self { clearance: 15.0, retract: 5.0, feed: 2.0, top: None, bottom: None }
}
}
impl Heights {
pub fn validate(&self) -> Result<(), String> {
for (name, v) in [("clearance", self.clearance), ("retract", self.retract), ("feed", self.feed)] {
if !v.is_finite() {
return Err(format!("{name} height is not finite"));
}
}
if self.clearance < self.retract {
return Err("clearance height must not be below retract height".into());
}
if self.retract < self.feed {
return Err("retract height must not be below feed height".into());
}
if let (Some(top), Some(bottom)) = (self.top, self.bottom) {
if !top.is_finite() || !bottom.is_finite() {
return Err("top/bottom height is not finite".into());
}
if top < bottom {
return Err("top height must not be below bottom height".into());
}
}
Ok(())
}
}