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