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