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