Skip to main content

brep_cam/dispatch/
chains.rs

1//! Identity-based chain extraction. Geometry is supplied by the worker; mesh
2//! silhouettes cannot recover persistent edges or sketch geometry.
3use super::*;
4use crate::{ChainSource, ChainSide, RigidTransform};
5#[cfg(feature="kernel")]
6use crate::planar::{machining::PlanarChain, Vec2};
7
8/// Resolve the exact requested source in machine coordinates. Implementations
9/// must preserve ordered curve selection and reject disconnected/nonplanar data.
10pub trait ChainGeometry {
11    fn resolve(&self, source: &ChainSource, transform: &RigidTransform, chord: f64, tol: &Tolerance) -> Result<PlanarInput, CamError>;
12}
13
14pub(super) fn selected(input: &GenerateInput<'_>, context: &GenerateContext<'_>) -> Result<PlanarInput, CamError> {
15    if input.operation.targets.chains.is_empty() { return planar_input(input); }
16    let resolver=context.chain_geometry.ok_or_else(||refused(input,"selected chains require resolved edge/sketch geometry in GenerateContext"))?;
17    let mut out=PlanarInput::default();
18    for selection in &input.operation.targets.chains {
19        selection.validate().map_err(invalid)?;
20        let supported=match (&input.operation.strategy,selection.side) {
21            (_,ChainSide::Auto)=>true,
22            (Strategy::Contour2D(p),side)=>if selection.closed {side==if p.outside {ChainSide::Outside}else{ChainSide::Inside}} else {side==if p.outside {ChainSide::Right}else{ChainSide::Left}},
23            (Strategy::Trace(p),side)=>side==match p.side {-1=>ChainSide::Left,1=>ChainSide::Right,_=>ChainSide::On},
24            (Strategy::Slot(_)|Strategy::Engrave(_),ChainSide::On)=>true,
25            (Strategy::Chamfer2D(_),side)=>side==if selection.closed {ChainSide::Outside}else{ChainSide::Right},
26            _=>false,
27        };
28        if !supported {return Err(refused(input,&format!("chain side {:?} is unsupported by the strategy's compensation settings",selection.side)));}
29        if selection.offset!=0. {return Err(refused(input,"chain offset is not supported"));}
30        if selection.start.is_some() {return Err(refused(input,"chain start/lead placement is not supported"));}
31        let mut resolved=resolver.resolve(&selection.source,&input.setup.model_to_machine,input.operation.tolerance,&input.tolerance)?;
32        if resolved.chains.is_empty() {return Err(refused(input,"selected chain source contains no curves"));}
33        if matches!(input.operation.strategy,Strategy::Contour2D(_)|Strategy::Chamfer2D(_)) && resolved.regions.iter().any(|r|r.region.loops.len()!=1) {return Err(refused(input,"profile islands require per-loop side compensation, which is not supported"));}
34        for chain in &mut resolved.chains {
35            if chain.chain.closed!=selection.closed {return Err(refused(input,"selected chain closure does not match geometry"));}
36            if chain.chain.segments.is_empty() || !chain.z.is_finite() || chain.chain.segments.iter().any(|s|!s.start.is_finite() || !s.end.is_finite()) {return Err(refused(input,"selected chain is empty or non-finite"));}
37            if selection.reversed {chain.chain=chain.chain.reversed();}
38        }
39        out.chains.extend(resolved.chains);
40        out.regions.extend(resolved.regions);
41    }
42    let all:Vec<u32>=(0..input.mesh.triangle_count() as u32).collect();
43    out.stock_top=if all.is_empty() {None}else{Some(input.mesh.bounds_of(&all).max.z)};
44    Ok(out)
45}
46
47/// Kernel-backed geometry named by document identities, in model coordinates.
48/// The caller supplies all selected objects; missing identities fail explicitly.
49#[cfg(feature="kernel")]
50pub struct KernelChainGeometry<'a> {
51    pub solids: &'a [(&'a str, &'a brep_kernel::BrepSolid)],
52    pub curves: &'a [(&'a str, &'a str, &'a brep_kernel::NurbsCurve)],
53    pub profiles: &'a [(&'a str, &'a brep_kernel::SketchProfile)],
54}
55
56#[cfg(feature="kernel")]
57impl ChainGeometry for KernelChainGeometry<'_> {
58    fn resolve(&self, source:&ChainSource, transform:&RigidTransform, chord:f64, tol:&Tolerance)->Result<PlanarInput,CamError> {
59        use crate::kernel_adapter::planar::{edge_chains,curve_segments,sketch_regions};
60        source.validate().map_err(invalid)?;
61        transform.validate().map_err(|e|invalid(e.to_string()))?;
62        if !chord.is_finite() || chord<=0. {return Err(invalid("chain chord tolerance must be positive"));}
63        let mut pieces=Vec::new();
64        match source {
65            ChainSource::Edges {edges}=>for edge in edges {
66                let solid=self.solids.iter().find(|(id,_)|*id==edge.solid).ok_or_else(||invalid(format!("chain body {:?} is unresolved",edge.solid)))?.1;
67                let mut extracted=edge_chains(solid,&[edge.edge.as_str()],transform,chord,tol)?;
68                if extracted.len()!=1 {return Err(invalid("selected edge must resolve to one connected chain"));}
69                pieces.push(extracted.remove(0));
70            },
71            ChainSource::SketchCurves {sketch,curves}=>for id in curves {
72                let curve=self.curves.iter().find(|(s,c,_)|*s==sketch && *c==id).ok_or_else(||invalid(format!("sketch curve {sketch}:G{id} is unresolved")))?.2;
73                let [a,b]=curve.domain().map_err(invalid)?;
74                let (segments,z)=curve_segments(curve,a,b,true,transform,chord,tol)?;
75                if z[1]-z[0]>tol.distance_eps {return Err(invalid("sketch curve is not horizontal planar geometry"));}
76                pieces.push(PlanarChain {chain:crate::planar::Chain::open(segments),z:(z[0]+z[1])*0.5});
77            },
78            ChainSource::SketchProfile {sketch,regions}=>{
79                let profile=self.profiles.iter().find(|(id,_)|*id==sketch).ok_or_else(||invalid(format!("sketch profile {sketch} is unresolved")))?.1;
80                let extracted=sketch_regions(profile,transform,chord,tol)?;
81                let indices:Vec<usize>=if regions.is_empty(){(0..extracted.len()).collect()}else{regions.iter().map(|i|*i as usize).collect()};
82                let mut out=PlanarInput::default();
83                for i in indices {
84                    let region=extracted.get(i).ok_or_else(||invalid(format!("sketch profile {sketch} region {i} is unresolved")))?.clone();
85                    out.chains.extend(region.region.loops.iter().map(|l|PlanarChain {chain:crate::planar::Chain::from_loop(l),z:region.z}));
86                    out.regions.push(region);
87                }
88                return Ok(out);
89            },
90        }
91        let chain=join_ordered(&pieces,tol)?;
92        Ok(PlanarInput {chains:vec![chain],..Default::default()})
93    }
94}
95
96#[cfg(feature="kernel")]
97fn join_ordered(pieces:&[PlanarChain],tol:&Tolerance)->Result<PlanarChain,CamError> {
98    let first=pieces.first().ok_or_else(||invalid("chain has no curves"))?;
99    if pieces.iter().any(|p|(p.z-first.z).abs()>tol.distance_eps || p.chain.segments.is_empty()) {return Err(invalid("chain curves must share one horizontal plane"));}
100    // Preserve selection order; the first edge may be intrinsically backwards.
101    for reverse in [false,true] {
102        let mut chain=if reverse {first.chain.reversed()}else{first.chain.clone()};
103        let mut connected=true;
104        for next in &pieces[1..] {
105            let end=chain.end().unwrap();
106            let next=if end.distance(next.chain.start().unwrap())<=tol.point_eps {next.chain.clone()}else if end.distance(next.chain.end().unwrap())<=tol.point_eps {next.chain.reversed()}else{connected=false;break};
107            chain.segments.extend(next.segments);
108        }
109        if connected {
110            chain.closed=chain.start().unwrap().distance(chain.end().unwrap())<=tol.point_eps;
111            return Ok(PlanarChain {chain,z:first.z});
112        }
113    }
114    Err(invalid("selected chain curves are disconnected or not in chain order"))
115}
116