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