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),
91                ModelingCmd::from(mcmd::ObjectVisible {
92                    object_id: custom_profile.id,
93                    hidden: true,
94                }),
95            )
96            .await?;
97        CutTypeV2::Custom {
98            path: custom_profile.id,
99        }
100    } else {
101        CutTypeV2::Chamfer {
102            distance: LengthUnit(length.to_mm()),
103            second_distance,
104            angle,
105            swap: false,
106        }
107    };
108
109    let mut solid = solid.clone();
110    for edge_tag in tags {
111        let edge_id = match edge_tag {
112            EdgeReference::Uuid(uuid) => uuid,
113            EdgeReference::Tag(edge_tag) => args.get_tag_engine_info(exec_state, &edge_tag)?.id,
114        };
115
116        let id = exec_state.next_uuid();
117        exec_state
118            .batch_end_cmd(
119                ModelingCmdMeta::from_args_id(&args, id),
120                ModelingCmd::from(mcmd::Solid3dCutEdges {
121                    edge_ids: vec![edge_id],
122                    extra_face_ids: vec![],
123                    strategy,
124                    object_id: solid.id,
125                    tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
126                    cut_type,
127                }),
128            )
129            .await?;
130
131        solid.edge_cuts.push(EdgeCut::Chamfer {
132            id,
133            edge_id,
134            length: length.clone(),
135            tag: Box::new(tag.clone()),
136        });
137
138        if let Some(ref tag) = tag {
139            solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
140                face_id: id,
141                tag: Some(tag.clone()),
142                geo_meta: GeoMeta {
143                    id,
144                    metadata: args.source_range.into(),
145                },
146            }));
147        }
148    }
149
150    Ok(solid)
151}