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([triangleSketch, squareSketch])
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    unlabeled_first = true,
125    args = {
126        sketches = {docs = "Which sketches to loft. Must include at least 2 sketches."},
127        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."},
128        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."},
129        base_curve_index = {docs = "This can be set to override the automatically determined topological base curve, which is usually the first section encountered."},
130        tolerance = {docs = "Tolerance for the loft operation."},
131        tag_start = { docs = "A named tag for the face at the start of the loft, i.e. the original sketch" },
132        tag_end = { docs = "A named tag for the face at the end of the loft, i.e. the last sketch" },
133    },
134    tags = ["sketch"]
135}]
136#[allow(clippy::too_many_arguments)]
137async fn inner_loft(
138    sketches: Vec<Sketch>,
139    v_degree: NonZeroU32,
140    bez_approximate_rational: bool,
141    base_curve_index: Option<u32>,
142    tolerance: Option<TyF64>,
143    tag_start: Option<TagNode>,
144    tag_end: Option<TagNode>,
145    exec_state: &mut ExecState,
146    args: Args,
147) -> Result<Box<Solid>, KclError> {
148    // Make sure we have at least two sketches.
149    if sketches.len() < 2 {
150        return Err(KclError::Semantic(KclErrorDetails::new(
151            format!(
152                "Loft requires at least two sketches, but only {} were provided.",
153                sketches.len()
154            ),
155            vec![args.source_range],
156        )));
157    }
158
159    let id = exec_state.next_uuid();
160    args.batch_modeling_cmd(
161        id,
162        ModelingCmd::from(mcmd::Loft {
163            section_ids: sketches.iter().map(|group| group.id).collect(),
164            base_curve_index,
165            bez_approximate_rational,
166            tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
167            v_degree,
168        }),
169    )
170    .await?;
171
172    // Using the first sketch as the base curve, idk we might want to change this later.
173    let mut sketch = sketches[0].clone();
174    // Override its id with the loft id so we can get its faces later
175    sketch.id = id;
176    Ok(Box::new(
177        do_post_extrude(
178            &sketch,
179            id.into(),
180            TyF64::new(0.0, NumericType::mm()),
181            false,
182            &super::extrude::NamedCapTags {
183                start: tag_start.as_ref(),
184                end: tag_end.as_ref(),
185            },
186            exec_state,
187            &args,
188            None,
189        )
190        .await?,
191    ))
192}