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;
20#[cfg(feature = "artifact-graph")]
21use crate::execution::ArtifactId;
22use crate::{
23 errors::{KclError, KclErrorDetails},
24 execution::{types::RuntimeType, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSurface, Solid},
25 parsing::ast::types::TagNode,
26 std::Args,
27};
28
29pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
31 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
32 let length: TyF64 = args.get_kw_arg_typed("length", &RuntimeType::length(), exec_state)?;
33 let symmetric = args.get_kw_arg_opt("symmetric")?;
34 let bidirectional_length: Option<TyF64> =
35 args.get_kw_arg_opt_typed("bidirectionalLength", &RuntimeType::length(), exec_state)?;
36 let tag_start = args.get_kw_arg_opt("tagStart")?;
37 let tag_end = args.get_kw_arg_opt("tagEnd")?;
38
39 let result = inner_extrude(
40 sketches,
41 length,
42 symmetric,
43 bidirectional_length,
44 tag_start,
45 tag_end,
46 exec_state,
47 args,
48 )
49 .await?;
50
51 Ok(result.into())
52}
53
54#[stdlib {
148 name = "extrude",
149 feature_tree_operation = true,
150 keywords = 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
156 extrusion will happen on only one side of the sketch." },
157 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."},
158 tag_start = { docs = "A named tag for the face at the start of the extrusion, i.e. the original sketch" },
159 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" },
160 },
161 tags = ["sketch"]
162}]
163#[allow(clippy::too_many_arguments)]
164async fn inner_extrude(
165 sketches: Vec<Sketch>,
166 length: TyF64,
167 symmetric: Option<bool>,
168 bidirectional_length: Option<TyF64>,
169 tag_start: Option<TagNode>,
170 tag_end: Option<TagNode>,
171 exec_state: &mut ExecState,
172 args: Args,
173) -> Result<Vec<Solid>, KclError> {
174 let mut solids = Vec::new();
176
177 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
178 return Err(KclError::Semantic(KclErrorDetails {
179 source_ranges: vec![args.source_range],
180 message: "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
181 .to_owned(),
182 }));
183 }
184
185 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
186
187 let opposite = match (symmetric, bidirection) {
188 (Some(true), _) => Opposite::Symmetric,
189 (None, None) => Opposite::None,
190 (Some(false), None) => Opposite::None,
191 (None, Some(length)) => Opposite::Other(length),
192 (Some(false), Some(length)) => Opposite::Other(length),
193 };
194
195 for sketch in &sketches {
196 let id = exec_state.next_uuid();
197 args.batch_modeling_cmds(&sketch.build_sketch_mode_cmds(
198 exec_state,
199 ModelingCmdReq {
200 cmd_id: id.into(),
201 cmd: ModelingCmd::from(mcmd::Extrude {
202 target: sketch.id.into(),
203 distance: LengthUnit(length.to_mm()),
204 faces: Default::default(),
205 opposite: opposite.clone(),
206 }),
207 },
208 ))
209 .await?;
210
211 solids.push(
212 do_post_extrude(
213 sketch,
214 #[cfg(feature = "artifact-graph")]
215 id.into(),
216 length.clone(),
217 false,
218 &NamedCapTags {
219 start: tag_start.as_ref(),
220 end: tag_end.as_ref(),
221 },
222 exec_state,
223 &args,
224 )
225 .await?,
226 );
227 }
228
229 Ok(solids)
230}
231
232#[derive(Debug, Default)]
233pub(crate) struct NamedCapTags<'a> {
234 pub start: Option<&'a TagNode>,
235 pub end: Option<&'a TagNode>,
236}
237
238pub(crate) async fn do_post_extrude<'a>(
239 sketch: &Sketch,
240 #[cfg(feature = "artifact-graph")] 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) -> Result<Solid, KclError> {
247 args.batch_modeling_cmd(
250 exec_state.next_uuid(),
251 ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
252 )
253 .await?;
254
255 let any_edge_id = if let Some(edge_id) = sketch.mirror {
256 edge_id
257 } else {
258 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
261 return Err(KclError::Type(KclErrorDetails {
262 message: "Expected a non-empty sketch".to_string(),
263 source_ranges: vec![args.source_range],
264 }));
265 };
266 any_edge_id
267 };
268
269 let mut sketch = sketch.clone();
270
271 if let SketchSurface::Face(ref face) = sketch.on {
273 sketch.id = face.solid.sketch.id;
274 }
275
276 let solid3d_info = args
277 .send_modeling_cmd(
278 exec_state.next_uuid(),
279 ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
280 edge_id: any_edge_id,
281 object_id: sketch.id,
282 }),
283 )
284 .await?;
285
286 let face_infos = if let OkWebSocketResponseData::Modeling {
287 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
288 } = solid3d_info
289 {
290 data.faces
291 } else {
292 vec![]
293 };
294
295 #[cfg(feature = "artifact-graph")]
301 let count_of_first_set_of_faces_if_sectional = if sectional {
302 sketch
303 .paths
304 .iter()
305 .filter(|p| {
306 let is_circle = matches!(p, Path::Circle { .. });
307 let has_length = p.get_base().from != p.get_base().to;
308 is_circle || has_length
309 })
310 .count()
311 } else {
312 usize::MAX
313 };
314
315 #[cfg(feature = "artifact-graph")]
317 for (curve_id, face_id) in face_infos
318 .iter()
319 .filter(|face_info| face_info.cap == ExtrusionFaceCapType::None)
320 .filter_map(|face_info| {
321 if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
322 Some((curve_id, face_id))
323 } else {
324 None
325 }
326 })
327 .take(count_of_first_set_of_faces_if_sectional)
328 {
329 let args_cloned = args.clone();
337 let opposite_edge_uuid = exec_state.next_uuid();
338 let next_adjacent_edge_uuid = exec_state.next_uuid();
339 let get_all_edge_faces_opposite_uuid = exec_state.next_uuid();
340 let get_all_edge_faces_next_uuid = exec_state.next_uuid();
341
342 args.batch_modeling_cmd(
345 exec_state.next_uuid(),
346 ModelingCmd::from(mcmd::Solid3dGetAllEdgeFaces {
347 edge_id: curve_id,
348 object_id: sketch.id,
349 }),
350 )
351 .await?;
352
353 get_bg_edge_info_opposite(
354 args_cloned.clone(),
355 curve_id,
356 sketch.id,
357 face_id,
358 opposite_edge_uuid,
359 get_all_edge_faces_opposite_uuid,
360 true,
361 )
362 .await?;
363
364 get_bg_edge_info_next(
365 args_cloned,
366 curve_id,
367 sketch.id,
368 face_id,
369 next_adjacent_edge_uuid,
370 get_all_edge_faces_next_uuid,
371 true,
372 )
373 .await?;
374 }
375
376 let Faces {
377 sides: face_id_map,
378 start_cap_id,
379 end_cap_id,
380 } = analyze_faces(exec_state, args, face_infos).await;
381
382 let no_engine_commands = args.ctx.no_engine_commands().await;
384 let mut new_value: Vec<ExtrudeSurface> = sketch
385 .paths
386 .iter()
387 .flat_map(|path| {
388 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
389 match path {
390 Path::Arc { .. }
391 | Path::TangentialArc { .. }
392 | Path::TangentialArcTo { .. }
393 | Path::Circle { .. }
394 | Path::CircleThreePoint { .. } => {
395 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
396 face_id: *actual_face_id,
397 tag: path.get_base().tag.clone(),
398 geo_meta: GeoMeta {
399 id: path.get_base().geo_meta.id,
400 metadata: path.get_base().geo_meta.metadata,
401 },
402 });
403 Some(extrude_surface)
404 }
405 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
406 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
407 face_id: *actual_face_id,
408 tag: path.get_base().tag.clone(),
409 geo_meta: GeoMeta {
410 id: path.get_base().geo_meta.id,
411 metadata: path.get_base().geo_meta.metadata,
412 },
413 });
414 Some(extrude_surface)
415 }
416 Path::ArcThreePoint { .. } => {
417 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
418 face_id: *actual_face_id,
419 tag: path.get_base().tag.clone(),
420 geo_meta: GeoMeta {
421 id: path.get_base().geo_meta.id,
422 metadata: path.get_base().geo_meta.metadata,
423 },
424 });
425 Some(extrude_surface)
426 }
427 }
428 } else if no_engine_commands {
429 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
432 face_id: exec_state.next_uuid(),
434 tag: path.get_base().tag.clone(),
435 geo_meta: GeoMeta {
436 id: path.get_base().geo_meta.id,
437 metadata: path.get_base().geo_meta.metadata,
438 },
439 });
440 Some(extrude_surface)
441 } else {
442 None
443 }
444 })
445 .collect();
446
447 if let Some(tag_start) = named_cap_tags.start {
449 let Some(start_cap_id) = start_cap_id else {
450 return Err(KclError::Type(KclErrorDetails {
451 message: format!(
452 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
453 tag_start.name, sketch.id
454 ),
455 source_ranges: vec![args.source_range],
456 }));
457 };
458
459 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
460 face_id: start_cap_id,
461 tag: Some(tag_start.clone()),
462 geo_meta: GeoMeta {
463 id: start_cap_id,
464 metadata: args.source_range.into(),
465 },
466 }));
467 }
468 if let Some(tag_end) = named_cap_tags.end {
469 let Some(end_cap_id) = end_cap_id else {
470 return Err(KclError::Type(KclErrorDetails {
471 message: format!(
472 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
473 tag_end.name, sketch.id
474 ),
475 source_ranges: vec![args.source_range],
476 }));
477 };
478
479 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
480 face_id: end_cap_id,
481 tag: Some(tag_end.clone()),
482 geo_meta: GeoMeta {
483 id: end_cap_id,
484 metadata: args.source_range.into(),
485 },
486 }));
487 }
488
489 Ok(Solid {
490 id: sketch.id,
494 #[cfg(feature = "artifact-graph")]
495 artifact_id: solid_id,
496 value: new_value,
497 meta: sketch.meta.clone(),
498 units: sketch.units,
499 height: length.to_length_units(sketch.units),
500 sectional,
501 sketch,
502 start_cap_id,
503 end_cap_id,
504 edge_cuts: vec![],
505 })
506}
507
508#[derive(Default)]
509struct Faces {
510 sides: HashMap<Uuid, Option<Uuid>>,
512 end_cap_id: Option<Uuid>,
514 start_cap_id: Option<Uuid>,
516}
517
518async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
519 let mut faces = Faces {
520 sides: HashMap::with_capacity(face_infos.len()),
521 ..Default::default()
522 };
523 if args.ctx.no_engine_commands().await {
524 faces.start_cap_id = Some(exec_state.next_uuid());
526 faces.end_cap_id = Some(exec_state.next_uuid());
527 }
528 for face_info in face_infos {
529 match face_info.cap {
530 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
531 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
532 ExtrusionFaceCapType::Both => {
533 faces.end_cap_id = face_info.face_id;
534 faces.start_cap_id = face_info.face_id;
535 }
536 ExtrusionFaceCapType::None => {
537 if let Some(curve_id) = face_info.curve_id {
538 faces.sides.insert(curve_id, face_info.face_id);
539 }
540 }
541 }
542 }
543 faces
544}
545
546#[cfg(feature = "artifact-graph")]
547async fn send_fn(args: &Args, id: uuid::Uuid, cmd: ModelingCmd, single_threaded: bool) -> Result<(), KclError> {
548 if single_threaded {
549 args.batch_modeling_cmd(id, cmd).await
551 } else {
552 args.send_modeling_cmd(id, cmd).await.map(|_| ())
555 }
556}
557
558#[cfg(feature = "artifact-graph")]
559#[allow(clippy::too_many_arguments)]
560async fn get_bg_edge_info_next(
561 args: Args,
562 curve_id: uuid::Uuid,
563 sketch_id: uuid::Uuid,
564 face_id: uuid::Uuid,
565 edge_uuid: uuid::Uuid,
566 get_all_edge_faces_uuid: uuid::Uuid,
567 single_threaded: bool,
568) -> Result<(), KclError> {
569 let next_adjacent_edge_id = args
570 .send_modeling_cmd(
571 edge_uuid,
572 ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
573 edge_id: curve_id,
574 object_id: sketch_id,
575 face_id,
576 }),
577 )
578 .await?;
579
580 if let OkWebSocketResponseData::Modeling {
582 modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(next_adjacent_edge),
583 } = next_adjacent_edge_id
584 {
585 if let Some(edge_id) = next_adjacent_edge.edge {
586 send_fn(
587 &args,
588 get_all_edge_faces_uuid,
589 ModelingCmd::from(mcmd::Solid3dGetAllEdgeFaces {
590 edge_id,
591 object_id: sketch_id,
592 }),
593 single_threaded,
594 )
595 .await?;
596 }
597 }
598
599 Ok(())
600}
601
602#[cfg(feature = "artifact-graph")]
603#[allow(clippy::too_many_arguments)]
604async fn get_bg_edge_info_opposite(
605 args: Args,
606 curve_id: uuid::Uuid,
607 sketch_id: uuid::Uuid,
608 face_id: uuid::Uuid,
609 edge_uuid: uuid::Uuid,
610 get_all_edge_faces_uuid: uuid::Uuid,
611 single_threaded: bool,
612) -> Result<(), KclError> {
613 let opposite_edge_id = args
614 .send_modeling_cmd(
615 edge_uuid,
616 ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
617 edge_id: curve_id,
618 object_id: sketch_id,
619 face_id,
620 }),
621 )
622 .await?;
623
624 if let OkWebSocketResponseData::Modeling {
626 modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
627 } = opposite_edge_id
628 {
629 send_fn(
630 &args,
631 get_all_edge_faces_uuid,
632 ModelingCmd::from(mcmd::Solid3dGetAllEdgeFaces {
633 edge_id: opposite_edge.edge,
634 object_id: sketch_id,
635 }),
636 single_threaded,
637 )
638 .await?;
639 }
640
641 Ok(())
642}