kcl_lib/std/
loft.rs

1//! Standard library lofts.
2
3use std::num::NonZeroU32;
4
5use anyhow::Result;
6use kcmc::{ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, shared::BodyType};
7use kittycad_modeling_cmds as kcmc;
8
9use super::{DEFAULT_TOLERANCE_MM, args::TyF64};
10use crate::{
11    errors::{KclError, KclErrorDetails},
12    execution::{ExecState, KclValue, ModelingCmdMeta, ProfileClosed, Sketch, Solid, types::RuntimeType},
13    parsing::ast::types::TagNode,
14    std::{Args, extrude::do_post_extrude},
15};
16
17const DEFAULT_V_DEGREE: u32 = 2;
18
19/// Create a 3D surface or solid by interpolating between two or more sketches.
20pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
21    let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
22    let v_degree: NonZeroU32 = args
23        .get_kw_arg_opt("vDegree", &RuntimeType::count(), exec_state)?
24        .unwrap_or(NonZeroU32::new(DEFAULT_V_DEGREE).unwrap());
25    // Attempt to approximate rational curves (such as arcs) using a bezier.
26    // This will remove banding around interpolations between arcs and non-arcs.  It may produce errors in other scenarios
27    // Over time, this field won't be necessary.
28    let bez_approximate_rational = args
29        .get_kw_arg_opt("bezApproximateRational", &RuntimeType::bool(), exec_state)?
30        .unwrap_or(false);
31    // This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
32    let base_curve_index: Option<u32> = args.get_kw_arg_opt("baseCurveIndex", &RuntimeType::count(), exec_state)?;
33    // Tolerance for the loft operation.
34    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
35    let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
36    let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
37    let body_type: Option<BodyType> = args.get_kw_arg_opt("bodyType", &RuntimeType::string(), exec_state)?;
38
39    let value = inner_loft(
40        sketches,
41        v_degree,
42        bez_approximate_rational,
43        base_curve_index,
44        tolerance,
45        tag_start,
46        tag_end,
47        body_type,
48        exec_state,
49        args,
50    )
51    .await?;
52    Ok(KclValue::Solid { value })
53}
54
55#[allow(clippy::too_many_arguments)]
56async fn inner_loft(
57    sketches: Vec<Sketch>,
58    v_degree: NonZeroU32,
59    bez_approximate_rational: bool,
60    base_curve_index: Option<u32>,
61    tolerance: Option<TyF64>,
62    tag_start: Option<TagNode>,
63    tag_end: Option<TagNode>,
64    body_type: Option<BodyType>,
65    exec_state: &mut ExecState,
66    args: Args,
67) -> Result<Box<Solid>, KclError> {
68    let body_type = body_type.unwrap_or_default();
69    if matches!(body_type, BodyType::Solid) && sketches.iter().any(|sk| matches!(sk.is_closed, ProfileClosed::No)) {
70        return Err(KclError::new_semantic(KclErrorDetails::new(
71            "Cannot solid loft an open profile. Either close the profile, or use a surface loft.".to_owned(),
72            vec![args.source_range],
73        )));
74    }
75
76    // Make sure we have at least two sketches.
77    if sketches.len() < 2 {
78        return Err(KclError::new_semantic(KclErrorDetails::new(
79            format!(
80                "Loft requires at least two sketches, but only {} were provided.",
81                sketches.len()
82            ),
83            vec![args.source_range],
84        )));
85    }
86
87    let id = exec_state.next_uuid();
88    exec_state
89        .batch_modeling_cmd(
90            ModelingCmdMeta::from_args_id(exec_state, &args, id),
91            ModelingCmd::from(if let Some(base_curve_index) = base_curve_index {
92                mcmd::Loft::builder()
93                    .section_ids(sketches.iter().map(|group| group.id).collect())
94                    .bez_approximate_rational(bez_approximate_rational)
95                    .tolerance(LengthUnit(
96                        tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM),
97                    ))
98                    .v_degree(v_degree)
99                    .body_type(body_type)
100                    .base_curve_index(base_curve_index)
101                    .build()
102            } else {
103                mcmd::Loft::builder()
104                    .section_ids(sketches.iter().map(|group| group.id).collect())
105                    .bez_approximate_rational(bez_approximate_rational)
106                    .tolerance(LengthUnit(
107                        tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM),
108                    ))
109                    .v_degree(v_degree)
110                    .body_type(body_type)
111                    .build()
112            }),
113        )
114        .await?;
115
116    // Using the first sketch as the base curve, idk we might want to change this later.
117    let mut sketch = sketches[0].clone();
118    // Override its id with the loft id so we can get its faces later
119    sketch.id = id;
120    Ok(Box::new(
121        do_post_extrude(
122            &sketch,
123            id.into(),
124            false,
125            &super::extrude::NamedCapTags {
126                start: tag_start.as_ref(),
127                end: tag_end.as_ref(),
128            },
129            kittycad_modeling_cmds::shared::ExtrudeMethod::Merge,
130            exec_state,
131            &args,
132            None,
133            None,
134            body_type,
135        )
136        .await?,
137    ))
138}