1use std::collections::HashMap;
4
5use anyhow::Result;
6use kcl_derive_docs::stdlib;
7use kcmc::{
8 each_cmd as mcmd,
9 length_unit::LengthUnit,
10 ok_response::OkModelingCmdResponse,
11 output::ExtrusionFaceInfo,
12 shared::{ExtrusionFaceCapType, Opposite},
13 websocket::{ModelingCmdReq, OkWebSocketResponseData},
14 ModelingCmd,
15};
16use kittycad_modeling_cmds::{self as kcmc};
17use uuid::Uuid;
18
19use super::args::TyF64;
20use crate::{
21 errors::{KclError, KclErrorDetails},
22 execution::{
23 types::RuntimeType, ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSurface,
24 Solid,
25 },
26 parsing::ast::types::TagNode,
27 std::Args,
28};
29
30pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
32 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
33 let length: TyF64 = args.get_kw_arg_typed("length", &RuntimeType::length(), exec_state)?;
34 let symmetric = args.get_kw_arg_opt("symmetric")?;
35 let bidirectional_length: Option<TyF64> =
36 args.get_kw_arg_opt_typed("bidirectionalLength", &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 result = inner_extrude(
41 sketches,
42 length,
43 symmetric,
44 bidirectional_length,
45 tag_start,
46 tag_end,
47 exec_state,
48 args,
49 )
50 .await?;
51
52 Ok(result.into())
53}
54
55#[stdlib {
149 name = "extrude",
150 feature_tree_operation = true,
151 unlabeled_first = true,
152 args = {
153 sketches = { docs = "Which sketch or sketches should be extruded"},
154 length = { docs = "How far to extrude the given sketches"},
155 symmetric = { docs = "If true, the extrusion will happen symmetrically around the sketch. Otherwise, the extrusion will happen on only one side of the sketch." },
156 bidirectional_length = { docs = "If specified, will also extrude in the opposite direction to 'distance' to the specified distance. If 'symmetric' is true, this value is ignored."},
157 tag_start = { docs = "A named tag for the face at the start of the extrusion, i.e. the original sketch" },
158 tag_end = { docs = "A named tag for the face at the end of the extrusion, i.e. the new face created by extruding the original sketch" },
159 },
160 tags = ["sketch"]
161}]
162#[allow(clippy::too_many_arguments)]
163async fn inner_extrude(
164 sketches: Vec<Sketch>,
165 length: TyF64,
166 symmetric: Option<bool>,
167 bidirectional_length: Option<TyF64>,
168 tag_start: Option<TagNode>,
169 tag_end: Option<TagNode>,
170 exec_state: &mut ExecState,
171 args: Args,
172) -> Result<Vec<Solid>, KclError> {
173 let mut solids = Vec::new();
175
176 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
177 return Err(KclError::Semantic(KclErrorDetails::new(
178 "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
179 .to_owned(),
180 vec![args.source_range],
181 )));
182 }
183
184 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
185
186 let opposite = match (symmetric, bidirection) {
187 (Some(true), _) => Opposite::Symmetric,
188 (None, None) => Opposite::None,
189 (Some(false), None) => Opposite::None,
190 (None, Some(length)) => Opposite::Other(length),
191 (Some(false), Some(length)) => Opposite::Other(length),
192 };
193
194 for sketch in &sketches {
195 let id = exec_state.next_uuid();
196 args.batch_modeling_cmds(&sketch.build_sketch_mode_cmds(
197 exec_state,
198 ModelingCmdReq {
199 cmd_id: id.into(),
200 cmd: ModelingCmd::from(mcmd::Extrude {
201 target: sketch.id.into(),
202 distance: LengthUnit(length.to_mm()),
203 faces: Default::default(),
204 opposite: opposite.clone(),
205 }),
206 },
207 ))
208 .await?;
209
210 solids.push(
211 do_post_extrude(
212 sketch,
213 id.into(),
214 length.clone(),
215 false,
216 &NamedCapTags {
217 start: tag_start.as_ref(),
218 end: tag_end.as_ref(),
219 },
220 exec_state,
221 &args,
222 None,
223 )
224 .await?,
225 );
226 }
227
228 Ok(solids)
229}
230
231#[derive(Debug, Default)]
232pub(crate) struct NamedCapTags<'a> {
233 pub start: Option<&'a TagNode>,
234 pub end: Option<&'a TagNode>,
235}
236
237#[allow(clippy::too_many_arguments)]
238pub(crate) async fn do_post_extrude<'a>(
239 sketch: &Sketch,
240 solid_id: ArtifactId,
241 length: TyF64,
242 sectional: bool,
243 named_cap_tags: &'a NamedCapTags<'a>,
244 exec_state: &mut ExecState,
245 args: &Args,
246 edge_id: Option<Uuid>,
247) -> Result<Solid, KclError> {
248 args.batch_modeling_cmd(
251 exec_state.next_uuid(),
252 ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
253 )
254 .await?;
255
256 let any_edge_id = if let Some(id) = edge_id {
257 id
258 } else if let Some(edge_id) = sketch.mirror {
259 edge_id
260 } else {
261 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
264 return Err(KclError::Type(KclErrorDetails::new(
265 "Expected a non-empty sketch".to_owned(),
266 vec![args.source_range],
267 )));
268 };
269 any_edge_id
270 };
271
272 let mut sketch = sketch.clone();
273
274 if let SketchSurface::Face(ref face) = sketch.on {
276 sketch.id = face.solid.sketch.id;
277 }
278
279 let solid3d_info = args
280 .send_modeling_cmd(
281 exec_state.next_uuid(),
282 ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
283 edge_id: any_edge_id,
284 object_id: sketch.id,
285 }),
286 )
287 .await?;
288
289 let face_infos = if let OkWebSocketResponseData::Modeling {
290 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
291 } = solid3d_info
292 {
293 data.faces
294 } else {
295 vec![]
296 };
297
298 #[cfg(feature = "artifact-graph")]
300 {
301 if !sectional {
304 args.batch_modeling_cmd(
305 exec_state.next_uuid(),
306 ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
307 object_id: sketch.id,
308 edge_id: any_edge_id,
309 }),
310 )
311 .await?;
312 }
313 }
314
315 let Faces {
316 sides: face_id_map,
317 start_cap_id,
318 end_cap_id,
319 } = analyze_faces(exec_state, args, face_infos).await;
320
321 let no_engine_commands = args.ctx.no_engine_commands().await;
323 let mut new_value: Vec<ExtrudeSurface> = sketch
324 .paths
325 .iter()
326 .flat_map(|path| {
327 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
328 match path {
329 Path::Arc { .. }
330 | Path::TangentialArc { .. }
331 | Path::TangentialArcTo { .. }
332 | Path::Circle { .. }
333 | Path::CircleThreePoint { .. } => {
334 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
335 face_id: *actual_face_id,
336 tag: path.get_base().tag.clone(),
337 geo_meta: GeoMeta {
338 id: path.get_base().geo_meta.id,
339 metadata: path.get_base().geo_meta.metadata,
340 },
341 });
342 Some(extrude_surface)
343 }
344 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
345 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
346 face_id: *actual_face_id,
347 tag: path.get_base().tag.clone(),
348 geo_meta: GeoMeta {
349 id: path.get_base().geo_meta.id,
350 metadata: path.get_base().geo_meta.metadata,
351 },
352 });
353 Some(extrude_surface)
354 }
355 Path::ArcThreePoint { .. } => {
356 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
357 face_id: *actual_face_id,
358 tag: path.get_base().tag.clone(),
359 geo_meta: GeoMeta {
360 id: path.get_base().geo_meta.id,
361 metadata: path.get_base().geo_meta.metadata,
362 },
363 });
364 Some(extrude_surface)
365 }
366 }
367 } else if no_engine_commands {
368 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
371 face_id: exec_state.next_uuid(),
373 tag: path.get_base().tag.clone(),
374 geo_meta: GeoMeta {
375 id: path.get_base().geo_meta.id,
376 metadata: path.get_base().geo_meta.metadata,
377 },
378 });
379 Some(extrude_surface)
380 } else {
381 None
382 }
383 })
384 .collect();
385
386 if let Some(tag_start) = named_cap_tags.start {
388 let Some(start_cap_id) = start_cap_id else {
389 return Err(KclError::Type(KclErrorDetails::new(
390 format!(
391 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
392 tag_start.name, sketch.id
393 ),
394 vec![args.source_range],
395 )));
396 };
397
398 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
399 face_id: start_cap_id,
400 tag: Some(tag_start.clone()),
401 geo_meta: GeoMeta {
402 id: start_cap_id,
403 metadata: args.source_range.into(),
404 },
405 }));
406 }
407 if let Some(tag_end) = named_cap_tags.end {
408 let Some(end_cap_id) = end_cap_id else {
409 return Err(KclError::Type(KclErrorDetails::new(
410 format!(
411 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
412 tag_end.name, sketch.id
413 ),
414 vec![args.source_range],
415 )));
416 };
417
418 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
419 face_id: end_cap_id,
420 tag: Some(tag_end.clone()),
421 geo_meta: GeoMeta {
422 id: end_cap_id,
423 metadata: args.source_range.into(),
424 },
425 }));
426 }
427
428 Ok(Solid {
429 id: sketch.id,
433 artifact_id: solid_id,
434 value: new_value,
435 meta: sketch.meta.clone(),
436 units: sketch.units,
437 height: length.to_length_units(sketch.units),
438 sectional,
439 sketch,
440 start_cap_id,
441 end_cap_id,
442 edge_cuts: vec![],
443 })
444}
445
446#[derive(Default)]
447struct Faces {
448 sides: HashMap<Uuid, Option<Uuid>>,
450 end_cap_id: Option<Uuid>,
452 start_cap_id: Option<Uuid>,
454}
455
456async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
457 let mut faces = Faces {
458 sides: HashMap::with_capacity(face_infos.len()),
459 ..Default::default()
460 };
461 if args.ctx.no_engine_commands().await {
462 faces.start_cap_id = Some(exec_state.next_uuid());
464 faces.end_cap_id = Some(exec_state.next_uuid());
465 }
466 for face_info in face_infos {
467 match face_info.cap {
468 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
469 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
470 ExtrusionFaceCapType::Both => {
471 faces.end_cap_id = face_info.face_id;
472 faces.start_cap_id = face_info.face_id;
473 }
474 ExtrusionFaceCapType::None => {
475 if let Some(curve_id) = face_info.curve_id {
476 faces.sides.insert(curve_id, face_info.face_id);
477 }
478 }
479 }
480 }
481 faces
482}