1use crate::cutter::{Cutter, CutterError};
19use crate::error::CamError;
20use crate::holes::Hole;
21use crate::index::TriangleIndex;
22use crate::mesh::MeshSnapshot;
23use crate::model::{FeedsSpeeds, Heights, Operation, Setup, Strategy, TargetRef, Tool, Toolpath};
24use crate::numeric::Tolerance;
25use crate::planar::extract::{face_outline, silhouette};
26use crate::planar::machining::{PlanarInput, PlanarRegion};
27use crate::Runner;
28
29pub mod chains;
30
31pub struct GenerateInput<'a> {
33 pub operation: &'a Operation,
34 pub setup: &'a Setup,
35 pub tools: &'a [Tool],
37 pub mesh: &'a MeshSnapshot,
38 pub index: Option<&'a dyn TriangleIndex>,
39 pub tolerance: Tolerance,
40 pub holes: &'a [Hole],
42}
43
44fn invalid(message: impl Into<String>) -> CamError {
45 CamError::InvalidInput(message.into())
46}
47
48fn selected_holes<'a>(operation: &Operation, holes: &'a [Hole]) -> Result<std::borrow::Cow<'a, [Hole]>, CamError> {
52 let picks = &operation.targets.drive_faces;
53 if picks.is_empty() { return Ok(std::borrow::Cow::Borrowed(holes)); }
54 let matches = |pick: &TargetRef, hole: &Hole| pick.solid == hole.solid
55 && pick.face.as_ref().is_some_and(|face| hole.faces.contains(face));
56 for pick in picks {
57 if !holes.iter().any(|hole| matches(pick, hole)) {
58 return Err(invalid(format!("{}: picked face {}/{} is not a recognised hole", operation.id, pick.solid, pick.face.as_deref().unwrap_or("<body>"))));
59 }
60 }
61 Ok(std::borrow::Cow::Owned(holes.iter().filter(|hole| picks.iter().any(|pick| matches(pick, hole))).cloned().collect()))
62}
63
64pub fn tool_of<'a>(operation: &Operation, tools: &'a [Tool]) -> Result<&'a Tool, CamError> {
66 tools
67 .iter()
68 .find(|t| t.id == operation.tool)
69 .ok_or_else(|| invalid(format!("operation {} names tool `{}`, which the document does not have", operation.id, operation.tool)))
70}
71
72pub fn heights_of(operation: &Operation, setup: &Setup) -> Heights {
74 operation.heights.clone().unwrap_or_else(|| setup.heights.clone())
75}
76
77pub fn feeds_of(operation: &Operation, tool: &Tool) -> FeedsSpeeds {
79 operation.feeds.clone().unwrap_or_else(|| tool.feeds.clone())
80}
81
82fn cutter_of(tool: &Tool) -> Result<Cutter, CamError> {
83 Cutter::from_tool(tool).map_err(|e: CutterError| CamError::Cutter(e))
84}
85
86pub fn planar_input(input: &GenerateInput<'_>) -> Result<PlanarInput, CamError> {
92 let op = input.operation;
93 let mesh = input.mesh;
94 let all: Vec<u32> = (0..mesh.triangle_count() as u32).collect();
95 let bounds = mesh.bounds_of(&all);
96 let mut regions = Vec::new();
97 let faces: Vec<TargetRef> = if op.targets.drive_faces.is_empty() {
98 op.targets.target_solids.iter().map(|s| TargetRef::solid(s.clone())).collect()
99 } else {
100 op.targets.drive_faces.clone()
101 };
102 for target in &faces {
103 let face_ids = mesh.resolve_faces(target);
104 if face_ids.is_empty() {
105 return Err(CamError::UnresolvedTarget { solid: target.solid.clone(), face: target.face.clone() });
106 }
107 let what = target.face.as_deref().unwrap_or(&target.solid).to_string();
108 let (region, z) = if target.is_whole_body() {
109 let region = silhouette(mesh, |f| face_ids.contains(&f), &input.tolerance)
110 .map_err(|e| invalid(format!("{}: the silhouette of {what} could not be built: {e:?}", op.id)))?;
111 let triangles = mesh.triangles_of_faces(&face_ids);
112 (region, mesh.bounds_of(&triangles).max.z)
113 } else {
114 let (region, z_range) = face_outline(mesh, |f| face_ids.contains(&f), &input.tolerance)
115 .map_err(|e| invalid(format!("{}: the outline of {what} could not be built: {e:?}", op.id)))?;
116 if matches!(op.strategy,Strategy::Slot(_)|Strategy::Chamfer2D(_)) && z_range[1]-z_range[0]>input.tolerance.distance_eps {
117 return Err(invalid(format!("{}: selected face must be horizontal",op.strategy.kind_name())));
118 }
119 (region, z_range[1])
120 };
121 if region.loops.is_empty() {
122 continue;
123 }
124 regions.push(PlanarRegion { region, z, upward: true });
125 }
126 if regions.is_empty() {
127 return Err(CamError::EmptyTarget { operation_id: op.id.as_str().to_string() });
128 }
129 let chains = if let Strategy::Slot(params)=&op.strategy {
132 if op.targets.drive_faces.is_empty() {return Err(invalid("slot: select a horizontal rectangular slot floor"));}
133 regions.iter().map(|r|slot_floor_chain(r,params.width,&input.tolerance)).collect::<Result<Vec<_>,_>>()?
134 } else if matches!(op.strategy, Strategy::Contour2D(_)|Strategy::Chamfer2D(_)) {
135 regions.iter().flat_map(|r| r.region.loops.iter().map(move |l|
136 crate::planar::machining::PlanarChain { chain: crate::planar::Chain::from_loop(l), z: r.z }
137 )).collect()
138 } else { Vec::new() };
139 Ok(PlanarInput { regions, chains, circles: Vec::new(), stock_top: Some(bounds.max.z) })
140}
141
142fn planar_for<'a>(input:&GenerateInput<'_>,context:&GenerateContext<'a>)->Result<std::borrow::Cow<'a,PlanarInput>,CamError> {
144 if let Some((_,geometry))=context.resolved_planar {
145 return Ok(std::borrow::Cow::Borrowed(geometry));
146 }
147 let geometry=if matches!(input.operation.strategy,Strategy::Contour2D(_)|Strategy::Slot(_)|Strategy::Trace(_)|Strategy::Chamfer2D(_)|Strategy::Engrave(_)) {chains::selected(input,context)?}else{planar_input(input)?};
148 Ok(std::borrow::Cow::Owned(geometry))
149}
150
151fn slot_floor_chain(region: &PlanarRegion, width: f64, tol: &Tolerance) -> Result<crate::planar::machining::PlanarChain, CamError> {
154 use crate::planar::{Chain, Segment, Vec2};
155 use crate::Point2;
156 let fail = || invalid("slot: selected floor must be a horizontal rectangle matching slot width");
157 if region.region.loops.len()!=1 || !width.is_finite() || width<=0.0 { return Err(fail()); }
158 let l=®ion.region.loops[0];
159 if l.segments.is_empty() || l.segments.iter().any(|s|s.arc.is_some()) { return Err(fail()); }
160 let edge=l.segments.iter().max_by(|a,b|a.start.distance(a.end).total_cmp(&b.start.distance(b.end))).unwrap();
161 let length=edge.start.distance(edge.end);
162 if length<=tol.point_eps { return Err(fail()); }
163 let u=Point2::new((edge.end.x-edge.start.x)/length,(edge.end.y-edge.start.y)/length);
164 let v=Point2::new(-u.y,u.x);
165 let project=|p:Point2| { let d=p.sub(edge.start); (d.dot(u),d.dot(v)) };
166 let mut bounds=[f64::INFINITY,f64::NEG_INFINITY,f64::INFINITY,f64::NEG_INFINITY];
167 for s in &l.segments { let (x,y)=project(s.start); bounds[0]=bounds[0].min(x);bounds[1]=bounds[1].max(x);bounds[2]=bounds[2].min(y);bounds[3]=bounds[3].max(y); }
168 let [x0,x1,y0,y1]=bounds;
169 let dx=x1-x0;let dy=y1-y0;let eps=tol.distance_eps;
170 if dx<=width+eps || (dy-width).abs()>eps { return Err(fail()); }
171 for s in &l.segments {
172 let (ax,ay)=project(s.start);let (bx,by)=project(s.end);
173 let vertical=(ax-bx).abs()<=eps && ((ax-x0).abs()<=eps || (ax-x1).abs()<=eps);
174 let horizontal=(ay-by).abs()<=eps && ((ay-y0).abs()<=eps || (ay-y1).abs()<=eps);
175 if !vertical && !horizontal {return Err(fail());}
176 }
177 if (l.signed_area().abs()-dx*dy).abs()>eps*(dx+dy) {return Err(fail());}
178 let point=|x:f64|Point2::new(edge.start.x+u.x*x+v.x*(y0+y1)*0.5,edge.start.y+u.y*x+v.y*(y0+y1)*0.5);
179 Ok(crate::planar::machining::PlanarChain {chain:Chain::open(vec![Segment::line(point(x0+width*0.5),point(x1-width*0.5))]),z:region.z})
180}
181
182#[derive(Default)]
185pub struct GenerateContext<'a> {
186 pub resolved_planar: Option<(&'a crate::OperationId, &'a PlanarInput)>,
188 pub chain_geometry: Option<&'a dyn chains::ChainGeometry>,
189 pub machine: Option<&'a crate::Machine>,
190 pub flow_faces: &'a [&'a dyn crate::flow::ParametricFace],
191 pub source: Option<&'a crate::program::OperationInput>,
193 pub source_operation: Option<&'a Operation>,
196 pub available: &'a [crate::program::OperationInput],
198 pub rest_stock: Option<&'a crate::DexelGrid>,
199 pub rough_stock: Option<&'a crate::stock::ResolvedStock>,
201 pub rough_reference_tool: Option<&'a crate::ToolId>,
204 pub turning: Option<&'a TurningInput<'a>>,
205 pub mill_turn: Option<&'a MillTurnInput<'a>>,
206 pub pre_op_stock: Option<&'a crate::StockEnvelope>,
210}
211
212pub struct TurningInput<'a> {
216 pub tool_id: &'a crate::ToolId,
217 pub tool_geometry: &'a crate::turning::TurningGeometry,
218 pub carriers: &'a [crate::turning::profile::AxialFace],
219 pub profile: Option<&'a crate::turning::profile::TurningProfile>,
220 pub contour: Option<&'a crate::turning::profile_path::ProfileInput>,
221 pub stick_out: f64,
222 pub groove_center_z: Option<f64>,
223 pub thread_radius: Option<f64>,
224}
225
226pub enum MillTurnInput<'a> {
230 IndexedDrill { hole: &'a Hole, indexed_mesh: &'a MeshSnapshot },
231 Polar { contour: &'a crate::planar::Chain, z:f64, part:&'a MeshSnapshot, fixed:&'a MeshSnapshot },
232 Cylindrical { contour: &'a crate::planar::Chain, radius:f64, part:&'a MeshSnapshot, fixed:&'a MeshSnapshot },
233}
234
235fn refused(input:&GenerateInput<'_>,why:&str)->CamError {
236 invalid(format!("{}: {}: {why}",input.operation.id,input.operation.strategy.kind_name()))
237}
238fn resolved_machine<'a>(input:&GenerateInput<'_>,context:&GenerateContext<'a>)->Result<Option<&'a crate::Machine>,CamError> {
239 match (&input.setup.machine,context.machine) {
240 (Some(id),Some(machine)) if *id==machine.id=>Ok(Some(machine)),
241 (None,None)=>Ok(None),
242 (Some(_),None)=>Err(refused(input,"setup machine must be resolved in GenerateContext")),
243 _=>Err(refused(input,"resolved machine identity does not match the setup")),
244 }
245}
246fn machine_chain(input:&GenerateInput<'_>,context:&GenerateContext<'_>)->Result<Option<crate::kinematics::Chain>,CamError> {
249 Ok(resolved_machine(input,context)?.map(|m|crate::kinematics::Chain::from_machine(m,crate::Point3::ZERO)).transpose()?)
250}
251fn validated_inner_path<'a>(input:&GenerateInput<'_>,context:&'a GenerateContext<'_>)->Result<&'a Toolpath,CamError> {
252 let s=context.source.ok_or_else(||refused(input,"requires an explicit resolved source operation/path in GenerateContext"))?;
253 let op=input.operation;
254 if !s.operation.enabled || s.operation.id==op.id || s.operation.setup!=op.setup || s.operation.tool!=op.tool || s.tool.id!=op.tool || s.toolpath.operation!=s.operation.id || s.toolpath.tool!=s.tool.id || s.operation.targets!=op.targets || !s.toolpath.report.is_usable() || s.toolpath.motions.is_empty() || s.tool!=*tool_of(op,input.tools)? || !s.toolpath.motions.iter().any(|m|m.kind.is_cutting()) || s.toolpath.motions.iter().any(|m|!m.kind.is_finite()) {
255 return Err(refused(input,"source must be a distinct enabled operation with matching setup, tool, targets and usable path"));
256 }
257 if s.toolpath.motions.iter().any(|m|!matches!(m.kind,crate::MotionKind::Rapid{..}|crate::MotionKind::Cut{..}|crate::MotionKind::Plunge{..}|crate::MotionKind::Retract{..}|crate::MotionKind::LeadIn{..}|crate::MotionKind::LeadOut{..}|crate::MotionKind::Link{..}|crate::MotionKind::Comment{..})) {
258 return Err(refused(input,"source must contain linear machining moves; arcs, cycles, probes and machine-control motions cannot be reinterpreted"));
259 }
260 Ok(&s.toolpath)
261}
262
263fn inner_path<'a>(input:&GenerateInput<'_>,context:&'a GenerateContext<'_>,runner:&mut Runner<'_>)->Result<std::borrow::Cow<'a,Toolpath>,CamError> {
266 use std::borrow::Cow;
267 let Some(source)=context.source_operation else {return validated_inner_path(input,context).map(Cow::Borrowed);};
268 if context.source.is_some() {return Err(refused(input,"supply either a source operation or a resolved source path, not both"));}
269 let op=input.operation;
270 if !source.enabled || source.id==op.id || source.setup!=op.setup || source.tool!=op.tool || source.targets!=op.targets {
271 return Err(refused(input,"source must be a distinct enabled operation with matching setup, tool, targets and usable path"));
272 }
273 if !matches!(source.strategy,Strategy::Parallel(_)|Strategy::Scallop(_)|Strategy::Pencil(_)|Strategy::Radial(_)|Strategy::Spiral(_)|Strategy::MorphedSpiral(_)|Strategy::Project(_)|Strategy::Morph(_)|Strategy::Contour3D(_)|Strategy::SteepShallow(_)|Strategy::Horizontal(_)|Strategy::Ramp(_)) {
274 return Err(refused(input,"source operation must be a self-contained three-axis finishing strategy"));
275 }
276 let source_input=GenerateInput {operation:source,..*input};
277 let path=generate(&source_input,runner).map_err(|e| match e {CamError::Cancelled=>CamError::Cancelled,e=>refused(input,&format!("source operation {} ({}) failed: {e}",source.id,source.strategy.kind_name()))})?;
279 let generated=crate::program::OperationInput {operation:source.clone(),tool:tool_of(source,input.tools)?.clone(),toolpath:path};
280 let resolved=GenerateContext {source:Some(&generated),..Default::default()};
281 validated_inner_path(input,&resolved)?;
282 Ok(Cow::Owned(generated.toolpath))
283}
284
285fn rough_stock(input:&GenerateInput<'_>,context:&GenerateContext<'_>)->Result<crate::stock::ResolvedStock,CamError> {
288 input.setup.model_to_machine.validate().map_err(|e|refused(input,&format!("invalid stock transform: {e}")))?;
289 if let Some(stock)=context.rough_stock {return Ok(stock.clone());}
290 let mut bounds=crate::Bounds3::empty();
291 let inverse=input.setup.model_to_machine.inverse();
292 let targets=if input.setup.targets.is_empty() {input.operation.targets.target_solids.iter().map(TargetRef::solid).collect()} else {input.setup.targets.clone()};
293 for target in targets.into_iter().filter(|_|matches!(input.setup.stock,crate::Stock::BoundsOffset{..})) {
294 let faces=input.mesh.resolve_faces(&target);
295 if faces.is_empty() {return Err(CamError::UnresolvedTarget {solid:target.solid,face:target.face});}
296 for id in input.mesh.triangles_of_faces(&faces) {
297 let triangle=&input.mesh.triangles[id as usize];
298 if input.mesh.faces[triangle.face as usize].protected_only {continue;}
299 for p in triangle.vertices() {bounds.include(inverse.apply_point(p));}
300 }
301 }
302 crate::stock::resolve_stock(input.setup,&bounds,None).map_err(|e|refused(input,&format!("stock resolution: {e}; FromSolid needs rough_stock in GenerateContext")))
303}
304
305fn roughing_path(input:&GenerateInput<'_>,context:&GenerateContext<'_>,tool:&Tool,runner:&mut Runner<'_>)->Result<Toolpath,CamError> {
306 use crate::{RestSource,Stock};
307 let op=input.operation;let setup=input.setup;let mesh=input.mesh;let index=input.index;let tol=input.tolerance;
308 let (rest,source)=match &op.strategy {
309 Strategy::Adaptive2D(p)=>(p.rest_machining,p.rest_source),
310 Strategy::Adaptive3D(p)=>(p.rest_machining,p.rest_source),
311 Strategy::Pocket3D(p)=>(p.rest_machining,p.rest_source),
312 _=>return Err(refused(input,"not a roughing strategy")),
313 };
314 let grid=if rest && source==RestSource::Stock {Some(context.rest_stock.ok_or_else(||refused(input,"rest machining requires simulated stock in GenerateContext.rest_stock"))?)} else {None};
315 if rest && source==RestSource::ReferenceTool && !matches!(op.strategy,Strategy::Pocket3D(_)) {
316 return Err(refused(input,"reference-tool rest is supported by Pocket3D; adaptive rest requires RestSource::Stock"));
317 }
318 if matches!(setup.stock,Stock::FromRest{..}) && grid.is_none() {
319 return Err(refused(input,"FromRest setup stock requires rest_machining with RestSource::Stock and a simulated grid"));
320 }
321 if matches!(op.strategy,Strategy::Adaptive2D(_)) {
322 let mut planar=planar_input(input)?;
323 let stock=rough_stock(input,context)?;
324 planar.stock_top=grid.map(|g|g.z_range[1]).or_else(||stock.top_z());
325 return crate::adaptive2d::generate(op,setup,tool,mesh,index,&planar,grid,tol,runner);
326 }
327 let stock=rough_stock(input,context)?;
328 match &op.strategy {
329 Strategy::Adaptive3D(_)=>crate::adaptive3d::generate(op,setup,tool,mesh,index,&stock,grid,tol,runner),
330 Strategy::Pocket3D(_) if rest && source==RestSource::ReferenceTool=>{
331 let id=context.rough_reference_tool.ok_or_else(||refused(input,"reference-tool rest requires explicit rough_reference_tool in GenerateContext"))?;
332 let reference=input.tools.iter().find(|t|t.id==*id).ok_or_else(||refused(input,"roughing reference tool is not in the document"))?;
333 let cutter=cutter_of(reference)?;
334 crate::rest_rough::generate(op,setup,tool,mesh,index,&stock,crate::rest_rough::RestInput::ReferenceTool(&cutter),tol,runner)
335 },
336 Strategy::Pocket3D(_)=>crate::pocket3d::generate(op,setup,tool,mesh,index,&stock,grid,tol,runner),
337 _=>Err(refused(input,"not a 3D roughing strategy")),
338 }
339}
340
341pub fn generate_expanded(input:&GenerateInput<'_>,context:&GenerateContext<'_>,runner:&mut Runner<'_>)->Result<Vec<crate::program::SetupInput>,CamError> {
345 if let Strategy::Pattern(p)=&input.operation.strategy {
346 let output=if matches!(p.kind,crate::PatternKind::WcsOffsets{..}) {
347 crate::pattern::generate::wcs_setups(input.operation,input.setup,context.available,input.mesh,input.tolerance,runner)?
348 }else{
349 vec![crate::program::SetupInput {setup:input.setup.clone(),operations:crate::pattern::generate::generate(input.operation,input.setup,context.available,input.mesh,input.tolerance,runner)?}]
350 };
351 if output.is_empty()||output.iter().any(|s|s.operations.is_empty()||s.operations.iter().any(|o|o.toolpath.motions.is_empty()||!o.toolpath.report.is_usable())) {return Err(refused(input,"pattern expansion produced no usable operations"));}return Ok(output);
352 }
353 let tool=tool_of(input.operation,input.tools)?.clone();let path=generate_with_context(input,context,runner)?;
354 Ok(vec![crate::program::SetupInput {setup:input.setup.clone(),operations:vec![crate::program::OperationInput {operation:input.operation.clone(),tool,toolpath:path}]}])
355}
356
357fn turning_outer_contour(profile:&crate::turning::profile::TurningProfile,tol:&Tolerance)->Result<crate::turning::profile_path::ProfileInput,CamError> {
360 let mut edges:Vec<_>=profile.edges.iter().filter(|e| e.start().x>tol.point_eps && e.end().x>tol.point_eps).cloned().collect();
361 let start=edges.iter().flat_map(|e|[e.start(),e.end()]).max_by(|a,b|a.z.total_cmp(&b.z).then(a.x.total_cmp(&b.x))).ok_or_else(||invalid("turning: no outside meridian; select a profile"))?;
362 let mut at=start;let mut selected=Vec::new();
363 while !edges.is_empty() {
364 let candidates:Vec<_>=edges.iter().enumerate().filter_map(|(i,e)| {
365 let e=if e.start().distance(at)<=tol.point_eps {e.clone()}else if e.end().distance(at)<=tol.point_eps {e.reversed()}else{return None};
366 (e.end().z<=at.z+tol.point_eps).then_some((i,e))
367 }).collect();
368 if candidates.len()!=1 {return Err(invalid("turning: outside meridian is disconnected, ambiguous or folds; select a profile"));}
369 let (i,e)=candidates.into_iter().next().unwrap();at=e.end();edges.remove(i);selected.push(e);
370 }
371 if start.z-at.z<=tol.point_eps {return Err(invalid("turning: outside meridian has no axial extent"));}
372 Ok(crate::turning::profile_path::ProfileInput{contour:crate::turning::profile_path::chain_from_edges(&selected),pilot_radius:0.})
373}
374
375fn turning_path(input:&GenerateInput<'_>,context:&GenerateContext<'_>,tool:&Tool,runner:&mut Runner<'_>)->Result<Toolpath,CamError> {
376 use crate::{MachineKind,Strategy,Stock,StockCylinder};
377 use crate::turning;
378 let machine=resolved_machine(input,context)?.ok_or_else(||refused(input,"requires a resolved lathe or mill-turn machine; generic setup is a mill"))?;
379 if !matches!(machine.kind,MachineKind::Lathe|MachineKind::MillTurn{..}) {return Err(refused(input,"requires a lathe or mill-turn setup, not a milling setup"));}
380 let geometry=context.turning.ok_or_else(||refused(input,"requires certified turning profile/carriers, insert geometry and stick-out in GenerateContext"))?;
381 if *geometry.tool_id!=tool.id {return Err(refused(input,"turning geometry belongs to a different tool"));}
382 let setup=input.setup;let op=input.operation;let transform=&setup.model_to_machine;
383 let stock=match &setup.stock {Stock::Cylinder{base,axis,radius,height}=>StockCylinder{base:transform.apply_point(*base),axis:transform.apply_vector(*axis),radius:*radius,height:*height},_=>return Err(refused(input,"requires cylindrical setup stock with explicit stick-out"))};
384 turning::profile::bar_section(&stock,geometry.stick_out)?;
385 let extracted;
386 let profile=if let Some(p)=geometry.profile {p}else{extracted=turning::profile::extract(geometry.carriers,input.mesh,op.tolerance*0.125,runner)?;&extracted};
387 let g=geometry.tool_geometry;let stick=geometry.stick_out;
388 match &op.strategy {
389 Strategy::TurnFace(_)=>turning::face::generate(op,setup,tool,g,&stock,stick,profile,runner),
390 Strategy::TurnPartOff(_)=>turning::part_off::generate(op,setup,tool,g,&stock,stick,profile,runner),
391 Strategy::TurnGroove(_)=>turning::groove::generate(op,setup,tool,g,&stock,stick,profile,geometry.groove_center_z.ok_or_else(||refused(input,"requires selected groove centre Z"))?,runner),
392 Strategy::TurnThread(_)=>turning::thread_generation::generate(op,setup,tool,g,&stock,stick,profile,geometry.thread_radius.ok_or_else(||refused(input,"requires selected thread nominal radius"))?,runner).map(|p|p.canned),
393 Strategy::TurnProfile(p)=>{
394 let derived;
395 let contour=if let Some(contour)=geometry.contour {contour}else if p.outside && op.targets.chains.is_empty() && op.targets.drive_faces.is_empty() {
396 derived=turning_outer_contour(profile,&input.tolerance)?;&derived
397 }else{return Err(refused(input,"requires selected front-to-back profile contour and explicit pilot-bore radius"));};
398 let direction=match p.direction {crate::RoughingDirection::Axial=>turning::profile_path::RoughingDirection::Axial,crate::RoughingDirection::Radial=>turning::profile_path::RoughingDirection::Radial};
399 match (p.roughing,p.finishing) {
400 (true,true)=>turning::profile_finish::generate_combined(op,setup,tool,g,&stock,stick,profile,contour,direction,input.tolerance,runner),
401 (true,false)=>turning::profile_rough::generate(op,setup,tool,g,&stock,stick,profile,contour,direction,input.tolerance,runner),
402 (false,true)=>turning::profile_finish::generate(op,setup,tool,g,&stock,stick,profile,contour,input.tolerance,runner),
403 _=>Err(refused(input,"enable roughing or finishing")),
404 }
405 },
406 _=>Err(refused(input,"not a turning strategy")),
407 }
408}
409
410fn live_milling_path(input:&GenerateInput<'_>,context:&GenerateContext<'_>,tool:&Tool,runner:&mut Runner<'_>)->Result<Toolpath,CamError> {
414 let machine=resolved_machine(input,context)?.ok_or_else(||refused(input,"live tooling requires a resolved mill-turn machine"))?;
415 crate::turning::millturn::chain(machine,crate::Point3::ZERO)?;
416 if input.mesh.faces.iter().any(|f|f.protected_only) {
417 return Err(refused(input,"automatic polar milling needs an explicit rotating-part/fixed-fixture selection when protected bodies are present"));
418 }
419 if matches!(&machine.kind,crate::MachineKind::MillTurn{c_axis,..} if c_axis.limits_deg.is_some()) {
420 return Err(refused(input,"automatic polar milling requires unrestricted C travel; use an explicit indexed selection"));
421 }
422 let op=input.operation;
423 if op.tool_axis.is_some_and(|axis|axis.distance(crate::Point3::Z)>input.tolerance.angle_eps) {
424 return Err(refused(input,"axial live milling requires the spindle Z tool axis"));
425 }
426 let feeds=op.feeds.as_ref().unwrap_or(&tool.feeds);
427 if feeds.css {return Err(refused(input,"live milling requires fixed tool spindle rpm"));}
428 if !matches!(op.strategy,Strategy::Contour2D(_)|Strategy::Trace(_)) {return Err(refused(input,"live milling strategy requires a supported axial contour"));}
429 let geometry=planar_for(input,context)?;
430 let mut path=match op.strategy {
431 Strategy::Contour2D(_)=>crate::contour2d::generate(op,input.setup,tool,input.mesh,input.index,geometry.as_ref(),input.tolerance,runner),
432 Strategy::Trace(_)=>crate::trace::generate(op,input.setup,tool,input.mesh,input.index,geometry.as_ref(),input.tolerance,runner),
433 _=>return Err(refused(input,"live milling strategy requires a supported axial contour")),
434 }?;
435 let mut previous:Option<crate::Point3>=None;
436 for motion in &path.motions {
437 if let Some(p)=motion.kind.end() {
438 crate::turning::millturn::validate_position(machine,crate::Point3::new(p.x.hypot(p.y),0.,p.z),input.tolerance)?;
439 if motion.kind.is_cutting() {
440 if let Some(from)=previous {
441 let samples=match motion.kind {
442 crate::MotionKind::ArcCw{to,center,plane,pitch}|crate::MotionKind::ArcCcw{to,center,plane,pitch}=> {
443 if plane!=crate::ArcPlane::Xy {return Err(refused(input,"automatic polar milling requires XY arcs"));}
444 let radius=(from.x-center.x).hypot(from.y-center.y);
447 for r in [(center.x.hypot(center.y)-radius).max(0.),center.x.hypot(center.y)+radius] {
448 for z in [from.z,to.z] {crate::turning::millturn::validate_position(machine,crate::Point3::new(r,0.,z),input.tolerance)?;}
449 }
450 let mut points=vec![from];
451 points.extend(crate::multiaxis::linearize_arc(from,to,center,plane,pitch,matches!(motion.kind,crate::MotionKind::ArcCw{..}),op.tolerance*0.125));points
452 },
453 _=>vec![from,p],
454 };
455 for pair in samples.windows(2) {
456 runner.work(1,"live-polar-check",0,0)?;
457 let edge=crate::planar::Segment::line(crate::Point2::new(pair[0].x,pair[0].y),crate::Point2::new(pair[1].x,pair[1].y));
458 if edge.distance(crate::Point2::new(0.,0.))<=op.tolerance {return Err(refused(input,"polar live contour crosses the spindle singularity"));}
459 }
460 }
461 }
462 previous=Some(p);
463 }
464 }
465 path.motions.insert(0,crate::Motion::new(crate::MotionKind::PolarInterpolation{enabled:true},crate::Feed::RAPID));
466 path.motions.push(crate::Motion::new(crate::MotionKind::PolarInterpolation{enabled:false},crate::Feed::RAPID));
467 path.report.counts.motions=path.motions.len() as u64;
468 Ok(path)
469}
470
471fn mill_turn_path(input:&GenerateInput<'_>,context:&GenerateContext<'_>,selection:&MillTurnInput<'_>,tool:&Tool,runner:&mut Runner<'_>)->Result<Toolpath,CamError> {
472 if input.operation.setup!=input.setup.id {return Err(refused(input,"operation setup identity mismatch"));}
473 let machine=resolved_machine(input,context)?.ok_or_else(||refused(input,"live tooling requires a resolved mill-turn machine"))?;
474 crate::turning::millturn::chain(machine,crate::Point3::ZERO)?;
475 match selection {
476 MillTurnInput::IndexedDrill{hole,indexed_mesh}=>{
477 if !matches!(input.operation.strategy,Strategy::Drill(_)){return Err(refused(input,"indexed live drilling requires Drill strategy"));}
478 selected_holes(input.operation,std::slice::from_ref(*hole))?;
479 crate::turning::millturn::drilling::generate_hole(input.operation,input.setup,machine,tool,hole,indexed_mesh,input.tolerance,runner).map(|p|p.canned)
480 },
481 MillTurnInput::Polar{contour,z,part,fixed}=>{
482 if !matches!(input.operation.strategy,Strategy::Contour2D(_)|Strategy::Trace(_)){return Err(refused(input,"polar live milling requires Contour2D or Trace with a compensated contour"));}
483 crate::turning::millturn::milling::polar(input.operation,machine,tool,contour,*z,part,fixed,input.tolerance,runner).map(|p|p.0)
484 },
485 MillTurnInput::Cylindrical{contour,radius,part,fixed}=>{
486 if !matches!(input.operation.strategy,Strategy::Contour2D(_)|Strategy::Trace(_)){return Err(refused(input,"cylindrical live milling requires Contour2D or Trace with a compensated contour"));}
487 crate::turning::millturn::milling::cylindrical(input.operation,machine,tool,contour,*radius,part,fixed,input.tolerance,runner).map(|p|p.0)
488 },
489 }
490}
491
492pub fn generate(input: &GenerateInput<'_>, runner: &mut Runner<'_>) -> Result<Toolpath, CamError> {
494 generate_with_context(input,&GenerateContext::default(),runner)
495}
496
497pub fn generate_with_context(input:&GenerateInput<'_>,context:&GenerateContext<'_>,runner:&mut Runner<'_>)->Result<Toolpath,CamError> {
499 let holder=input.tools.iter().find(|t|t.id==input.operation.tool).and_then(|t|t.holder.as_ref());
503 crate::link::with_pre_op_stock(context.pre_op_stock,||crate::strategy_linking::with_tool_holder(holder,||generate_in_context(input,context,runner)))
504}
505
506fn generate_in_context(input:&GenerateInput<'_>,context:&GenerateContext<'_>,runner:&mut Runner<'_>)->Result<Toolpath,CamError> {
507 runner.token().check()?;
508 if context.resolved_planar.is_some() && !matches!(input.operation.strategy,Strategy::Face(_)|Strategy::Pocket2D(_)|Strategy::Contour2D(_)|Strategy::Slot(_)|Strategy::Trace(_)|Strategy::Chamfer2D(_)|Strategy::Engrave(_)|Strategy::Bore(_)|Strategy::Circular(_)|Strategy::ThreadMill(_)) {
509 return Err(refused(input,"resolved planar geometry is not supported by this strategy"));
510 }
511 if let Some((operation,_))=context.resolved_planar {
512 if operation!=&input.operation.id {return Err(refused(input,"resolved planar geometry belongs to another operation"));}
513 if !input.operation.targets.chains.is_empty() {return Err(refused(input,"supply resolved planar geometry or stored chains, not both"));}
514 }
515 if matches!(input.operation.strategy,Strategy::ManualNc(_)) {return crate::pattern::manual_nc::generate(input.operation,runner);}
516 if matches!(input.operation.strategy,Strategy::Pattern(_)) {return Err(refused(input,"requires generate_expanded to preserve source tool and WCS sections"));}
517 let op = input.operation;
518 let setup = input.setup;
519 let mesh = input.mesh;
520 let index = input.index;
521 let tol = input.tolerance;
522 if !op.targets.chains.is_empty() && !matches!(op.strategy,Strategy::Contour2D(_)|Strategy::Slot(_)|Strategy::Trace(_)|Strategy::Chamfer2D(_)|Strategy::Engrave(_)) {
523 return Err(refused(input,"selected chains are not supported by this strategy"));
524 }
525 if op.targets.target_solids.is_empty() && op.targets.chains.is_empty() {
526 return Err(CamError::EmptyTarget { operation_id: op.id.as_str().to_string() });
527 }
528 let tool = tool_of(op, input.tools)?;
529 let heights = heights_of(op, setup);
530 let feeds = feeds_of(op, tool);
531 if let Some(selection)=context.mill_turn {return mill_turn_path(input,context,selection,tool,runner);}
532 if op.live_tool==Some(true) && !context.machine.is_some_and(|m|matches!(m.kind,crate::MachineKind::MillTurn{..})) {
533 return Err(refused(input,"live tooling requires a resolved mill-turn machine"));
534 }
535 if let Some(machine)=context.machine {
536 resolved_machine(input,context)?;
537 if matches!(machine.kind,crate::MachineKind::MillTurn{..}) && !op.strategy.is_turning() && !op.strategy.is_targetless() && op.live_tool.unwrap_or(true) {
538 return live_milling_path(input,context,tool,runner);
539 }
540 if matches!(machine.kind,crate::MachineKind::Lathe) && matches!(op.strategy,Strategy::Drill(_)) {
541 let holes = selected_holes(op, input.holes)?;
542 return crate::turning::drill::generate(op,setup,tool,&holes,mesh,tol,runner).map(|p|p.canned);
543 }
544 if matches!(machine.kind,crate::MachineKind::Lathe|crate::MachineKind::MillTurn{..}) && !matches!(op.strategy,Strategy::TurnFace(_)|Strategy::TurnProfile(_)|Strategy::TurnGroove(_)|Strategy::TurnThread(_)|Strategy::TurnPartOff(_)) {
545 return Err(refused(input,"this setup requires an explicit live-tool selection or a turning strategy"));
546 }
547 }
548 if let Strategy::RestFinish(p)=&op.strategy {if p.source==crate::RestSource::Stock {
549 let stock=context.rest_stock.ok_or_else(||refused(input,"stock rest finishing requires the previous simulated DexelGrid"))?;
550 return crate::rest_finish::generate_from_stock(op,&cutter_of(tool)?,mesh,index,&heights,&feeds,tol,runner,stock);
551 }}
552 match &op.strategy {
553 Strategy::Parallel(_) => crate::parallel::generate(op, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner),
555 Strategy::Scallop(_) => crate::scallop::generate(op, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner),
556 Strategy::Pencil(_) => crate::pencil::generate(op, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner),
557 Strategy::Radial(_) => crate::radial::generate(op, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner),
558 Strategy::Spiral(_) => crate::spiral::generate(op, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner),
559 Strategy::MorphedSpiral(_) => crate::morphed_spiral::generate(op, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner),
560 Strategy::Project(_) => crate::project::generate(op, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner),
561 Strategy::Morph(_) => crate::morph::generate(op, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner),
562 Strategy::RestFinish(p) => {
563 let reference = input
564 .tools
565 .iter()
566 .find(|t| t.id == p.reference_tool)
567 .ok_or_else(|| invalid(format!("{}: reference tool `{}` is not in the document", op.id, p.reference_tool)))?;
568 crate::rest_finish::generate(op, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner, &cutter_of(reference)?)
569 }
570 Strategy::Contour3D(_) => crate::waterline::contour::generate(op, setup, tool, mesh, tol, Default::default(), runner),
572 Strategy::SteepShallow(_) => crate::waterline::steep_shallow::generate(op, setup, tool, mesh, tol, Default::default(), runner),
573 Strategy::Horizontal(_) => crate::waterline::horizontal::generate(op, setup, tool, mesh, tol, runner),
574 Strategy::Ramp(_) => crate::waterline::ramp::generate(op, setup, tool, mesh, tol, Default::default(), runner),
575 Strategy::Swarf(_) => crate::swarf::generate(op, &cutter_of(tool)?, tool.holder.as_ref(), mesh, index, &heights, &feeds, tol, runner),
577 Strategy::MultiAxisContour(_) => {
578 crate::multiaxis_contour::generate(op, &cutter_of(tool)?, tool.holder.as_ref(), mesh, index, &heights, &feeds, tol, runner)
579 }
580 Strategy::Face(_) => crate::face::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
582 Strategy::Pocket2D(_) => crate::pocket2d::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
583 Strategy::Contour2D(_) => crate::contour2d::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
584 Strategy::Slot(_) => crate::slot::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
585 Strategy::Trace(_) => crate::trace::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
586 Strategy::Chamfer2D(_) => crate::chamfer2d::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
587 Strategy::Engrave(_) => crate::engrave::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
588 Strategy::Bore(_) => crate::bore::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
589 Strategy::Circular(_) => crate::circular::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
590 Strategy::ThreadMill(_) => crate::thread_mill::generate(op, setup, tool, mesh, index, planar_for(input,context)?.as_ref(), tol, runner),
591 Strategy::Drill(_) => {
593 let holes = selected_holes(op, input.holes)?;
594 if holes.is_empty() {
595 return Err(invalid(format!("{}: no hole was recognised on its target bodies", op.id)));
596 }
597 crate::drill::strategy::generate(op, setup, tool, &holes, mesh, tol, runner).map(|out| out.canned)
598 }
599 Strategy::ProbeWcs(_) => crate::probe_wcs::generate(op, setup, tool, mesh, tol, runner),
601 Strategy::ProbeGeometry(_) => crate::probe_geometry::generate(op, setup, tool, mesh, tol, runner),
602 Strategy::RotaryFinish(p) => crate::rotary::generate_rotary_finish(op, p, &cutter_of(tool)?, mesh, index, &heights, &feeds, tol, runner),
603 Strategy::MultiAxisFinish(p) => {
605 let inner=inner_path(input,context,runner)?;
606 let chain=machine_chain(input,context)?;
607 let machine=chain.as_ref().map(|chain|crate::multiaxis_finish::MachineCheck {chain,options:Default::default()});
608 crate::multiaxis_finish::generate(op,p,&inner,&cutter_of(tool)?,tool.holder.as_ref(),mesh,index,machine.as_ref(),&heights,&feeds,tol,runner)
609 },
610 Strategy::Adaptive2D(_) | Strategy::Adaptive3D(_) | Strategy::Pocket3D(_) => {
611 roughing_path(input,context,tool,runner)
612 }
613 Strategy::Flow(_) => {
614 if context.flow_faces.is_empty(){return Err(refused(input,"requires selected parametric face samplers from the geometry adapter"));}
615 crate::flow::generate(op,&cutter_of(tool)?,mesh,index,&heights,&feeds,tol,runner,context.flow_faces)
616 },
617 Strategy::RotaryWrap(p) => {
618 if context.source_operation.is_some() {return Err(refused(input,"requires a precomputed unwrapped source path; source_operation is only for MultiAxisFinish"));}
619 crate::rotary::wrap_toolpath(op,p,validated_inner_path(input,context)?,&cutter_of(tool)?,mesh,index,tol,runner)
620 },
621 Strategy::TurnFace(_) | Strategy::TurnProfile(_) | Strategy::TurnGroove(_) | Strategy::TurnThread(_) | Strategy::TurnPartOff(_) => {
622 turning_path(input,context,tool,runner)
623 }
624 Strategy::ManualNc(_) => crate::pattern::manual_nc::generate(op,runner),
625 Strategy::Pattern(_) => Err(refused(input,"requires generate_expanded to preserve source tool and WCS sections")),
626 }
627}
628