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 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 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 let mut sketch = sketches[0].clone();
175 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}