1use 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
23pub 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 let bez_approximate_rational = args.get_kw_arg_opt("bezApproximateRational")?.unwrap_or(false);
33 let base_curve_index: Option<u32> = args.get_kw_arg_opt("baseCurveIndex")?;
35 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#[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 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 let mut sketch = sketches[0].clone();
174 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}