kcl_lib/std/
loft.rs

1//! Standard library lofts.
2
3use std::num::NonZeroU32;
4
5use anyhow::Result;
6use derive_docs::stdlib;
7use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
8use kittycad_modeling_cmds as kcmc;
9
10use crate::{
11    errors::{KclError, KclErrorDetails},
12    execution::{ExecState, KclValue, Sketch, Solid},
13    std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
14};
15
16const DEFAULT_V_DEGREE: u32 = 2;
17
18/// Create a 3D surface or solid by interpolating between two or more sketches.
19pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
20    let sketches = args.get_unlabeled_kw_arg("sketches")?;
21    let v_degree: NonZeroU32 = args
22        .get_kw_arg_opt("vDegree")?
23        .unwrap_or(NonZeroU32::new(DEFAULT_V_DEGREE).unwrap());
24    // Attempt to approximate rational curves (such as arcs) using a bezier.
25    // This will remove banding around interpolations between arcs and non-arcs.  It may produce errors in other scenarios
26    // Over time, this field won't be necessary.
27    let bez_approximate_rational = args.get_kw_arg_opt("bezApproximateRational")?.unwrap_or(false);
28    // This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
29    let base_curve_index: Option<u32> = args.get_kw_arg_opt("baseCurveIndex")?;
30    // Tolerance for the loft operation.
31    let tolerance: Option<f64> = args.get_kw_arg_opt("tolerance")?;
32
33    let value = inner_loft(
34        sketches,
35        v_degree,
36        bez_approximate_rational,
37        base_curve_index,
38        tolerance,
39        exec_state,
40        args,
41    )
42    .await?;
43    Ok(KclValue::Solid { value })
44}
45
46/// Create a 3D surface or solid by interpolating between two or more sketches.
47///
48/// The sketches need to closed and on the same plane.
49///
50/// ```no_run
51/// // Loft a square and a triangle.
52/// squareSketch = startSketchOn('XY')
53///     |> startProfileAt([-100, 200], %)
54///     |> line(end = [200, 0])
55///     |> line(end = [0, -200])
56///     |> line(end = [-200, 0])
57///     |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
58///     |> close()
59///
60/// triangleSketch = startSketchOn(offsetPlane('XY', offset = 75))
61///     |> startProfileAt([0, 125], %)
62///     |> line(end = [-15, -30])
63///     |> line(end = [30, 0])
64///     |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
65///     |> close()
66///
67/// loft([squareSketch, triangleSketch])
68/// ```
69///
70/// ```no_run
71/// // Loft a square, a circle, and another circle.
72/// squareSketch = startSketchOn('XY')
73///     |> startProfileAt([-100, 200], %)
74///     |> line(end = [200, 0])
75///     |> line(end = [0, -200])
76///     |> line(end = [-200, 0])
77///     |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
78///     |> close()
79///
80/// circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
81///     |> circle({ center = [0, 100], radius = 50 }, %)
82///
83/// circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
84///     |> circle({ center = [0, 100], radius = 20 }, %)
85///
86/// loft([squareSketch, circleSketch0, circleSketch1])
87/// ```
88///
89/// ```no_run
90/// // Loft a square, a circle, and another circle with options.
91/// squareSketch = startSketchOn('XY')
92///     |> startProfileAt([-100, 200], %)
93///     |> line(end = [200, 0])
94///     |> line(end = [0, -200])
95///     |> line(end = [-200, 0])
96///     |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
97///     |> close()
98///
99/// circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
100///     |> circle({ center = [0, 100], radius = 50 }, %)
101///
102/// circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
103///     |> circle({ center = [0, 100], radius = 20 }, %)
104///
105/// loft([squareSketch, circleSketch0, circleSketch1],
106///     baseCurveIndex = 0,
107///     bezApproximateRational = false,
108///     tolerance = 0.000001,
109///     vDegree = 2,
110/// )
111/// ```
112#[stdlib {
113    name = "loft",
114    feature_tree_operation = true,
115    keywords = true,
116    unlabeled_first = true,
117    args = {
118        sketches = {docs = "Which sketches to loft. Must include at least 2 sketches."},
119        v_degree = {docs = "Degree of the interpolation. Must be greater than zero. For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction. This defaults to 2, if not specified."},
120        bez_approximate_rational = {docs = "Attempt to approximate rational curves (such as arcs) using a bezier. This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios Over time, this field won't be necessary."},
121        base_curve_index = {docs = "This can be set to override the automatically determined topological base curve, which is usually the first section encountered."},
122        tolerance = {docs = "Tolerance for the loft operation."},
123    }
124}]
125async fn inner_loft(
126    sketches: Vec<Sketch>,
127    v_degree: NonZeroU32,
128    bez_approximate_rational: bool,
129    base_curve_index: Option<u32>,
130    tolerance: Option<f64>,
131    exec_state: &mut ExecState,
132    args: Args,
133) -> Result<Box<Solid>, KclError> {
134    // Make sure we have at least two sketches.
135    if sketches.len() < 2 {
136        return Err(KclError::Semantic(KclErrorDetails {
137            message: format!(
138                "Loft requires at least two sketches, but only {} were provided.",
139                sketches.len()
140            ),
141            source_ranges: vec![args.source_range],
142        }));
143    }
144
145    let id = exec_state.next_uuid();
146    args.batch_modeling_cmd(
147        id,
148        ModelingCmd::from(mcmd::Loft {
149            section_ids: sketches.iter().map(|group| group.id).collect(),
150            base_curve_index,
151            bez_approximate_rational,
152            tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
153            v_degree,
154        }),
155    )
156    .await?;
157
158    // Using the first sketch as the base curve, idk we might want to change this later.
159    let mut sketch = sketches[0].clone();
160    // Override its id with the loft id so we can get its faces later
161    sketch.id = id;
162    do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await
163}