1use crate::mesh::MeshSnapshot;
5use crate::model::{Bounds3, Point3, RigidTransform, Setup, SetupId, Stock};
6
7#[derive(Clone, Debug, PartialEq)]
10pub struct StockBox {
11 pub bounds: Bounds3,
14 pub to_machine: RigidTransform,
16}
17
18pub fn bounds_offset_box(setup: &Setup, target_bounds_model: &Bounds3) -> Option<StockBox> {
30 let Stock::BoundsOffset { x, y, top, bottom } = &setup.stock else { return None };
31 if target_bounds_model.is_empty() {
32 return None;
33 }
34 let r = &setup.model_to_machine.rotation;
35 let mut min = [target_bounds_model.min.x, target_bounds_model.min.y, target_bounds_model.min.z];
36 let mut max = [target_bounds_model.max.x, target_bounds_model.max.y, target_bounds_model.max.z];
37 let offsets = [(*x, *x), (*y, *y), (*bottom, *top)];
39 let mut used = [false; 3];
40 for machine_axis in [2usize, 0, 1] {
42 let (lo, hi) = offsets[machine_axis];
43 let model_axis = (0..3)
45 .filter(|i| !used[*i])
46 .max_by(|a, b| r[machine_axis][*a].abs().partial_cmp(&r[machine_axis][*b].abs()).unwrap_or(std::cmp::Ordering::Equal))
47 .unwrap_or(machine_axis);
48 used[model_axis] = true;
49 if r[machine_axis][model_axis] >= 0.0 {
50 min[model_axis] -= lo;
51 max[model_axis] += hi;
52 } else {
53 min[model_axis] -= hi;
54 max[model_axis] += lo;
55 }
56 }
57 Some(StockBox { bounds: Bounds3::new(Point3::new(min[0], min[1], min[2]), Point3::new(max[0], max[1], max[2])), to_machine: setup.model_to_machine })
58}
59
60impl StockBox {
61 pub fn machine_bounds(&self) -> Bounds3 {
63 self.to_machine.apply_bounds(&self.bounds)
64 }
65
66 pub fn contains(&self, machine_point: Point3) -> bool {
68 self.bounds.contains_point(self.to_machine.inverse().apply_point(machine_point))
69 }
70
71 pub fn volume(&self) -> f64 {
72 let s = self.bounds.size();
73 if self.bounds.is_empty() {
74 0.0
75 } else {
76 s.x * s.y * s.z
77 }
78 }
79}
80
81#[derive(Clone, Debug, PartialEq)]
83pub struct StockCylinder {
84 pub base: Point3,
85 pub axis: Point3,
87 pub radius: f64,
88 pub height: f64,
89}
90
91impl StockCylinder {
92 pub fn machine_bounds(&self) -> Bounds3 {
93 let a = self.axis;
94 let top = self.base.add(a.scale(self.height));
95 let ext = Point3::new(
96 self.radius * (1.0 - a.x * a.x).max(0.0).sqrt(),
97 self.radius * (1.0 - a.y * a.y).max(0.0).sqrt(),
98 self.radius * (1.0 - a.z * a.z).max(0.0).sqrt(),
99 );
100 Bounds3::new(self.base.min(top).sub(ext), self.base.max(top).add(ext))
101 }
102
103 pub fn contains(&self, machine_point: Point3) -> bool {
104 let d = machine_point.sub(self.base);
105 let along = d.dot(self.axis);
106 if along < 0.0 || along > self.height {
107 return false;
108 }
109 let radial = d.sub(self.axis.scale(along)).length();
110 radial <= self.radius
111 }
112
113 pub fn volume(&self) -> f64 {
114 std::f64::consts::PI * self.radius * self.radius * self.height
115 }
116}
117
118#[derive(Clone, Debug, PartialEq)]
120pub enum ResolvedStock {
121 Box(StockBox),
122 Cylinder(StockCylinder),
123 Mesh(MeshSnapshot),
125 Rest(SetupId),
127}
128
129impl ResolvedStock {
130 pub fn machine_bounds(&self) -> Option<Bounds3> {
132 match self {
133 ResolvedStock::Box(b) => Some(b.machine_bounds()),
134 ResolvedStock::Cylinder(c) => Some(c.machine_bounds()),
135 ResolvedStock::Mesh(m) => Some(m.bounds),
136 ResolvedStock::Rest(_) => None,
137 }
138 }
139
140 pub fn contains(&self, machine_point: Point3) -> Option<bool> {
142 match self {
143 ResolvedStock::Box(b) => Some(b.contains(machine_point)),
144 ResolvedStock::Cylinder(c) => Some(c.contains(machine_point)),
145 ResolvedStock::Mesh(_) | ResolvedStock::Rest(_) => None,
146 }
147 }
148
149 pub fn top_z(&self) -> Option<f64> {
151 self.machine_bounds().map(|b| b.max.z)
152 }
153}
154
155#[derive(Clone, Debug, PartialEq, Eq)]
157pub enum StockError {
158 Invalid(String),
160 NoTargets,
162 MissingSolid(String),
164}
165
166impl std::fmt::Display for StockError {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 match self {
169 StockError::Invalid(m) => write!(f, "stock: {m}"),
170 StockError::NoTargets => write!(f, "stock: the setup has no target geometry to size the stock from"),
171 StockError::MissingSolid(s) => write!(f, "stock: body {s:?} was not supplied"),
172 }
173 }
174}
175
176impl std::error::Error for StockError {}
177
178pub fn resolve_stock(
183 setup: &Setup,
184 target_bounds_model: &Bounds3,
185 from_solid: Option<&MeshSnapshot>,
186) -> Result<ResolvedStock, StockError> {
187 setup.stock.validate().map_err(StockError::Invalid)?;
188 let t = &setup.model_to_machine;
189 match &setup.stock {
190 Stock::BoundsOffset { .. } => Ok(ResolvedStock::Box(bounds_offset_box(setup, target_bounds_model).ok_or(StockError::NoTargets)?)),
191 Stock::Box { min, max } => Ok(ResolvedStock::Box(StockBox { bounds: Bounds3::new(*min, *max), to_machine: *t })),
192 Stock::Cylinder { base, axis, radius, height } => {
193 let axis = axis.normalized().ok_or_else(|| StockError::Invalid("cylinder axis is zero".into()))?;
194 Ok(ResolvedStock::Cylinder(StockCylinder {
195 base: t.apply_point(*base),
196 axis: t.apply_vector(axis),
197 radius: *radius,
198 height: *height,
199 }))
200 }
201 Stock::FromSolid { solid } => {
202 let mesh = from_solid.ok_or_else(|| StockError::MissingSolid(solid.clone()))?;
203 Ok(ResolvedStock::Mesh(crate::mesh::to_machine(setup, mesh)))
204 }
205 Stock::FromRest { setup: previous } => Ok(ResolvedStock::Rest(previous.clone())),
206 }
207}
208