kcl_lib/std/
fillet.rs

1//! Standard library fillets.
2
3use anyhow::Result;
4use derive_docs::stdlib;
5use kcmc::{
6    each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::CutType,
7    websocket::OkWebSocketResponseData, ModelingCmd,
8};
9use kittycad_modeling_cmds as kcmc;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14use crate::{
15    errors::{KclError, KclErrorDetails},
16    execution::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier},
17    parsing::ast::types::TagNode,
18    settings::types::UnitLength,
19    std::Args,
20};
21
22/// A tag or a uuid of an edge.
23#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Ord, PartialOrd, Hash)]
24#[ts(export)]
25#[serde(untagged)]
26pub enum EdgeReference {
27    /// A uuid of an edge.
28    Uuid(uuid::Uuid),
29    /// A tag of an edge.
30    Tag(Box<TagIdentifier>),
31}
32
33impl EdgeReference {
34    pub fn get_engine_id(&self, exec_state: &mut ExecState, args: &Args) -> Result<uuid::Uuid, KclError> {
35        match self {
36            EdgeReference::Uuid(uuid) => Ok(*uuid),
37            EdgeReference::Tag(tag) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
38        }
39    }
40}
41
42/// Create fillets on tagged paths.
43pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
44    let solid = args.get_unlabeled_kw_arg("solid")?;
45    let radius = args.get_kw_arg("radius")?;
46    let tolerance = args.get_kw_arg_opt("tolerance")?;
47    let tags = args.get_kw_arg("tags")?;
48    let tag = args.get_kw_arg_opt("tag")?;
49    let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
50    Ok(KclValue::Solid { value })
51}
52
53/// Blend a transitional edge along a tagged path, smoothing the sharp edge.
54///
55/// Fillet is similar in function and use to a chamfer, except
56/// a chamfer will cut a sharp transition along an edge while fillet
57/// will smoothly blend the transition.
58///
59/// ```no_run
60/// width = 20
61/// length = 10
62/// thickness = 1
63/// filletRadius = 2
64///
65/// mountingPlateSketch = startSketchOn("XY")
66///   |> startProfileAt([-width/2, -length/2], %)
67///   |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
68///   |> line(endAbsolute = [width/2, length/2], tag = $edge2)
69///   |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
70///   |> close(tag = $edge4)
71///
72/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
73///   |> fillet(
74///     radius = filletRadius,
75///     tags = [
76///       getNextAdjacentEdge(edge1),
77///       getNextAdjacentEdge(edge2),
78///       getNextAdjacentEdge(edge3),
79///       getNextAdjacentEdge(edge4)
80///     ],
81///   )
82/// ```
83///
84/// ```no_run
85/// width = 20
86/// length = 10
87/// thickness = 1
88/// filletRadius = 1
89///
90/// mountingPlateSketch = startSketchOn("XY")
91///   |> startProfileAt([-width/2, -length/2], %)
92///   |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
93///   |> line(endAbsolute = [width/2, length/2], tag = $edge2)
94///   |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
95///   |> close(tag = $edge4)
96///
97/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
98///   |> fillet(
99///     radius = filletRadius,
100///     tolerance = 0.000001,
101///     tags = [
102///       getNextAdjacentEdge(edge1),
103///       getNextAdjacentEdge(edge2),
104///       getNextAdjacentEdge(edge3),
105///       getNextAdjacentEdge(edge4)
106///     ],
107///   )
108/// ```
109#[stdlib {
110    name = "fillet",
111    feature_tree_operation = true,
112    keywords = true,
113    unlabeled_first = true,
114    args = {
115        solid = { docs = "The solid whose edges should be filletted" },
116        radius = { docs = "The radius of the fillet" },
117        tags = { docs = "The paths you want to fillet" },
118        tolerance = { docs = "The tolerance for this fillet" },
119        tag = { docs = "Create a new tag which refers to this fillet"},
120    }
121}]
122async fn inner_fillet(
123    solid: Box<Solid>,
124    radius: f64,
125    tags: Vec<EdgeReference>,
126    tolerance: Option<f64>,
127    tag: Option<TagNode>,
128    exec_state: &mut ExecState,
129    args: Args,
130) -> Result<Box<Solid>, KclError> {
131    // Check if tags contains any duplicate values.
132    let mut unique_tags = tags.clone();
133    unique_tags.sort();
134    unique_tags.dedup();
135    if unique_tags.len() != tags.len() {
136        return Err(KclError::Type(KclErrorDetails {
137            message: "Duplicate tags are not allowed.".to_string(),
138            source_ranges: vec![args.source_range],
139        }));
140    }
141
142    let mut solid = solid.clone();
143    for edge_tag in tags {
144        let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
145
146        let id = exec_state.next_uuid();
147        args.batch_end_cmd(
148            id,
149            ModelingCmd::from(mcmd::Solid3dFilletEdge {
150                edge_id,
151                object_id: solid.id,
152                radius: LengthUnit(radius),
153                tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
154                cut_type: CutType::Fillet,
155                // We make this a none so that we can remove it in the future.
156                face_id: None,
157            }),
158        )
159        .await?;
160
161        solid.edge_cuts.push(EdgeCut::Fillet {
162            id,
163            edge_id,
164            radius,
165            tag: Box::new(tag.clone()),
166        });
167
168        if let Some(ref tag) = tag {
169            solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
170                face_id: id,
171                tag: Some(tag.clone()),
172                geo_meta: GeoMeta {
173                    id,
174                    metadata: args.source_range.into(),
175                },
176            }));
177        }
178    }
179
180    Ok(solid)
181}
182
183/// Get the opposite edge to the edge given.
184pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
185    let tag: TagIdentifier = args.get_data()?;
186
187    let edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?;
188    Ok(KclValue::Uuid {
189        value: edge,
190        meta: vec![args.source_range.into()],
191    })
192}
193
194/// Get the opposite edge to the edge given.
195///
196/// ```no_run
197/// exampleSketch = startSketchOn('XZ')
198///   |> startProfileAt([0, 0], %)
199///   |> line(end = [10, 0])
200///   |> angledLine({
201///     angle = 60,
202///     length = 10,
203///   }, %)
204///   |> angledLine({
205///     angle = 120,
206///     length = 10,
207///   }, %)
208///   |> line(end = [-10, 0])
209///   |> angledLine({
210///     angle = 240,
211///     length = 10,
212///   }, %, $referenceEdge)
213///   |> close()
214///
215/// example = extrude(exampleSketch, length = 5)
216///   |> fillet(
217///     radius = 3,
218///     tags = [getOppositeEdge(referenceEdge)],
219///   )
220/// ```
221#[stdlib {
222    name = "getOppositeEdge",
223}]
224async fn inner_get_opposite_edge(tag: TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<Uuid, KclError> {
225    if args.ctx.no_engine_commands().await {
226        return Ok(exec_state.next_uuid());
227    }
228    let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
229
230    let id = exec_state.next_uuid();
231    let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
232
233    let resp = args
234        .send_modeling_cmd(
235            id,
236            ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
237                edge_id: tagged_path.id,
238                object_id: tagged_path.sketch,
239                face_id,
240            }),
241        )
242        .await?;
243    let OkWebSocketResponseData::Modeling {
244        modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
245    } = &resp
246    else {
247        return Err(KclError::Engine(KclErrorDetails {
248            message: format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {:?}", resp),
249            source_ranges: vec![args.source_range],
250        }));
251    };
252
253    Ok(opposite_edge.edge)
254}
255
256/// Get the next adjacent edge to the edge given.
257pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
258    let tag: TagIdentifier = args.get_data()?;
259
260    let edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?;
261    Ok(KclValue::Uuid {
262        value: edge,
263        meta: vec![args.source_range.into()],
264    })
265}
266
267/// Get the next adjacent edge to the edge given.
268///
269/// ```no_run
270/// exampleSketch = startSketchOn('XZ')
271///   |> startProfileAt([0, 0], %)
272///   |> line(end = [10, 0])
273///   |> angledLine({
274///     angle = 60,
275///     length = 10,
276///   }, %)
277///   |> angledLine({
278///     angle = 120,
279///     length = 10,
280///   }, %)
281///   |> line(end = [-10, 0])
282///   |> angledLine({
283///     angle = 240,
284///     length = 10,
285///   }, %, $referenceEdge)
286///   |> close()
287///
288/// example = extrude(exampleSketch, length = 5)
289///   |> fillet(
290///     radius = 3,
291///     tags = [getNextAdjacentEdge(referenceEdge)],
292///   )
293/// ```
294#[stdlib {
295    name = "getNextAdjacentEdge",
296}]
297async fn inner_get_next_adjacent_edge(
298    tag: TagIdentifier,
299    exec_state: &mut ExecState,
300    args: Args,
301) -> Result<Uuid, KclError> {
302    if args.ctx.no_engine_commands().await {
303        return Ok(exec_state.next_uuid());
304    }
305    let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
306
307    let id = exec_state.next_uuid();
308    let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
309
310    let resp = args
311        .send_modeling_cmd(
312            id,
313            ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
314                edge_id: tagged_path.id,
315                object_id: tagged_path.sketch,
316                face_id,
317            }),
318        )
319        .await?;
320    let OkWebSocketResponseData::Modeling {
321        modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
322    } = &resp
323    else {
324        return Err(KclError::Engine(KclErrorDetails {
325            message: format!(
326                "mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {:?}",
327                resp
328            ),
329            source_ranges: vec![args.source_range],
330        }));
331    };
332
333    adjacent_edge.edge.ok_or_else(|| {
334        KclError::Type(KclErrorDetails {
335            message: format!("No edge found next adjacent to tag: `{}`", tag.value),
336            source_ranges: vec![args.source_range],
337        })
338    })
339}
340
341/// Get the previous adjacent edge to the edge given.
342pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
343    let tag: TagIdentifier = args.get_data()?;
344
345    let edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?;
346    Ok(KclValue::Uuid {
347        value: edge,
348        meta: vec![args.source_range.into()],
349    })
350}
351
352/// Get the previous adjacent edge to the edge given.
353///
354/// ```no_run
355/// exampleSketch = startSketchOn('XZ')
356///   |> startProfileAt([0, 0], %)
357///   |> line(end = [10, 0])
358///   |> angledLine({
359///     angle = 60,
360///     length = 10,
361///   }, %)
362///   |> angledLine({
363///     angle = 120,
364///     length = 10,
365///   }, %)
366///   |> line(end = [-10, 0])
367///   |> angledLine({
368///     angle = 240,
369///     length = 10,
370///   }, %, $referenceEdge)
371///   |> close()
372///
373/// example = extrude(exampleSketch, length = 5)
374///   |> fillet(
375///     radius = 3,
376///     tags = [getPreviousAdjacentEdge(referenceEdge)],
377///   )
378/// ```
379#[stdlib {
380    name = "getPreviousAdjacentEdge",
381}]
382async fn inner_get_previous_adjacent_edge(
383    tag: TagIdentifier,
384    exec_state: &mut ExecState,
385    args: Args,
386) -> Result<Uuid, KclError> {
387    if args.ctx.no_engine_commands().await {
388        return Ok(exec_state.next_uuid());
389    }
390    let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
391
392    let id = exec_state.next_uuid();
393    let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
394
395    let resp = args
396        .send_modeling_cmd(
397            id,
398            ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge {
399                edge_id: tagged_path.id,
400                object_id: tagged_path.sketch,
401                face_id,
402            }),
403        )
404        .await?;
405    let OkWebSocketResponseData::Modeling {
406        modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
407    } = &resp
408    else {
409        return Err(KclError::Engine(KclErrorDetails {
410            message: format!(
411                "mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {:?}",
412                resp
413            ),
414            source_ranges: vec![args.source_range],
415        }));
416    };
417
418    adjacent_edge.edge.ok_or_else(|| {
419        KclError::Type(KclErrorDetails {
420            message: format!("No edge found previous adjacent to tag: `{}`", tag.value),
421            source_ranges: vec![args.source_range],
422        })
423    })
424}
425
426pub(crate) fn default_tolerance(units: &UnitLength) -> f64 {
427    match units {
428        UnitLength::Mm => 0.0000001,
429        UnitLength::Cm => 0.0000001,
430        UnitLength::In => 0.0000001,
431        UnitLength::Ft => 0.0001,
432        UnitLength::Yd => 0.001,
433        UnitLength::M => 0.001,
434    }
435}