Skip to main content

kcl_lib/std/
chamfer.rs

1//! Standard library chamfers.
2
3use anyhow::Result;
4use kcmc::ModelingCmd;
5use kcmc::each_cmd as mcmd;
6use kcmc::length_unit::LengthUnit;
7use kcmc::shared::Angle;
8use kcmc::shared::CutStrategy;
9use kcmc::shared::CutTypeV2;
10use kcmc::shared::EdgeCutVersion;
11use kittycad_modeling_cmds::{self as kcmc};
12
13use super::args::TyF64;
14use crate::errors::KclError;
15use crate::errors::KclErrorDetails;
16use crate::execution::ChamferSurface;
17use crate::execution::EdgeCut;
18use crate::execution::ExecState;
19use crate::execution::ExtrudeSurface;
20use crate::execution::GeoMeta;
21use crate::execution::KclValue;
22use crate::execution::ModelingCmdMeta;
23use crate::execution::Sketch;
24use crate::execution::Solid;
25use crate::execution::types::RuntimeType;
26use crate::parsing::ast::types::TagNode;
27use crate::std::Args;
28use crate::std::csg::CsgAlgorithm;
29use crate::std::fillet::EdgeReference;
30
31pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
32
33/// Create chamfers on tagged paths.
34pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
35    let solid = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
36    let length: TyF64 = args.get_kw_arg("length", &RuntimeType::length(), exec_state)?;
37    let tags = args.kw_arg_edge_array_and_source("tags")?;
38    let second_length = args.get_kw_arg_opt("secondLength", &RuntimeType::length(), exec_state)?;
39    let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
40    let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
41    let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
42    let edge_cut_number: Option<u32> = args.get_kw_arg_opt("version", &RuntimeType::count(), exec_state)?;
43    let edge_cut_version: EdgeCutVersion = edge_cut_number
44        .map(|num| {
45            num.try_into().map_err(|()| {
46                KclError::new_semantic(KclErrorDetails::new(
47                    format!("{} is not a version of the Zoo edge cut algorithm", num),
48                    vec![args.source_range],
49                ))
50            })
51        })
52        .transpose()?
53        .unwrap_or_default();
54
55    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
56
57    super::fillet::validate_unique(&tags)?;
58    let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
59    let value = inner_chamfer(
60        solid,
61        length,
62        tags,
63        second_length,
64        angle,
65        None,
66        tag,
67        csg_algorithm,
68        edge_cut_version,
69        exec_state,
70        args,
71    )
72    .await?;
73    Ok(KclValue::Solid { value })
74}
75
76#[allow(clippy::too_many_arguments)]
77async fn inner_chamfer(
78    solid: Box<Solid>,
79    length: TyF64,
80    tags: Vec<EdgeReference>,
81    second_length: Option<TyF64>,
82    angle: Option<TyF64>,
83    custom_profile: Option<Sketch>,
84    tag: Option<TagNode>,
85    csg_algorithm: CsgAlgorithm,
86    edge_cut_version: EdgeCutVersion,
87    exec_state: &mut ExecState,
88    args: Args,
89) -> Result<Box<Solid>, KclError> {
90    // If you try and tag multiple edges with a tagged chamfer, we want to return an
91    // error to the user that they can only tag one edge at a time.
92    if tag.is_some() && tags.len() > 1 {
93        return Err(KclError::new_type(KclErrorDetails::new(
94            "You can only tag one edge at a time with a tagged chamfer. Either delete the tag for the chamfer fn if you don't need it OR separate into individual chamfer functions for each tag.".to_string(),
95            vec![args.source_range],
96        )));
97    }
98
99    if angle.is_some() && second_length.is_some() {
100        return Err(KclError::new_semantic(KclErrorDetails::new(
101            "Cannot specify both an angle and a second length. Specify only one.".to_string(),
102            vec![args.source_range],
103        )));
104    }
105
106    let strategy = if second_length.is_some() || angle.is_some() || custom_profile.is_some() {
107        CutStrategy::Csg
108    } else {
109        Default::default()
110    };
111
112    let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
113    let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
114    if let Some(angle) = angle
115        && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
116    {
117        return Err(KclError::new_semantic(KclErrorDetails::new(
118            "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
119            vec![args.source_range],
120        )));
121    }
122
123    let cut_type = if let Some(custom_profile) = custom_profile {
124        // Hide the custom profile since it's no longer its own profile
125        exec_state
126            .batch_modeling_cmd(
127                ModelingCmdMeta::from_args(exec_state, &args),
128                ModelingCmd::from(
129                    mcmd::ObjectVisible::builder()
130                        .object_id(custom_profile.id)
131                        .hidden(true)
132                        .build(),
133                ),
134            )
135            .await?;
136        CutTypeV2::Custom {
137            path: custom_profile.id,
138        }
139    } else {
140        CutTypeV2::Chamfer {
141            distance: LengthUnit(length.to_mm()),
142            second_distance,
143            angle,
144            swap: false,
145        }
146    };
147
148    let mut solid = solid.clone();
149    for edge_tag in tags {
150        let edge_ids = edge_tag.get_all_engine_ids(exec_state, &args)?;
151        for edge_id in edge_ids {
152            let id = exec_state.next_uuid();
153            exec_state
154                .batch_end_cmd(
155                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
156                    ModelingCmd::from(
157                        mcmd::Solid3dCutEdges::builder()
158                            .use_legacy(csg_algorithm.is_legacy())
159                            .edge_ids(vec![edge_id])
160                            .extra_face_ids(vec![])
161                            .strategy(strategy)
162                            .object_id(solid.id)
163                            // We can let the user set this in the future.
164                            .tolerance(LengthUnit(DEFAULT_TOLERANCE))
165                            .cut_type(cut_type)
166                            .version(edge_cut_version)
167                            .build(),
168                    ),
169                )
170                .await?;
171
172            solid.edge_cuts.push(EdgeCut::Chamfer {
173                id,
174                edge_id,
175                length: length.clone(),
176                tag: Box::new(tag.clone()),
177            });
178
179            if let Some(ref tag) = tag {
180                solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
181                    face_id: id,
182                    tag: Some(tag.clone()),
183                    geo_meta: GeoMeta {
184                        id,
185                        metadata: args.source_range.into(),
186                    },
187                }));
188            }
189        }
190    }
191
192    Ok(solid)
193}