1use 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
19pub 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 let bez_approximate_rational = args
29 .get_kw_arg_opt("bezApproximateRational", &RuntimeType::bool(), exec_state)?
30 .unwrap_or(false);
31 let base_curve_index: Option<u32> = args.get_kw_arg_opt("baseCurveIndex", &RuntimeType::count(), exec_state)?;
33 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 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 let mut sketch = sketches[0].clone();
118 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}