kcl_lib/std/
chamfer.rs

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