use super::carry::carry_from_setup;
use super::preop::{PlannedOp, PreOpStockCache, PreOpStockError, PreOperationStock};
use super::removal::RemovalOptions;
use super::{DexelGrid, DexelOptions};
use crate::cancel::Runner;
use crate::mesh::MeshSnapshot;
use crate::model::{ApproximationLimit, Bounds3, Diagnostic, Machine, Operation, Setup, TargetRef, Tool, Toolpath};
use crate::numeric::Tolerance;
use crate::stock::{resolve_stock, ResolvedStock};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetupHistory {
pub setup: Setup,
#[serde(default)]
pub machine: Option<Machine>,
#[serde(default)]
pub operations: Vec<(Operation, Option<Toolpath>)>,
}
pub struct JobStock<'a> {
pub setup: &'a Setup,
pub machine: Option<&'a Machine>,
pub tools: &'a [Tool],
pub mesh: &'a MeshSnapshot,
pub fallback_targets: &'a [String],
pub earlier: &'a [(Operation, Option<Toolpath>)],
pub previous: &'a [SetupHistory],
pub tolerance: Tolerance,
}
#[derive(Clone, Debug, PartialEq)]
pub struct JobStockResult {
pub stock: PreOperationStock,
pub limits: Vec<ApproximationLimit>,
pub start: SetupStart,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetupStart {
#[serde(default)]
pub target_bounds: Option<Bounds3>,
#[serde(default)]
pub carried: Option<DexelGrid>,
}
pub fn job_dexel_options(mesh: &MeshSnapshot) -> DexelOptions {
let d = mesh.bounds.diagonal();
let cell = if d.is_finite() && d > 0.0 { (d / 500.0).clamp(0.05, 5.0) } else { 0.5 };
DexelOptions { cell, ..Default::default() }
}
pub fn uses_pre_op_stock(strategy: &crate::Strategy, machine: Option<&Machine>) -> bool {
use crate::Strategy as S;
if machine.is_some_and(|m| matches!(m.kind, crate::MachineKind::Lathe)) {
return false;
}
!matches!(
strategy,
S::ProbeWcs(_)
| S::ProbeGeometry(_)
| S::Pattern(_)
| S::ManualNc(_)
| S::Swarf(_)
| S::MultiAxisContour(_)
| S::RotaryWrap(_)
| S::RotaryFinish(_)
| S::MultiAxisFinish(_)
| S::TurnFace(_)
| S::TurnProfile(_)
| S::TurnGroove(_)
| S::TurnThread(_)
| S::TurnPartOff(_)
)
}
pub fn unavailable_warning(error: &PreOpStockError) -> Diagnostic {
Diagnostic::new(
"pre-operation-stock-unavailable",
format!("pre-operation stock unavailable: {error}; links keep above the stock top"),
)
}
fn target_bounds(mesh: &MeshSnapshot, frame: &Setup, targets: &[TargetRef]) -> Result<Bounds3, PreOpStockError> {
let inverse = frame.model_to_machine.inverse();
let mut bounds = Bounds3::empty();
for target in targets {
let faces = mesh.resolve_faces(target);
if faces.is_empty() {
return Err(PreOpStockError::InvalidInput(format!("stock target body {:?} is not in the job's snapshot", target.solid)));
}
for id in mesh.triangles_of_faces(&faces) {
let triangle = &mesh.triangles[id as usize];
if mesh.faces[triangle.face as usize].protected_only {
continue;
}
for p in triangle.vertices() {
bounds.include(inverse.apply_point(p));
}
}
}
Ok(bounds)
}
fn resolve(job: &JobStock<'_>, setup: &Setup, continues_from: Option<&Setup>) -> Result<(ResolvedStock, Option<Bounds3>), PreOpStockError> {
let targets: Vec<TargetRef> =
if setup.targets.is_empty() { job.fallback_targets.iter().map(TargetRef::solid).collect() } else { setup.targets.clone() };
let bounds = if matches!(setup.stock, crate::Stock::BoundsOffset { .. }) { Some(target_bounds(job.mesh, job.setup, &targets)?) } else { None };
let stock = resolve_stock(setup, &bounds.unwrap_or_else(Bounds3::empty), None)
.map_err(|e| PreOpStockError::InvalidInput(format!("setup {:?}: {e} (a stock body is not carried into pre-operation stock)", setup.id.as_str())))?;
if let ResolvedStock::Rest(name) = &stock {
match continues_from {
Some(p) if p.id.as_str() == name.as_str() => {}
_ => {
return Err(PreOpStockError::InvalidInput(format!(
"setup {:?} continues from setup {:?}, which the job does not carry",
setup.id.as_str(),
name.as_str()
)))
}
}
}
Ok((stock, bounds))
}
fn planned<'a>(ops: &'a [(Operation, Option<Toolpath>)], tools: &'a [Tool]) -> Result<Vec<PlannedOp<'a>>, PreOpStockError> {
ops.iter()
.map(|(operation, toolpath)| {
let tool = tools.iter().find(|t| t.id == operation.tool).ok_or_else(|| {
PreOpStockError::InvalidInput(format!("operation {:?}: its tool {:?} is not in the document", operation.id.as_str(), operation.tool.as_str()))
})?;
Ok(PlannedOp { operation, tool, toolpath: toolpath.as_ref() })
})
.collect()
}
pub fn job_stock(cache: &mut PreOpStockCache, job: &JobStock<'_>, runner: &mut Runner<'_>) -> Result<JobStockResult, PreOpStockError> {
let options = job_dexel_options(job.mesh);
let removal = RemovalOptions::default();
let mut carried: Option<DexelGrid> = None;
let mut limits = Vec::new();
let mut before: Option<&Setup> = None;
for (k, history) in job.previous.iter().enumerate() {
let next = job.previous.get(k + 1).map_or(job.setup, |h| &h.setup);
let (initial, _) = resolve(job, &history.setup, before)?;
let ops = planned(&history.operations, job.tools)?;
let c = carry_from_setup(cache, &history.setup, history.machine.as_ref(), &initial, carried.as_ref(), &options, &ops, &job.tolerance, &removal, runner, next)?;
limits.extend(c.limits);
carried = Some(c.grid);
before = Some(&history.setup);
}
let (initial, target_bounds) = resolve(job, job.setup, before)?;
let ops = planned(job.earlier, job.tools)?;
let stock = cache.stock_before(job.setup, job.machine, &initial, carried.as_ref(), &options, &ops, ops.len(), &job.tolerance, &removal, runner)?;
Ok(JobStockResult { stock, limits, start: SetupStart { target_bounds, carried } })
}