1use std::num::NonZeroU32;
4
5use anyhow::Result;
6use kcmc::ModelingCmd;
7use kcmc::each_cmd as mcmd;
8use kcmc::length_unit::LengthUnit;
9use kcmc::shared::BodyType;
10use kittycad_modeling_cmds as kcmc;
11
12use super::DEFAULT_TOLERANCE_MM;
13use super::args::TyF64;
14use crate::errors::KclError;
15use crate::errors::KclErrorDetails;
16use crate::execution::ExecState;
17use crate::execution::ExecutorContext;
18use crate::execution::KclValue;
19use crate::execution::ModelingCmdMeta;
20use crate::execution::ProfileClosed;
21use crate::execution::Sketch;
22use crate::execution::Solid;
23use crate::execution::types::ArrayLen;
24use crate::execution::types::RuntimeType;
25use crate::parsing::ast::types::TagNode;
26use crate::std::Args;
27use crate::std::args::FromKclValue;
28use crate::std::extrude::build_segment_surface_sketch;
29use crate::std::extrude::do_post_extrude;
30
31const DEFAULT_V_DEGREE: u32 = 2;
32
33pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
35 let sketch_values: Vec<KclValue> = args.get_unlabeled_kw_arg(
36 "sketches",
37 &RuntimeType::Array(
38 Box::new(RuntimeType::Union(vec![RuntimeType::sketch(), RuntimeType::segment()])),
39 ArrayLen::Minimum(2),
40 ),
41 exec_state,
42 )?;
43 let v_degree: NonZeroU32 = args
44 .get_kw_arg_opt("vDegree", &RuntimeType::count(), exec_state)?
45 .unwrap_or(NonZeroU32::new(DEFAULT_V_DEGREE).unwrap());
46 let bez_approximate_rational = args
50 .get_kw_arg_opt("bezApproximateRational", &RuntimeType::bool(), exec_state)?
51 .unwrap_or(false);
52 let base_curve_index: Option<u32> = args.get_kw_arg_opt("baseCurveIndex", &RuntimeType::count(), exec_state)?;
54 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
56 let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
57 let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
58 let body_type: Option<BodyType> = args.get_kw_arg_opt("bodyType", &RuntimeType::string(), exec_state)?;
59
60 let sketches = coerce_loft_targets(
61 sketch_values,
62 body_type.unwrap_or_default(),
63 tag_start.as_ref(),
64 tag_end.as_ref(),
65 exec_state,
66 &args.ctx,
67 args.source_range,
68 )
69 .await?;
70 let value = inner_loft(
71 sketches,
72 v_degree,
73 bez_approximate_rational,
74 base_curve_index,
75 tolerance,
76 tag_start,
77 tag_end,
78 body_type,
79 exec_state,
80 args,
81 )
82 .await?;
83 Ok(KclValue::Solid { value })
84}
85
86async fn coerce_loft_targets(
87 sketch_values: Vec<KclValue>,
88 body_type: BodyType,
89 tag_start: Option<&TagNode>,
90 tag_end: Option<&TagNode>,
91 exec_state: &mut ExecState,
92 ctx: &ExecutorContext,
93 source_range: crate::SourceRange,
94) -> Result<Vec<Sketch>, KclError> {
95 let mut sketches = Vec::new();
96 let mut segments = Vec::new();
97
98 for value in sketch_values {
99 if let Some(segment) = value.clone().into_segment() {
100 segments.push(segment);
101 continue;
102 }
103
104 let Some(sketch) = Sketch::from_kcl_val(&value) else {
105 return Err(KclError::new_type(KclErrorDetails::new(
106 "Expected sketches or solved sketch segments for loft.".to_owned(),
107 vec![source_range],
108 )));
109 };
110 sketches.push(sketch);
111 }
112
113 if !segments.is_empty() && !sketches.is_empty() {
114 return Err(KclError::new_semantic(KclErrorDetails::new(
115 "Cannot loft sketch segments together with sketches in the same call. Use separate `loft()` calls."
116 .to_owned(),
117 vec![source_range],
118 )));
119 }
120
121 if !segments.is_empty() {
122 if !matches!(body_type, BodyType::Surface) {
123 return Err(KclError::new_semantic(KclErrorDetails::new(
124 "Lofting sketch segments is only supported for surface lofts. Set `bodyType = SURFACE`.".to_owned(),
125 vec![source_range],
126 )));
127 }
128
129 if tag_start.is_some() || tag_end.is_some() {
130 return Err(KclError::new_semantic(KclErrorDetails::new(
131 "`tagStart` and `tagEnd` are not supported when lofting sketch segments. Segment surface lofts do not create start or end caps."
132 .to_owned(),
133 vec![source_range],
134 )));
135 }
136
137 let mut loft_sections = Vec::with_capacity(segments.len());
138 for segment in segments {
139 loft_sections.push(build_segment_surface_sketch(vec![segment], exec_state, ctx, source_range).await?);
140 }
141 return Ok(loft_sections);
142 }
143
144 Ok(sketches)
145}
146
147#[allow(clippy::too_many_arguments)]
148async fn inner_loft(
149 sketches: Vec<Sketch>,
150 v_degree: NonZeroU32,
151 bez_approximate_rational: bool,
152 base_curve_index: Option<u32>,
153 tolerance: Option<TyF64>,
154 tag_start: Option<TagNode>,
155 tag_end: Option<TagNode>,
156 body_type: Option<BodyType>,
157 exec_state: &mut ExecState,
158 args: Args,
159) -> Result<Box<Solid>, KclError> {
160 let body_type = body_type.unwrap_or_default();
161 if matches!(body_type, BodyType::Solid) && sketches.iter().any(|sk| matches!(sk.is_closed, ProfileClosed::No)) {
162 return Err(KclError::new_semantic(KclErrorDetails::new(
163 "Cannot solid loft an open profile. Either close the profile, or use a surface loft.".to_owned(),
164 vec![args.source_range],
165 )));
166 }
167
168 if sketches.len() < 2 {
170 return Err(KclError::new_semantic(KclErrorDetails::new(
171 format!(
172 "Loft requires at least two sketches, but only {} were provided.",
173 sketches.len()
174 ),
175 vec![args.source_range],
176 )));
177 }
178
179 let id = exec_state.next_uuid();
180 exec_state
181 .batch_modeling_cmd(
182 ModelingCmdMeta::from_args_id(exec_state, &args, id),
183 ModelingCmd::from(if let Some(base_curve_index) = base_curve_index {
184 mcmd::Loft::builder()
185 .section_ids(sketches.iter().map(|group| group.id).collect())
186 .bez_approximate_rational(bez_approximate_rational)
187 .tolerance(LengthUnit(
188 tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM),
189 ))
190 .v_degree(v_degree)
191 .body_type(body_type)
192 .base_curve_index(base_curve_index)
193 .build()
194 } else {
195 mcmd::Loft::builder()
196 .section_ids(sketches.iter().map(|group| group.id).collect())
197 .bez_approximate_rational(bez_approximate_rational)
198 .tolerance(LengthUnit(
199 tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM),
200 ))
201 .v_degree(v_degree)
202 .body_type(body_type)
203 .build()
204 }),
205 )
206 .await?;
207
208 let mut sketch = sketches[0].clone();
210 sketch.id = id;
212 Ok(Box::new(
213 do_post_extrude(
214 &sketch,
215 id.into(),
216 false,
217 &super::extrude::NamedCapTags {
218 start: tag_start.as_ref(),
219 end: tag_end.as_ref(),
220 },
221 kittycad_modeling_cmds::shared::ExtrudeMethod::Merge,
222 exec_state,
223 &args,
224 None,
225 None,
226 body_type,
227 crate::std::extrude::BeingExtruded::Sketch,
228 )
229 .await?,
230 ))
231}
232
233#[cfg(test)]
234mod tests {
235 use kittycad_modeling_cmds::units::UnitLength;
236
237 use super::*;
238 use crate::execution::AbstractSegment;
239 use crate::execution::Plane;
240 use crate::execution::Segment;
241 use crate::execution::SegmentKind;
242 use crate::execution::SegmentRepr;
243 use crate::execution::SketchSurface;
244 use crate::execution::types::NumericType;
245 use crate::front::Expr;
246 use crate::front::LineCtor;
247 use crate::front::Number;
248 use crate::front::ObjectId;
249 use crate::front::Point2d;
250 use crate::parsing::ast::types::TagDeclarator;
251 use crate::std::sketch::PlaneData;
252
253 fn point_expr(x: f64, y: f64) -> Point2d<Expr> {
254 Point2d {
255 x: Expr::Var(Number::from((x, UnitLength::Millimeters))),
256 y: Expr::Var(Number::from((y, UnitLength::Millimeters))),
257 }
258 }
259
260 fn line_segment_value(exec_state: &mut ExecState, plane_data: PlaneData, object_id_seed: usize) -> KclValue {
261 let plane = Plane::from_plane_data_skipping_engine(plane_data, exec_state).unwrap();
262 let start = [TyF64::new(-2.0, NumericType::mm()), TyF64::new(0.0, NumericType::mm())];
263 let end = [TyF64::new(2.0, NumericType::mm()), TyF64::new(0.0, NumericType::mm())];
264 let segment = Segment {
265 id: exec_state.next_uuid(),
266 object_id: ObjectId(object_id_seed),
267 kind: SegmentKind::Line {
268 start,
269 end,
270 ctor: Box::new(LineCtor {
271 start: point_expr(-2.0, 0.0),
272 end: point_expr(2.0, 0.0),
273 construction: None,
274 }),
275 start_object_id: ObjectId(object_id_seed + 1),
276 end_object_id: ObjectId(object_id_seed + 2),
277 start_freedom: None,
278 end_freedom: None,
279 construction: false,
280 },
281 surface: SketchSurface::Plane(Box::new(plane)),
282 sketch_id: exec_state.next_uuid(),
283 sketch: None,
284 tag: None,
285 meta: vec![],
286 node_path: None,
287 };
288 KclValue::Segment {
289 value: Box::new(AbstractSegment {
290 repr: SegmentRepr::Solved {
291 segment: Box::new(segment),
292 },
293 meta: vec![],
294 }),
295 }
296 }
297
298 #[tokio::test(flavor = "multi_thread")]
299 async fn segment_loft_supports_sections_from_different_sketches() {
300 let ctx = ExecutorContext::new_mock(None).await;
301 let mut exec_state = ExecState::new(&ctx);
302 let sketches = coerce_loft_targets(
303 vec![
304 line_segment_value(&mut exec_state, PlaneData::XY, 1),
305 line_segment_value(&mut exec_state, PlaneData::NegXY, 10),
306 line_segment_value(&mut exec_state, PlaneData::XZ, 20),
307 ],
308 BodyType::Surface,
309 None,
310 None,
311 &mut exec_state,
312 &ctx,
313 crate::SourceRange::default(),
314 )
315 .await
316 .unwrap();
317
318 assert_eq!(sketches.len(), 3);
319 assert!(sketches.iter().all(|sketch| sketch.paths.len() == 1));
320 assert_ne!(sketches[0].id, sketches[1].id);
321 assert_ne!(sketches[1].id, sketches[2].id);
322 ctx.close().await;
323 }
324
325 #[tokio::test(flavor = "multi_thread")]
326 async fn segment_loft_rejects_cap_tags() {
327 let ctx = ExecutorContext::new_mock(None).await;
328 let mut exec_state = ExecState::new(&ctx);
329 let err = coerce_loft_targets(
330 vec![line_segment_value(&mut exec_state, PlaneData::XY, 1)],
331 BodyType::Surface,
332 Some(&TagDeclarator::new("cap_start")),
333 None,
334 &mut exec_state,
335 &ctx,
336 crate::SourceRange::default(),
337 )
338 .await
339 .unwrap_err();
340
341 assert!(
342 err.message()
343 .contains("`tagStart` and `tagEnd` are not supported when lofting sketch segments"),
344 "{err:?}"
345 );
346 ctx.close().await;
347 }
348}