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: Box<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 second_length = args.get_kw_arg_opt("secondLength", &RuntimeType::length(), exec_state)?;
38    let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
39    let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
40    let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
41    let edge_cut_number: Option<u32> = args.get_kw_arg_opt("version", &RuntimeType::count(), exec_state)?;
42    let edge_cut_version: EdgeCutVersion = edge_cut_number
43        .map(|num| {
44            num.try_into().map_err(|()| {
45                KclError::new_semantic(KclErrorDetails::new(
46                    format!("{} is not a version of the Zoo edge cut algorithm", num),
47                    vec![args.source_range],
48                ))
49            })
50        })
51        .transpose()?
52        .unwrap_or_default();
53
54    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
55
56    // Edge specifiers are object-shaped payloads, so there is no narrow RuntimeType for them yet.
57    // Keep this broad at the boundary and validate the shape in parse_tagged_edge_inputs.
58    let edge_refs = args.get_kw_arg_opt("edges", &RuntimeType::any_array(), exec_state)?;
59    let tags = args.kw_arg_edge_array_and_source_opt("tags")?;
60
61    let edge_inputs = super::fillet::parse_tagged_edge_inputs(
62        edge_refs,
63        tags,
64        Some(solid.as_ref()),
65        exec_state,
66        &args,
67        "You must provide either 'tags' or 'edges' to chamfer edges",
68        "You must provide either 'tags' or 'edges' to chamfer edges, not both",
69    )
70    .await?;
71
72    match edge_inputs {
73        super::fillet::TaggedEdgeInputs::EngineRefs(edge_refs) => {
74            let value = inner_chamfer_with_engine_refs(
75                solid,
76                length,
77                edge_refs,
78                second_length,
79                angle,
80                csg_algorithm,
81                edge_cut_version,
82                tag,
83                exec_state,
84                args,
85            )
86            .await?;
87            Ok(KclValue::Solid { value })
88        }
89        super::fillet::TaggedEdgeInputs::Tags(tags) => {
90            let value = inner_chamfer(
91                solid,
92                length,
93                tags,
94                second_length,
95                angle,
96                None,
97                tag,
98                csg_algorithm,
99                edge_cut_version,
100                exec_state,
101                args,
102            )
103            .await?;
104            Ok(KclValue::Solid { value })
105        }
106    }
107}
108
109#[allow(clippy::too_many_arguments)]
110async fn inner_chamfer(
111    solid: Box<Solid>,
112    length: TyF64,
113    tags: Vec<EdgeReference>,
114    second_length: Option<TyF64>,
115    angle: Option<TyF64>,
116    custom_profile: Option<Sketch>,
117    tag: Option<TagNode>,
118    csg_algorithm: CsgAlgorithm,
119    edge_cut_version: EdgeCutVersion,
120    exec_state: &mut ExecState,
121    args: Args,
122) -> Result<Box<Solid>, KclError> {
123    // If you try and tag multiple edges with a tagged chamfer, we want to return an
124    // error to the user that they can only tag one edge at a time.
125    if tag.is_some() && tags.len() > 1 {
126        return Err(KclError::new_type(KclErrorDetails::new(
127            "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(),
128            vec![args.source_range],
129        )));
130    }
131
132    if angle.is_some() && second_length.is_some() {
133        return Err(KclError::new_semantic(KclErrorDetails::new(
134            "Cannot specify both an angle and a second length. Specify only one.".to_string(),
135            vec![args.source_range],
136        )));
137    }
138
139    let strategy = if second_length.is_some() || angle.is_some() || custom_profile.is_some() {
140        CutStrategy::Csg
141    } else {
142        Default::default()
143    };
144
145    let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
146    let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
147    if let Some(angle) = angle
148        && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
149    {
150        return Err(KclError::new_semantic(KclErrorDetails::new(
151            "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
152            vec![args.source_range],
153        )));
154    }
155
156    let cut_type = if let Some(custom_profile) = custom_profile {
157        // Hide the custom profile since it's no longer its own profile
158        exec_state
159            .batch_modeling_cmd(
160                ModelingCmdMeta::from_args(exec_state, &args),
161                ModelingCmd::from(
162                    mcmd::ObjectVisible::builder()
163                        .object_id(custom_profile.id)
164                        .hidden(true)
165                        .build(),
166                ),
167            )
168            .await?;
169        CutTypeV2::Custom {
170            path: custom_profile.id,
171        }
172    } else {
173        CutTypeV2::Chamfer {
174            distance: LengthUnit(length.to_mm()),
175            second_distance,
176            angle,
177            swap: false,
178        }
179    };
180
181    let mut solid = solid.clone();
182    for edge_tag in tags {
183        let edge_ids = edge_tag.get_all_engine_ids(exec_state, &args)?;
184        for edge_id in edge_ids {
185            let id = exec_state.next_uuid();
186            exec_state
187                .batch_end_cmd(
188                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
189                    ModelingCmd::from(
190                        mcmd::Solid3dCutEdges::builder()
191                            .use_legacy(csg_algorithm.is_legacy())
192                            .edge_ids(vec![edge_id])
193                            .extra_face_ids(vec![])
194                            .strategy(strategy)
195                            .object_id(solid.id)
196                            // We can let the user set this in the future.
197                            .tolerance(LengthUnit(DEFAULT_TOLERANCE))
198                            .cut_type(cut_type)
199                            .version(edge_cut_version)
200                            .build(),
201                    ),
202                )
203                .await?;
204
205            solid.edge_cuts.push(EdgeCut::Chamfer {
206                id,
207                edge_id,
208                length: length.clone(),
209                tag: Box::new(tag.clone()),
210            });
211
212            if let Some(ref tag) = tag {
213                solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
214                    face_id: id,
215                    tag: Some(tag.clone()),
216                    geo_meta: GeoMeta {
217                        id,
218                        metadata: args.source_range.into(),
219                    },
220                }));
221            }
222        }
223    }
224
225    Ok(solid)
226}
227
228#[expect(clippy::too_many_arguments)]
229async fn inner_chamfer_with_engine_refs(
230    solid: Box<Solid>,
231    length: TyF64,
232    edge_references: Vec<kcmc::shared::EdgeSpecifier>,
233    second_length: Option<TyF64>,
234    angle: Option<TyF64>,
235    csg_algorithm: CsgAlgorithm,
236    edge_cut_version: EdgeCutVersion,
237    tag: Option<TagNode>,
238    exec_state: &mut ExecState,
239    args: Args,
240) -> Result<Box<Solid>, KclError> {
241    if tag.is_some() && edge_references.len() > 1 {
242        return Err(KclError::new_type(KclErrorDetails::new(
243            "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 edgeRef.".to_string(),
244            vec![args.source_range],
245        )));
246    }
247
248    if angle.is_some() && second_length.is_some() {
249        return Err(KclError::new_semantic(KclErrorDetails::new(
250            "Cannot specify both an angle and a second length. Specify only one.".to_string(),
251            vec![args.source_range],
252        )));
253    }
254
255    let strategy = if second_length.is_some() || angle.is_some() {
256        CutStrategy::Csg
257    } else {
258        Default::default()
259    };
260
261    let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
262    let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
263    if let Some(angle) = angle
264        && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
265    {
266        return Err(KclError::new_semantic(KclErrorDetails::new(
267            "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
268            vec![args.source_range],
269        )));
270    }
271
272    let cut_type = CutTypeV2::Chamfer {
273        distance: LengthUnit(length.to_mm()),
274        second_distance,
275        angle,
276        swap: false,
277    };
278
279    let id = exec_state.next_uuid();
280    let num_extra_ids = edge_references.len().saturating_sub(1);
281    let mut extra_face_ids = Vec::with_capacity(num_extra_ids);
282    for _ in 0..num_extra_ids {
283        extra_face_ids.push(exec_state.next_uuid());
284    }
285
286    let mut solid = solid.clone();
287    exec_state
288        .batch_end_cmd(
289            ModelingCmdMeta::from_args_id(exec_state, &args, id),
290            ModelingCmd::from(
291                mcmd::Solid3dCutEdgeReferences::builder()
292                    .object_id(solid.id)
293                    .edges_references(edge_references)
294                    .cut_type(cut_type)
295                    .tolerance(LengthUnit(DEFAULT_TOLERANCE))
296                    .strategy(strategy)
297                    .extra_face_ids(extra_face_ids)
298                    .use_legacy(csg_algorithm.is_legacy())
299                    .version(edge_cut_version)
300                    .build(),
301            ),
302        )
303        .await?;
304
305    solid.pending_edge_cut_ids.push(id);
306
307    if let Some(ref tag) = tag {
308        solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
309            face_id: id,
310            tag: Some(tag.clone()),
311            geo_meta: GeoMeta {
312                id,
313                metadata: args.source_range.into(),
314            },
315        }));
316    }
317
318    Ok(solid)
319}