use super::envelope::StockEnvelope;
use super::removal::{simulate, simulate_on_machine, RemovalOptions};
use super::{DexelGrid, DexelOptions, StockSimError};
use crate::cancel::{Cancelled, Runner};
use crate::cutter::{Cutter, CutterError};
use crate::kinematics::Chain;
use crate::model::{Machine, Operation, RigidTransform, Setup, Tool, Toolpath};
use crate::numeric::Tolerance;
use crate::stock::ResolvedStock;
#[derive(Clone, Copy, Debug)]
pub struct PlannedOp<'a> {
pub operation: &'a Operation,
pub tool: &'a Tool,
pub toolpath: Option<&'a Toolpath>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PreOperationStock {
pub grid: DexelGrid,
pub envelope: StockEnvelope,
pub cached_ops: usize,
pub replayed_ops: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PreOpStockError {
Ungenerated { operation: String },
Unsimulatable { operation: String, motions: Vec<usize>, reason: String },
Cutter { operation: String, error: CutterError },
Kinematics(String),
Grid(StockSimError),
InvalidInput(String),
Cancelled,
}
impl std::fmt::Display for PreOpStockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PreOpStockError::Ungenerated { operation } => {
write!(f, "operation {operation:?} before this one has no usable toolpath; generate it first")
}
PreOpStockError::Unsimulatable { operation, reason, .. } => {
write!(f, "operation {operation:?} before this one cannot be simulated: {reason}")
}
PreOpStockError::Cutter { operation, error } => write!(f, "operation {operation:?}: {error}"),
PreOpStockError::Kinematics(m) => write!(f, "machine kinematics: {m}"),
PreOpStockError::Grid(e) => write!(f, "{e}"),
PreOpStockError::InvalidInput(m) => write!(f, "{m}"),
PreOpStockError::Cancelled => write!(f, "cancelled"),
}
}
}
impl std::error::Error for PreOpStockError {}
impl From<Cancelled> for PreOpStockError {
fn from(_: Cancelled) -> Self {
PreOpStockError::Cancelled
}
}
type Key = [u64; 2];
fn hash(parts: &[&[u8]]) -> Key {
let mut out = [0xcbf2_9ce4_8422_2325_u64, 0x6c62_272e_07bb_0142_u64];
for h in out.iter_mut() {
for part in parts {
for &b in part.iter() {
*h ^= b as u64;
*h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
*h ^= 0xff;
*h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
out
}
#[derive(Clone, Debug)]
struct Entry {
base: Key,
ops: Vec<Key>,
grid: DexelGrid,
}
#[derive(Clone, Debug, Default)]
pub struct PreOpStockCache {
max_entries: usize,
entries: Vec<Entry>,
}
impl PreOpStockCache {
pub fn new(max_entries: usize) -> Self {
Self { max_entries: max_entries.max(1), entries: Vec::new() }
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
#[allow(clippy::too_many_arguments)]
pub fn stock_before(
&mut self,
setup: &Setup,
machine: Option<&Machine>,
initial: &ResolvedStock,
rest: Option<&DexelGrid>,
options: &DexelOptions,
ops: &[PlannedOp<'_>],
n: usize,
tol: &Tolerance,
removal: &RemovalOptions,
runner: &mut Runner<'_>,
) -> Result<PreOperationStock, PreOpStockError> {
if n > ops.len() {
return Err(PreOpStockError::InvalidInput(format!("operation index {n} is past the setup's {} operations", ops.len())));
}
if let Some(foreign) = ops[..n].iter().find(|o| o.operation.setup != setup.id) {
return Err(PreOpStockError::InvalidInput(format!(
"operation {:?} belongs to setup {:?}, not {:?}",
foreign.operation.id.as_str(),
foreign.operation.setup.as_str(),
setup.id.as_str()
)));
}
let chain = match machine {
Some(m) => Some(Chain::from_machine(m, setup.wcs.origin).map_err(|e| PreOpStockError::Kinematics(format!("{e}")))?),
None => None,
};
let rest_json = rest.map(|g| serde_json::to_vec(g).unwrap_or_default()).unwrap_or_default();
let base = hash(&[
format!("{initial:?}").as_bytes(),
format!("{options:?}").as_bytes(),
format!("{machine:?}").as_bytes(),
format!("{:?}{:?}", setup.model_to_machine, setup.wcs.origin).as_bytes(),
&rest_json,
format!("{removal:?}{tol:?}").as_bytes(),
]);
let keys: Vec<Key> = ops[..n]
.iter()
.map(|o| {
let tool = serde_json::to_vec(o.tool).unwrap_or_default();
let path = o.toolpath.map(|t| serde_json::to_vec(&t.motions).unwrap_or_default()).unwrap_or_default();
let enabled: &[u8] = if o.operation.enabled { b"on" } else { b"off" };
hash(&[enabled, &tool, &path])
})
.collect();
let best = self
.entries
.iter()
.filter(|e| e.base == base && e.ops.len() <= n && keys[..e.ops.len()] == e.ops[..])
.max_by_key(|e| e.ops.len());
let (mut grid, start) = match best {
Some(e) => (e.grid.clone(), e.ops.len()),
None => (DexelGrid::from_stock(initial, options, rest, None).map_err(PreOpStockError::Grid)?, 0),
};
if start == n && best.is_some() {
let envelope = StockEnvelope::new(&grid, RigidTransform::identity()).map_err(PreOpStockError::Grid)?;
return Ok(PreOperationStock { grid, envelope, cached_ops: n, replayed_ops: 0 });
}
for (k, o) in ops[start..n].iter().enumerate() {
runner.stage("pre-operation-stock", "replaying an earlier operation", k as u64, (n - start) as u64)?;
if !o.operation.enabled {
continue;
}
let path = match o.toolpath {
Some(t) if t.report.is_usable() => t,
_ => return Err(PreOpStockError::Ungenerated { operation: o.operation.id.as_str().to_string() }),
};
let cutter = Cutter::from_tool(o.tool).map_err(|error| PreOpStockError::Cutter { operation: o.operation.id.as_str().to_string(), error })?;
let report = match &chain {
Some(c) => simulate_on_machine(&mut grid, &cutter, &path.motions, tol, removal, c, runner)?,
None => simulate(&mut grid, &cutter, &path.motions, tol, removal, runner)?,
};
if !report.unsupported.is_empty() {
let reason = report
.warnings
.iter()
.find(|w| w.code == "motion-not-simulated")
.and_then(|w| w.detail.clone())
.unwrap_or_else(|| "motion not simulated".into());
return Err(PreOpStockError::Unsimulatable { operation: o.operation.id.as_str().to_string(), motions: report.unsupported, reason });
}
}
let replayed_ops = n - start;
self.entries.push(Entry { base, ops: keys, grid: grid.clone() });
while self.entries.len() > self.max_entries {
self.entries.remove(0);
}
let envelope = StockEnvelope::new(&grid, RigidTransform::identity()).map_err(PreOpStockError::Grid)?;
Ok(PreOperationStock { grid, envelope, cached_ops: start, replayed_ops })
}
}