kcl_lib/std/
loft.rs

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