1use std::collections::HashMap;
4
5use anyhow::Result;
6use kcmc::{
7 ModelingCmd, each_cmd as mcmd,
8 length_unit::LengthUnit,
9 ok_response::OkModelingCmdResponse,
10 output::ExtrusionFaceInfo,
11 shared::{ExtrusionFaceCapType, Opposite},
12 websocket::{ModelingCmdReq, OkWebSocketResponseData},
13};
14use kittycad_modeling_cmds::{
15 self as kcmc,
16 shared::{Angle, Point2d},
17};
18use uuid::Uuid;
19
20use super::{DEFAULT_TOLERANCE_MM, args::TyF64, utils::point_to_mm};
21use crate::{
22 errors::{KclError, KclErrorDetails},
23 execution::{
24 ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta, Path, Sketch, SketchSurface, Solid,
25 types::RuntimeType,
26 },
27 parsing::ast::types::TagNode,
28 std::Args,
29};
30
31pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
33 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
34 let length: TyF64 = args.get_kw_arg("length", &RuntimeType::length(), exec_state)?;
35 let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
36 let bidirectional_length: Option<TyF64> =
37 args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
38 let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
39 let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
40 let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
41 let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
42 let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
43 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
44
45 let result = inner_extrude(
46 sketches,
47 length,
48 symmetric,
49 bidirectional_length,
50 tag_start,
51 tag_end,
52 twist_angle,
53 twist_angle_step,
54 twist_center,
55 tolerance,
56 exec_state,
57 args,
58 )
59 .await?;
60
61 Ok(result.into())
62}
63
64#[allow(clippy::too_many_arguments)]
65async fn inner_extrude(
66 sketches: Vec<Sketch>,
67 length: TyF64,
68 symmetric: Option<bool>,
69 bidirectional_length: Option<TyF64>,
70 tag_start: Option<TagNode>,
71 tag_end: Option<TagNode>,
72 twist_angle: Option<TyF64>,
73 twist_angle_step: Option<TyF64>,
74 twist_center: Option<[TyF64; 2]>,
75 tolerance: Option<TyF64>,
76 exec_state: &mut ExecState,
77 args: Args,
78) -> Result<Vec<Solid>, KclError> {
79 let mut solids = Vec::new();
81 let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
82
83 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
84 return Err(KclError::new_semantic(KclErrorDetails::new(
85 "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
86 .to_owned(),
87 vec![args.source_range],
88 )));
89 }
90
91 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
92
93 let opposite = match (symmetric, bidirection) {
94 (Some(true), _) => Opposite::Symmetric,
95 (None, None) => Opposite::None,
96 (Some(false), None) => Opposite::None,
97 (None, Some(length)) => Opposite::Other(length),
98 (Some(false), Some(length)) => Opposite::Other(length),
99 };
100
101 for sketch in &sketches {
102 let id = exec_state.next_uuid();
103 let cmd = match (&twist_angle, &twist_angle_step, &twist_center) {
104 (Some(angle), angle_step, center) => {
105 let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
106 let total_rotation_angle = Angle::from_degrees(angle.to_degrees());
107 let angle_step_size = Angle::from_degrees(angle_step.clone().map(|a| a.to_degrees()).unwrap_or(15.0));
108 ModelingCmd::from(mcmd::TwistExtrude {
109 target: sketch.id.into(),
110 distance: LengthUnit(length.to_mm()),
111 faces: Default::default(),
112 center_2d: center,
113 total_rotation_angle,
114 angle_step_size,
115 tolerance,
116 })
117 }
118 (None, _, _) => ModelingCmd::from(mcmd::Extrude {
119 target: sketch.id.into(),
120 distance: LengthUnit(length.to_mm()),
121 faces: Default::default(),
122 opposite: opposite.clone(),
123 }),
124 };
125 let cmds = sketch.build_sketch_mode_cmds(exec_state, ModelingCmdReq { cmd_id: id.into(), cmd });
126 exec_state
127 .batch_modeling_cmds(ModelingCmdMeta::from_args_id(&args, id), &cmds)
128 .await?;
129
130 solids.push(
131 do_post_extrude(
132 sketch,
133 id.into(),
134 length.clone(),
135 false,
136 &NamedCapTags {
137 start: tag_start.as_ref(),
138 end: tag_end.as_ref(),
139 },
140 exec_state,
141 &args,
142 None,
143 )
144 .await?,
145 );
146 }
147
148 Ok(solids)
149}
150
151#[derive(Debug, Default)]
152pub(crate) struct NamedCapTags<'a> {
153 pub start: Option<&'a TagNode>,
154 pub end: Option<&'a TagNode>,
155}
156
157#[allow(clippy::too_many_arguments)]
158pub(crate) async fn do_post_extrude<'a>(
159 sketch: &Sketch,
160 solid_id: ArtifactId,
161 length: TyF64,
162 sectional: bool,
163 named_cap_tags: &'a NamedCapTags<'a>,
164 exec_state: &mut ExecState,
165 args: &Args,
166 edge_id: Option<Uuid>,
167) -> Result<Solid, KclError> {
168 exec_state
171 .batch_modeling_cmd(
172 args.into(),
173 ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
174 )
175 .await?;
176
177 let any_edge_id = if let Some(edge_id) = sketch.mirror {
178 edge_id
179 } else if let Some(id) = edge_id {
180 id
181 } else {
182 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
185 return Err(KclError::new_type(KclErrorDetails::new(
186 "Expected a non-empty sketch".to_owned(),
187 vec![args.source_range],
188 )));
189 };
190 any_edge_id
191 };
192
193 let mut sketch = sketch.clone();
194
195 if let SketchSurface::Face(ref face) = sketch.on {
197 sketch.id = face.solid.sketch.id;
198 }
199
200 let solid3d_info = exec_state
201 .send_modeling_cmd(
202 args.into(),
203 ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
204 edge_id: any_edge_id,
205 object_id: sketch.id,
206 }),
207 )
208 .await?;
209
210 let face_infos = if let OkWebSocketResponseData::Modeling {
211 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
212 } = solid3d_info
213 {
214 data.faces
215 } else {
216 vec![]
217 };
218
219 #[cfg(feature = "artifact-graph")]
221 {
222 if !sectional {
225 exec_state
226 .batch_modeling_cmd(
227 args.into(),
228 ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
229 object_id: sketch.id,
230 edge_id: any_edge_id,
231 }),
232 )
233 .await?;
234 }
235 }
236
237 let Faces {
238 sides: face_id_map,
239 start_cap_id,
240 end_cap_id,
241 } = analyze_faces(exec_state, args, face_infos).await;
242
243 let no_engine_commands = args.ctx.no_engine_commands().await;
245 let mut new_value: Vec<ExtrudeSurface> = sketch
246 .paths
247 .iter()
248 .flat_map(|path| {
249 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
250 match path {
251 Path::Arc { .. }
252 | Path::TangentialArc { .. }
253 | Path::TangentialArcTo { .. }
254 | Path::Circle { .. }
255 | Path::CircleThreePoint { .. } => {
256 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
257 face_id: *actual_face_id,
258 tag: path.get_base().tag.clone(),
259 geo_meta: GeoMeta {
260 id: path.get_base().geo_meta.id,
261 metadata: path.get_base().geo_meta.metadata,
262 },
263 });
264 Some(extrude_surface)
265 }
266 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
267 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
268 face_id: *actual_face_id,
269 tag: path.get_base().tag.clone(),
270 geo_meta: GeoMeta {
271 id: path.get_base().geo_meta.id,
272 metadata: path.get_base().geo_meta.metadata,
273 },
274 });
275 Some(extrude_surface)
276 }
277 Path::ArcThreePoint { .. } => {
278 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
279 face_id: *actual_face_id,
280 tag: path.get_base().tag.clone(),
281 geo_meta: GeoMeta {
282 id: path.get_base().geo_meta.id,
283 metadata: path.get_base().geo_meta.metadata,
284 },
285 });
286 Some(extrude_surface)
287 }
288 }
289 } else if no_engine_commands {
290 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
293 face_id: exec_state.next_uuid(),
295 tag: path.get_base().tag.clone(),
296 geo_meta: GeoMeta {
297 id: path.get_base().geo_meta.id,
298 metadata: path.get_base().geo_meta.metadata,
299 },
300 });
301 Some(extrude_surface)
302 } else {
303 None
304 }
305 })
306 .collect();
307
308 if let Some(tag_start) = named_cap_tags.start {
310 let Some(start_cap_id) = start_cap_id else {
311 return Err(KclError::new_type(KclErrorDetails::new(
312 format!(
313 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
314 tag_start.name, sketch.id
315 ),
316 vec![args.source_range],
317 )));
318 };
319
320 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
321 face_id: start_cap_id,
322 tag: Some(tag_start.clone()),
323 geo_meta: GeoMeta {
324 id: start_cap_id,
325 metadata: args.source_range.into(),
326 },
327 }));
328 }
329 if let Some(tag_end) = named_cap_tags.end {
330 let Some(end_cap_id) = end_cap_id else {
331 return Err(KclError::new_type(KclErrorDetails::new(
332 format!(
333 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
334 tag_end.name, sketch.id
335 ),
336 vec![args.source_range],
337 )));
338 };
339
340 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
341 face_id: end_cap_id,
342 tag: Some(tag_end.clone()),
343 geo_meta: GeoMeta {
344 id: end_cap_id,
345 metadata: args.source_range.into(),
346 },
347 }));
348 }
349
350 Ok(Solid {
351 id: sketch.id,
355 artifact_id: solid_id,
356 value: new_value,
357 meta: sketch.meta.clone(),
358 units: sketch.units,
359 height: length.to_length_units(sketch.units),
360 sectional,
361 sketch,
362 start_cap_id,
363 end_cap_id,
364 edge_cuts: vec![],
365 })
366}
367
368#[derive(Default)]
369struct Faces {
370 sides: HashMap<Uuid, Option<Uuid>>,
372 end_cap_id: Option<Uuid>,
374 start_cap_id: Option<Uuid>,
376}
377
378async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
379 let mut faces = Faces {
380 sides: HashMap::with_capacity(face_infos.len()),
381 ..Default::default()
382 };
383 if args.ctx.no_engine_commands().await {
384 faces.start_cap_id = Some(exec_state.next_uuid());
386 faces.end_cap_id = Some(exec_state.next_uuid());
387 }
388 for face_info in face_infos {
389 match face_info.cap {
390 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
391 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
392 ExtrusionFaceCapType::Both => {
393 faces.end_cap_id = face_info.face_id;
394 faces.start_cap_id = face_info.face_id;
395 }
396 ExtrusionFaceCapType::None => {
397 if let Some(curve_id) = face_info.curve_id {
398 faces.sides.insert(curve_id, face_info.face_id);
399 }
400 }
401 }
402 }
403 faces
404}