1use std::collections::HashMap;
4
5use anyhow::Result;
6use indexmap::IndexMap;
7use kcmc::ModelingCmd;
8use kcmc::each_cmd as mcmd;
9use kcmc::length_unit::LengthUnit;
10use kcmc::ok_response::OkModelingCmdResponse;
11use kcmc::output::ExtrusionFaceInfo;
12use kcmc::shared::ExtrudeReference;
13use kcmc::shared::ExtrusionFaceCapType;
14use kcmc::shared::Opposite;
15use kcmc::shared::Point3d as KPoint3d; use kcmc::websocket::ModelingCmdReq;
17use kcmc::websocket::OkWebSocketResponseData;
18use kittycad_modeling_cmds::shared::Angle;
19use kittycad_modeling_cmds::shared::BodyType;
20use kittycad_modeling_cmds::shared::DirectionType;
21use kittycad_modeling_cmds::shared::ExtrudeMethod;
22use kittycad_modeling_cmds::shared::Point2d;
23use kittycad_modeling_cmds::{self as kcmc};
24use uuid::Uuid;
25
26use super::DEFAULT_TOLERANCE_MM;
27use super::args::TyF64;
28use super::utils::point_to_mm;
29use crate::errors::KclError;
30use crate::errors::KclErrorDetails;
31use crate::execution::ArtifactId;
32use crate::execution::CreatorFace;
33use crate::execution::ExecState;
34use crate::execution::ExecutorContext;
35use crate::execution::Extrudable;
36use crate::execution::ExtrudeSurface;
37use crate::execution::GeoMeta;
38use crate::execution::KclValue;
39use crate::execution::ModelingCmdMeta;
40use crate::execution::Path;
41use crate::execution::ProfileClosed;
42use crate::execution::Segment;
43use crate::execution::SegmentKind;
44use crate::execution::Sketch;
45use crate::execution::SketchSurface;
46use crate::execution::Solid;
47use crate::execution::SolidCreator;
48use crate::execution::annotations;
49use crate::execution::types::ArrayLen;
50use crate::execution::types::PrimitiveType;
51use crate::execution::types::RuntimeType;
52use crate::parsing::ast::types::TagDeclarator;
53use crate::parsing::ast::types::TagNode;
54use crate::std::Args;
55use crate::std::args::FromKclValue;
56use crate::std::axis_or_reference::Point3dAxis3dOrGeometryReference;
57use crate::std::axis_or_reference::Point3dOrEdgeReference;
58use crate::std::solver::create_segments_in_engine;
59
60pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
62 let sketch_values: Vec<KclValue> = args.get_unlabeled_kw_arg(
63 "sketches",
64 &RuntimeType::Array(
65 Box::new(RuntimeType::Union(vec![
66 RuntimeType::sketch(),
67 RuntimeType::face(),
68 RuntimeType::tagged_face(),
69 RuntimeType::segment(),
70 ])),
71 ArrayLen::Minimum(1),
72 ),
73 exec_state,
74 )?;
75
76 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
77 let to = args.get_kw_arg_opt(
78 "to",
79 &RuntimeType::Union(vec![
80 RuntimeType::point3d(),
81 RuntimeType::Primitive(PrimitiveType::Axis3d),
82 RuntimeType::Primitive(PrimitiveType::Edge),
83 RuntimeType::plane(),
84 RuntimeType::Primitive(PrimitiveType::Face),
85 RuntimeType::sketch(),
86 RuntimeType::Primitive(PrimitiveType::Solid),
87 RuntimeType::tagged_edge(),
88 RuntimeType::tagged_face(),
89 ]),
90 exec_state,
91 )?;
92 let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
93 let bidirectional_length: Option<TyF64> =
94 args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
95 let direction = args.get_kw_arg_opt(
96 "direction",
97 &RuntimeType::Union(vec![
98 RuntimeType::point3d(),
99 RuntimeType::Primitive(PrimitiveType::Edge),
100 RuntimeType::tagged_edge(),
101 RuntimeType::segment(),
102 ]),
103 exec_state,
104 )?;
105 let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
106 let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
107 let draft_angle: Option<TyF64> = args.get_kw_arg_opt("draftAngle", &RuntimeType::degrees(), exec_state)?;
108 let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
109 let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
110 let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
111 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
112 let method: Option<String> = args.get_kw_arg_opt("method", &RuntimeType::string(), exec_state)?;
113 let hide_seams: Option<bool> = args.get_kw_arg_opt("hideSeams", &RuntimeType::bool(), exec_state)?;
114 let body_type: Option<BodyType> = args.get_kw_arg_opt("bodyType", &RuntimeType::string(), exec_state)?;
115 let sketches = coerce_extrude_targets(
116 sketch_values,
117 body_type.unwrap_or_default(),
118 tag_start.as_ref(),
119 tag_end.as_ref(),
120 exec_state,
121 &args.ctx,
122 args.source_range,
123 )
124 .await?;
125
126 let result = inner_extrude(
127 sketches,
128 length,
129 to,
130 symmetric,
131 direction,
132 bidirectional_length,
133 tag_start,
134 tag_end,
135 draft_angle,
136 twist_angle,
137 twist_angle_step,
138 twist_center,
139 tolerance,
140 method,
141 hide_seams,
142 body_type,
143 exec_state,
144 args,
145 )
146 .await?;
147
148 Ok(result.into())
149}
150
151async fn coerce_extrude_targets(
152 sketch_values: Vec<KclValue>,
153 body_type: BodyType,
154 tag_start: Option<&TagNode>,
155 tag_end: Option<&TagNode>,
156 exec_state: &mut ExecState,
157 ctx: &ExecutorContext,
158 source_range: crate::SourceRange,
159) -> Result<Vec<Extrudable>, KclError> {
160 let mut extrudables = Vec::new();
161 let mut segments = Vec::new();
162
163 for value in sketch_values {
164 if let Some(segment) = value.clone().into_segment() {
165 segments.push(segment);
166 continue;
167 }
168
169 let Some(extrudable) = Extrudable::from_kcl_val(&value) else {
170 return Err(KclError::new_type(KclErrorDetails::new(
171 "Expected sketches, faces, tagged faces, or solved sketch segments for extrusion.".to_owned(),
172 vec![source_range],
173 )));
174 };
175 extrudables.push(extrudable);
176 }
177
178 if !segments.is_empty() && !extrudables.is_empty() {
179 return Err(KclError::new_semantic(KclErrorDetails::new(
180 "Cannot extrude sketch segments together with sketches or faces in the same call. Use separate `extrude()` calls.".to_owned(),
181 vec![source_range],
182 )));
183 }
184
185 if !segments.is_empty() {
186 if !matches!(body_type, BodyType::Surface) {
187 return Err(KclError::new_semantic(KclErrorDetails::new(
188 "Extruding sketch segments is only supported for surface extrudes. Set `bodyType = SURFACE`."
189 .to_owned(),
190 vec![source_range],
191 )));
192 }
193
194 if tag_start.is_some() || tag_end.is_some() {
195 return Err(KclError::new_semantic(KclErrorDetails::new(
196 "`tagStart` and `tagEnd` are not supported when extruding sketch segments. Segment surface extrudes do not create start or end caps."
197 .to_owned(),
198 vec![source_range],
199 )));
200 }
201
202 let synthetic_sketch = build_segment_surface_sketch(segments, exec_state, ctx, source_range).await?;
203 return Ok(vec![Extrudable::from(synthetic_sketch)]);
204 }
205
206 Ok(extrudables)
207}
208
209pub(crate) async fn build_segment_surface_sketch(
210 mut segments: Vec<Segment>,
211 exec_state: &mut ExecState,
212 ctx: &ExecutorContext,
213 source_range: crate::SourceRange,
214) -> Result<Sketch, KclError> {
215 let Some(first_segment) = segments.first() else {
216 return Err(KclError::new_semantic(KclErrorDetails::new(
217 "Expected at least one sketch segment.".to_owned(),
218 vec![source_range],
219 )));
220 };
221
222 let sketch_id = first_segment.sketch_id;
223 let sketch_surface = first_segment.surface.clone();
224 for segment in &segments {
225 if segment.sketch_id != sketch_id {
226 return Err(KclError::new_semantic(KclErrorDetails::new(
227 "All sketch segments passed to this operation must come from the same sketch.".to_owned(),
228 vec![source_range],
229 )));
230 }
231
232 if segment.surface != sketch_surface {
233 return Err(KclError::new_semantic(KclErrorDetails::new(
234 "All sketch segments passed to this operation must lie on the same sketch surface.".to_owned(),
235 vec![source_range],
236 )));
237 }
238
239 if matches!(segment.kind, SegmentKind::Point { .. }) {
240 return Err(KclError::new_semantic(KclErrorDetails::new(
241 "Point segments cannot be used here. Select line, arc, or circle segments instead.".to_owned(),
242 vec![source_range],
243 )));
244 }
245
246 if segment.is_construction() {
247 return Err(KclError::new_semantic(KclErrorDetails::new(
248 "Construction segments cannot be used here. Select non-construction sketch segments instead."
249 .to_owned(),
250 vec![source_range],
251 )));
252 }
253 }
254
255 let synthetic_sketch_id = exec_state.next_uuid();
256 let segment_tags = IndexMap::from_iter(segments.iter().filter_map(|segment| {
257 segment
258 .tag
259 .as_ref()
260 .map(|tag| (segment.object_id, TagDeclarator::new(&tag.value)))
261 }));
262
263 for segment in &mut segments {
264 segment.id = exec_state.next_uuid();
265 segment.sketch_id = synthetic_sketch_id;
266 segment.sketch = None;
267 }
268
269 create_segments_in_engine(
270 &sketch_surface,
271 synthetic_sketch_id,
272 &mut segments,
273 &segment_tags,
274 ctx,
275 exec_state,
276 source_range,
277 )
278 .await?
279 .ok_or_else(|| {
280 KclError::new_semantic(KclErrorDetails::new(
281 "Expected at least one usable sketch segment.".to_owned(),
282 vec![source_range],
283 ))
284 })
285}
286
287#[allow(clippy::too_many_arguments)]
288async fn inner_extrude(
289 extrudables: Vec<Extrudable>,
290 length: Option<TyF64>,
291 to: Option<Point3dAxis3dOrGeometryReference>,
292 symmetric: Option<bool>,
293 direction: Option<Point3dOrEdgeReference>,
294 bidirectional_length: Option<TyF64>,
295 tag_start: Option<TagNode>,
296 tag_end: Option<TagNode>,
297 draft_angle: Option<TyF64>,
298 twist_angle: Option<TyF64>,
299 twist_angle_step: Option<TyF64>,
300 twist_center: Option<[TyF64; 2]>,
301 tolerance: Option<TyF64>,
302 method: Option<String>,
303 hide_seams: Option<bool>,
304 body_type: Option<BodyType>,
305 exec_state: &mut ExecState,
306 args: Args,
307) -> Result<Vec<Solid>, KclError> {
308 let body_type = body_type.unwrap_or_default();
309
310 if matches!(body_type, BodyType::Solid) && extrudables.iter().any(|sk| matches!(sk.is_closed(), ProfileClosed::No))
311 {
312 return Err(KclError::new_semantic(KclErrorDetails::new(
313 "Cannot solid extrude an open profile. Either close the profile, or use a surface extrude.".to_owned(),
314 vec![args.source_range],
315 )));
316 }
317
318 if draft_angle.is_some() && twist_angle.is_some() {
319 return Err(KclError::new_semantic(KclErrorDetails::new(
320 "Zoo currently does not support adding both draft angle and twist angle to an extrude simultaneously"
321 .to_owned(),
322 vec![args.source_range],
323 )));
324 }
325
326 if direction.is_some() && twist_angle.is_some() {
327 return Err(KclError::new_semantic(KclErrorDetails::new(
328 "Zoo currently does not support adding both direction and twist angle to an extrude simultaneously"
329 .to_owned(),
330 vec![args.source_range],
331 )));
332 }
333
334 let mut solids = Vec::new();
336 let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
337
338 let extrude_method = match method.as_deref() {
339 Some("new" | "NEW") => ExtrudeMethod::New,
340 Some("merge" | "MERGE") => ExtrudeMethod::Merge,
341 None => ExtrudeMethod::default(),
342 Some(other) => {
343 return Err(KclError::new_semantic(KclErrorDetails::new(
344 format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
345 vec![args.source_range],
346 )));
347 }
348 };
349
350 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
351 return Err(KclError::new_semantic(KclErrorDetails::new(
352 "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
353 .to_owned(),
354 vec![args.source_range],
355 )));
356 }
357
358 if (length.is_some() || twist_angle.is_some()) && to.is_some() {
359 return Err(KclError::new_semantic(KclErrorDetails::new(
360 "You cannot give `length` or `twist` params with the `to` param, you have to choose one or the other"
361 .to_owned(),
362 vec![args.source_range],
363 )));
364 }
365
366 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
367
368 let opposite = match (symmetric, bidirection) {
369 (Some(true), _) => Opposite::Symmetric,
370 (None, None) => Opposite::None,
371 (Some(false), None) => Opposite::None,
372 (None, Some(length)) => Opposite::Other(length),
373 (Some(false), Some(length)) => Opposite::Other(length),
374 };
375
376 for extrudable in &extrudables {
377 let extrude_cmd_id = exec_state.next_uuid();
378 let sketch_or_face_id = extrudable.id_to_extrude(exec_state, &args, false).await?;
379 let cmd = match (
380 &twist_angle,
381 &twist_angle_step,
382 &twist_center,
383 length.clone(),
384 &to,
385 &direction,
386 ) {
387 (Some(angle), angle_step, center, Some(length), None, None) => {
388 let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
389 let total_rotation_angle = Angle::from_degrees(angle.to_degrees(exec_state, args.source_range));
390 let angle_step_size = Angle::from_degrees(
391 angle_step
392 .clone()
393 .map(|a| a.to_degrees(exec_state, args.source_range))
394 .unwrap_or(15.0),
395 );
396 ModelingCmd::from(
397 mcmd::TwistExtrude::builder()
398 .target(sketch_or_face_id.into())
399 .distance(LengthUnit(length.to_mm()))
400 .center_2d(center)
401 .total_rotation_angle(total_rotation_angle)
402 .angle_step_size(angle_step_size)
403 .tolerance(tolerance)
404 .body_type(body_type)
405 .build(),
406 )
407 }
408 (None, None, None, Some(length), None, None) => ModelingCmd::from(
409 mcmd::Extrude::builder()
410 .target(sketch_or_face_id.into())
411 .distance(LengthUnit(length.to_mm()))
412 .opposite(opposite.clone())
413 .maybe_draft_angle(
414 draft_angle
415 .clone()
416 .map(|a| Angle::from_degrees(a.to_degrees(exec_state, args.source_range))),
417 )
418 .extrude_method(extrude_method)
419 .body_type(body_type)
420 .maybe_merge_coplanar_faces(hide_seams)
421 .build(),
422 ),
423 (None, None, None, Some(length), None, Some(dir)) => {
424 let direction3d = match dir {
425 Point3dOrEdgeReference::Point(p) => DirectionType::Axis {
426 direction: KPoint3d {
427 x: p[0].n,
428 y: p[1].n,
429 z: p[2].n,
430 },
431 },
432 Point3dOrEdgeReference::Edge(edge) => match edge {
433 crate::std::fillet::EdgeReference::Uuid(uuid) => DirectionType::Edge { id: *uuid },
434 crate::std::fillet::EdgeReference::Tag(tag) => DirectionType::Edge {
435 id: match tag.get_cur_info() {
436 Some(info) => info.id,
437 None => {
438 return Err(KclError::new_semantic(KclErrorDetails::new(
439 "Failed to get current info for tag".to_string(),
440 vec![args.source_range],
441 )));
442 }
443 },
444 },
445 },
446 };
447 ModelingCmd::from(
448 mcmd::Extrude::builder()
449 .target(sketch_or_face_id.into())
450 .distance(LengthUnit(length.to_mm()))
451 .opposite(opposite.clone())
452 .maybe_draft_angle(
453 draft_angle
454 .clone()
455 .map(|a| Angle::from_degrees(a.to_degrees(exec_state, args.source_range))),
456 )
457 .extrude_method(extrude_method)
458 .body_type(body_type)
459 .maybe_merge_coplanar_faces(hide_seams)
460 .direction(direction3d)
461 .build(),
462 )
463 }
464 (None, None, None, None, Some(to), None) => match to {
465 Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(
466 mcmd::ExtrudeToReference::builder()
467 .target(sketch_or_face_id.into())
468 .reference(ExtrudeReference::Point {
469 point: KPoint3d {
470 x: LengthUnit(point[0].to_mm()),
471 y: LengthUnit(point[1].to_mm()),
472 z: LengthUnit(point[2].to_mm()),
473 },
474 })
475 .extrude_method(extrude_method)
476 .body_type(body_type)
477 .build(),
478 ),
479 Point3dAxis3dOrGeometryReference::Axis { direction, origin } => ModelingCmd::from(
480 mcmd::ExtrudeToReference::builder()
481 .target(sketch_or_face_id.into())
482 .reference(ExtrudeReference::Axis {
483 axis: KPoint3d {
484 x: direction[0].to_mm(),
485 y: direction[1].to_mm(),
486 z: direction[2].to_mm(),
487 },
488 point: KPoint3d {
489 x: LengthUnit(origin[0].to_mm()),
490 y: LengthUnit(origin[1].to_mm()),
491 z: LengthUnit(origin[2].to_mm()),
492 },
493 })
494 .extrude_method(extrude_method)
495 .body_type(body_type)
496 .build(),
497 ),
498 Point3dAxis3dOrGeometryReference::Plane(plane) => {
499 let plane_id = if plane.is_uninitialized() {
500 if plane.info.origin.units.is_none() {
501 return Err(KclError::new_semantic(KclErrorDetails::new(
502 "Origin of plane has unknown units".to_string(),
503 vec![args.source_range],
504 )));
505 }
506 let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
507 plane.clone().info.into_plane_data(),
508 exec_state,
509 &args,
510 )
511 .await?;
512 sketch_plane.id
513 } else {
514 plane.id
515 };
516 ModelingCmd::from(
517 mcmd::ExtrudeToReference::builder()
518 .target(sketch_or_face_id.into())
519 .reference(ExtrudeReference::EntityReference {
520 entity_id: Some(plane_id),
521 entity_reference: None,
522 })
523 .extrude_method(extrude_method)
524 .body_type(body_type)
525 .build(),
526 )
527 }
528 Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
529 let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
530 ModelingCmd::from(
531 mcmd::ExtrudeToReference::builder()
532 .target(sketch_or_face_id.into())
533 .reference(ExtrudeReference::EntityReference {
534 entity_id: Some(edge_id),
535 entity_reference: None,
536 })
537 .extrude_method(extrude_method)
538 .body_type(body_type)
539 .build(),
540 )
541 }
542 Point3dAxis3dOrGeometryReference::Face(face_tag) => {
543 let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
544 ModelingCmd::from(
545 mcmd::ExtrudeToReference::builder()
546 .target(sketch_or_face_id.into())
547 .reference(ExtrudeReference::EntityReference {
548 entity_id: Some(face_id),
549 entity_reference: None,
550 })
551 .extrude_method(extrude_method)
552 .body_type(body_type)
553 .build(),
554 )
555 }
556 Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(
557 mcmd::ExtrudeToReference::builder()
558 .target(sketch_or_face_id.into())
559 .reference(ExtrudeReference::EntityReference {
560 entity_id: Some(sketch_ref.id),
561 entity_reference: None,
562 })
563 .extrude_method(extrude_method)
564 .body_type(body_type)
565 .build(),
566 ),
567 Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(
568 mcmd::ExtrudeToReference::builder()
569 .target(sketch_or_face_id.into())
570 .reference(ExtrudeReference::EntityReference {
571 entity_id: Some(solid.id),
572 entity_reference: None,
573 })
574 .extrude_method(extrude_method)
575 .body_type(body_type)
576 .build(),
577 ),
578 Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
579 let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
580 let tagged_edge_or_face_id = tagged_edge_or_face.id;
581 ModelingCmd::from(
582 mcmd::ExtrudeToReference::builder()
583 .target(sketch_or_face_id.into())
584 .reference(ExtrudeReference::EntityReference {
585 entity_id: Some(tagged_edge_or_face_id),
586 entity_reference: None,
587 })
588 .extrude_method(extrude_method)
589 .body_type(body_type)
590 .build(),
591 )
592 }
593 },
594 (Some(_), _, _, None, None, None) => {
595 return Err(KclError::new_semantic(KclErrorDetails::new(
596 "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
597 vec![args.source_range],
598 )));
599 }
600 (_, _, _, None, None, None) => {
601 return Err(KclError::new_semantic(KclErrorDetails::new(
602 "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
603 vec![args.source_range],
604 )));
605 }
606 (_, _, _, Some(_), Some(_), None) => {
607 return Err(KclError::new_semantic(KclErrorDetails::new(
608 "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
609 vec![args.source_range],
610 )));
611 }
612 (_, _, _, _, _, _) => {
613 return Err(KclError::new_semantic(KclErrorDetails::new(
614 "Invalid combination of parameters for extrusion.".to_owned(),
615 vec![args.source_range],
616 )));
617 }
618 };
619
620 let being_extruded = match extrudable {
621 Extrudable::Sketch(..) => BeingExtruded::Sketch,
622 Extrudable::Face(face_tag) => {
623 let face_id = sketch_or_face_id;
624 let solid_id = match face_tag.geometry() {
625 Some(crate::execution::Geometry::Solid(solid)) => solid.id,
626 Some(crate::execution::Geometry::Sketch(sketch)) => match sketch.on {
627 SketchSurface::Face(face) => face.parent_solid.solid_id,
628 SketchSurface::Plane(_) => sketch.id,
629 },
630 None => face_id,
631 };
632 BeingExtruded::Face { face_id, solid_id }
633 }
634 };
635 if let Some(post_extr_sketch) = extrudable.as_sketch() {
636 let cmds = post_extr_sketch.build_sketch_mode_cmds(
637 exec_state,
638 ModelingCmdReq {
639 cmd_id: extrude_cmd_id.into(),
640 cmd,
641 },
642 );
643 exec_state
644 .batch_modeling_cmds(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), &cmds)
645 .await?;
646 solids.push(
647 do_post_extrude(
648 &post_extr_sketch,
649 extrude_cmd_id.into(),
650 false,
651 &NamedCapTags {
652 start: tag_start.as_ref(),
653 end: tag_end.as_ref(),
654 },
655 extrude_method,
656 exec_state,
657 &args,
658 None,
659 None,
660 body_type,
661 being_extruded,
662 )
663 .await?,
664 );
665 } else {
666 return Err(KclError::new_type(KclErrorDetails::new(
667 "Expected a sketch for extrusion".to_owned(),
668 vec![args.source_range],
669 )));
670 }
671 }
672
673 Ok(solids)
674}
675
676#[derive(Debug, Default)]
677pub(crate) struct NamedCapTags<'a> {
678 pub start: Option<&'a TagNode>,
679 pub end: Option<&'a TagNode>,
680}
681
682#[derive(Debug, Clone, Copy)]
683pub enum BeingExtruded {
684 Sketch,
685 Face { face_id: Uuid, solid_id: Uuid },
686}
687
688#[allow(clippy::too_many_arguments)]
689pub(crate) async fn do_post_extrude<'a>(
690 sketch: &Sketch,
691 extrude_cmd_id: ArtifactId,
692 sectional: bool,
693 named_cap_tags: &'a NamedCapTags<'a>,
694 extrude_method: ExtrudeMethod,
695 exec_state: &mut ExecState,
696 args: &Args,
697 edge_id: Option<Uuid>,
698 clone_id_map: Option<&HashMap<Uuid, Uuid>>, body_type: BodyType,
700 being_extruded: BeingExtruded,
701) -> Result<Solid, KclError> {
702 exec_state
706 .batch_modeling_cmd(
707 ModelingCmdMeta::from_args(exec_state, args),
708 ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(sketch.id).build()),
709 )
710 .await?;
711
712 let any_edge_id = if let Some(edge_id) = sketch.mirror {
713 edge_id
714 } else if let Some(id) = edge_id {
715 id
716 } else {
717 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
720 return Err(KclError::new_type(KclErrorDetails::new(
721 "Expected a non-empty sketch".to_owned(),
722 vec![args.source_range],
723 )));
724 };
725 any_edge_id
726 };
727
728 let mut extrusion_info_edge_id = any_edge_id;
730 if sketch.clone.is_some() && clone_id_map.is_some() {
731 extrusion_info_edge_id = if let Some(clone_map) = clone_id_map {
732 if let Some(new_edge_id) = clone_map.get(&extrusion_info_edge_id) {
733 *new_edge_id
734 } else {
735 extrusion_info_edge_id
736 }
737 } else {
738 any_edge_id
739 };
740 }
741
742 let mut sketch = sketch.clone();
743 match body_type {
744 BodyType::Solid => {
745 sketch.is_closed = ProfileClosed::Explicitly;
746 }
747 BodyType::Surface => {}
748 _other => {
749 }
752 }
753
754 match (extrude_method, being_extruded) {
755 (ExtrudeMethod::Merge, BeingExtruded::Face { .. }) => {
756 if let SketchSurface::Face(ref face) = sketch.on {
759 sketch.id = face.parent_solid.sketch_or_solid_id();
762 }
763 }
764 (ExtrudeMethod::New, BeingExtruded::Face { .. }) => {
765 sketch.id = extrude_cmd_id.into();
768 }
769 (ExtrudeMethod::New, BeingExtruded::Sketch) => {
770 }
773 (ExtrudeMethod::Merge, BeingExtruded::Sketch) => {
774 if let SketchSurface::Face(ref face) = sketch.on {
775 sketch.id = face.parent_solid.sketch_or_solid_id();
778 }
779 }
780 (other, _) => {
781 return Err(KclError::new_internal(KclErrorDetails::new(
783 format!("Zoo does not yet support creating bodies via {other:?}"),
784 vec![args.source_range],
785 )));
786 }
787 }
788
789 let sketch_id = if let Some(cloned_from) = sketch.clone
791 && clone_id_map.is_some()
792 {
793 cloned_from
794 } else {
795 sketch.id
796 };
797
798 let solid3d_info = exec_state
799 .send_modeling_cmd(
800 ModelingCmdMeta::from_args(exec_state, args),
801 ModelingCmd::from(
802 mcmd::Solid3dGetExtrusionFaceInfo::builder()
803 .edge_id(extrusion_info_edge_id)
804 .object_id(sketch_id)
805 .build(),
806 ),
807 )
808 .await?;
809
810 let face_infos = if let OkWebSocketResponseData::Modeling {
811 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
812 } = solid3d_info
813 {
814 data.faces
815 } else {
816 vec![]
817 };
818
819 if !args.ctx.settings.skip_artifact_graph {
821 if !sectional {
824 exec_state
825 .batch_modeling_cmd(
826 ModelingCmdMeta::from_args(exec_state, args),
827 ModelingCmd::from(
828 mcmd::Solid3dGetAdjacencyInfo::builder()
829 .object_id(sketch.id)
830 .edge_id(any_edge_id)
831 .build(),
832 ),
833 )
834 .await?;
835 }
836 }
837
838 let Faces {
839 sides: mut face_id_map,
840 start_cap_id,
841 end_cap_id,
842 } = analyze_faces(exec_state, args, face_infos).await;
843
844 if sketch.clone.is_some()
846 && let Some(clone_id_map) = clone_id_map
847 {
848 face_id_map = face_id_map
849 .into_iter()
850 .filter_map(|(k, v)| {
851 let fe_key = clone_id_map.get(&k)?;
852 let fe_value = clone_id_map.get(&(v?)).copied();
853 Some((*fe_key, fe_value))
854 })
855 .collect::<HashMap<Uuid, Option<Uuid>>>();
856 }
857
858 let no_engine_commands = args.ctx.no_engine_commands().await;
860 let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
861 let outer_surfaces = sketch.paths.iter().flat_map(|path| {
862 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
863 surface_of(path, *actual_face_id)
864 } else if no_engine_commands {
865 crate::log::logln!(
866 "No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
867 path.get_base().geo_meta.id
868 );
869 fake_extrude_surface(exec_state, path)
871 } else if sketch.clone.is_some()
872 && let Some(clone_map) = clone_id_map
873 {
874 let new_path = clone_map.get(&(path.get_base().geo_meta.id));
875
876 if let Some(new_path) = new_path {
877 match face_id_map.get(new_path) {
878 Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
879 _ => {
880 let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
881 if let Some(value) = value {
882 if value == new_path { Some(key) } else { None }
883 } else {
884 None
885 }
886 });
887 match actual_face_id {
888 Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
889 None => {
890 crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
891 None
892 }
893 }
894 }
895 }
896 } else {
897 None
898 }
899 } else {
900 crate::log::logln!(
901 "No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
902 path.get_base().geo_meta.id
903 );
904 None
905 }
906 });
907
908 new_value.extend(outer_surfaces);
909 let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
910 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
911 surface_of(path, *actual_face_id)
912 } else if no_engine_commands {
913 fake_extrude_surface(exec_state, path)
915 } else {
916 None
917 }
918 });
919 new_value.extend(inner_surfaces);
920
921 if let Some(tag_start) = named_cap_tags.start {
923 let Some(start_cap_id) = start_cap_id else {
924 return Err(KclError::new_type(KclErrorDetails::new(
925 format!(
926 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
927 tag_start.name, sketch.id
928 ),
929 vec![args.source_range],
930 )));
931 };
932
933 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
934 face_id: start_cap_id,
935 tag: Some(tag_start.clone()),
936 geo_meta: GeoMeta {
937 id: start_cap_id,
938 metadata: args.source_range.into(),
939 },
940 }));
941 }
942 if let Some(tag_end) = named_cap_tags.end {
943 let Some(end_cap_id) = end_cap_id else {
944 return Err(KclError::new_type(KclErrorDetails::new(
945 format!(
946 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
947 tag_end.name, sketch.id
948 ),
949 vec![args.source_range],
950 )));
951 };
952
953 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
954 face_id: end_cap_id,
955 tag: Some(tag_end.clone()),
956 geo_meta: GeoMeta {
957 id: end_cap_id,
958 metadata: args.source_range.into(),
959 },
960 }));
961 }
962
963 let meta = sketch.meta.clone();
964 let units = sketch.units;
965 let id = sketch.id;
966 let creator = match being_extruded {
967 BeingExtruded::Sketch => SolidCreator::Sketch(sketch),
968 BeingExtruded::Face { face_id, solid_id } => SolidCreator::Face(CreatorFace {
969 face_id,
970 solid_id,
971 sketch,
972 }),
973 };
974
975 Ok(Solid {
976 id,
977 value_id: extrude_cmd_id.into(),
978 artifact_id: extrude_cmd_id,
979 value: new_value,
980 meta,
981 units,
982 sectional,
983 creator,
984 start_cap_id,
985 end_cap_id,
986 edge_cuts: vec![],
987 })
988}
989
990#[derive(Default)]
991struct Faces {
992 sides: HashMap<Uuid, Option<Uuid>>,
994 end_cap_id: Option<Uuid>,
996 start_cap_id: Option<Uuid>,
998}
999
1000async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
1001 let mut faces = Faces {
1002 sides: HashMap::with_capacity(face_infos.len()),
1003 ..Default::default()
1004 };
1005 if args.ctx.no_engine_commands().await {
1006 faces.start_cap_id = Some(exec_state.next_uuid());
1008 faces.end_cap_id = Some(exec_state.next_uuid());
1009 }
1010 for face_info in face_infos {
1011 match face_info.cap {
1012 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
1013 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
1014 ExtrusionFaceCapType::Both => {
1015 faces.end_cap_id = face_info.face_id;
1016 faces.start_cap_id = face_info.face_id;
1017 }
1018 ExtrusionFaceCapType::None => {
1019 if let Some(curve_id) = face_info.curve_id {
1020 faces.sides.insert(curve_id, face_info.face_id);
1021 }
1022 }
1023 other => {
1024 exec_state.warn(
1025 crate::CompilationIssue {
1026 source_range: args.source_range,
1027 message: format!("unknown extrusion face type {other:?}"),
1028 suggestion: None,
1029 severity: crate::errors::Severity::Warning,
1030 tag: crate::errors::Tag::Unnecessary,
1031 },
1032 annotations::WARN_NOT_YET_SUPPORTED,
1033 );
1034 }
1035 }
1036 }
1037 faces
1038}
1039fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
1040 match path {
1041 Path::Arc { .. }
1042 | Path::TangentialArc { .. }
1043 | Path::TangentialArcTo { .. }
1044 | Path::Ellipse { .. }
1046 | Path::Conic {.. }
1047 | Path::Circle { .. }
1048 | Path::CircleThreePoint { .. } => {
1049 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1050 face_id: actual_face_id,
1051 tag: path.get_base().tag.clone(),
1052 geo_meta: GeoMeta {
1053 id: path.get_base().geo_meta.id,
1054 metadata: path.get_base().geo_meta.metadata,
1055 },
1056 });
1057 Some(extrude_surface)
1058 }
1059 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
1060 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1061 face_id: actual_face_id,
1062 tag: path.get_base().tag.clone(),
1063 geo_meta: GeoMeta {
1064 id: path.get_base().geo_meta.id,
1065 metadata: path.get_base().geo_meta.metadata,
1066 },
1067 });
1068 Some(extrude_surface)
1069 }
1070 Path::ArcThreePoint { .. } => {
1071 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1072 face_id: actual_face_id,
1073 tag: path.get_base().tag.clone(),
1074 geo_meta: GeoMeta {
1075 id: path.get_base().geo_meta.id,
1076 metadata: path.get_base().geo_meta.metadata,
1077 },
1078 });
1079 Some(extrude_surface)
1080 }
1081 }
1082}
1083
1084fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
1085 match path {
1086 Path::Arc { .. }
1087 | Path::TangentialArc { .. }
1088 | Path::TangentialArcTo { .. }
1089 | Path::Ellipse { .. }
1091 | Path::Conic {.. }
1092 | Path::Circle { .. }
1093 | Path::CircleThreePoint { .. } => {
1094 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1095 face_id: actual_face_id,
1096 tag: path.get_base().tag.clone(),
1097 geo_meta: GeoMeta {
1098 id: clone_path_id,
1099 metadata: path.get_base().geo_meta.metadata,
1100 },
1101 });
1102 Some(extrude_surface)
1103 }
1104 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
1105 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1106 face_id: actual_face_id,
1107 tag: path.get_base().tag.clone(),
1108 geo_meta: GeoMeta {
1109 id: clone_path_id,
1110 metadata: path.get_base().geo_meta.metadata,
1111 },
1112 });
1113 Some(extrude_surface)
1114 }
1115 Path::ArcThreePoint { .. } => {
1116 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1117 face_id: actual_face_id,
1118 tag: path.get_base().tag.clone(),
1119 geo_meta: GeoMeta {
1120 id: clone_path_id,
1121 metadata: path.get_base().geo_meta.metadata,
1122 },
1123 });
1124 Some(extrude_surface)
1125 }
1126 }
1127}
1128
1129fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
1131 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1132 face_id: exec_state.next_uuid(),
1134 tag: path.get_base().tag.clone(),
1135 geo_meta: GeoMeta {
1136 id: path.get_base().geo_meta.id,
1137 metadata: path.get_base().geo_meta.metadata,
1138 },
1139 });
1140 Some(extrude_surface)
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145 use kittycad_modeling_cmds::units::UnitLength;
1146
1147 use super::*;
1148 use crate::execution::AbstractSegment;
1149 use crate::execution::Plane;
1150 use crate::execution::SegmentRepr;
1151 use crate::execution::types::NumericType;
1152 use crate::front::Expr;
1153 use crate::front::Number;
1154 use crate::front::ObjectId;
1155 use crate::front::Point2d;
1156 use crate::front::PointCtor;
1157 use crate::std::sketch::PlaneData;
1158
1159 fn point_expr(x: f64, y: f64) -> Point2d<Expr> {
1160 Point2d {
1161 x: Expr::Var(Number::from((x, UnitLength::Millimeters))),
1162 y: Expr::Var(Number::from((y, UnitLength::Millimeters))),
1163 }
1164 }
1165
1166 fn segment_value(exec_state: &mut ExecState) -> KclValue {
1167 let plane = Plane::from_plane_data_skipping_engine(PlaneData::XY, exec_state).unwrap();
1168 let segment = Segment {
1169 id: exec_state.next_uuid(),
1170 object_id: ObjectId(1),
1171 kind: SegmentKind::Point {
1172 position: [TyF64::new(0.0, NumericType::mm()), TyF64::new(0.0, NumericType::mm())],
1173 ctor: Box::new(PointCtor {
1174 position: point_expr(0.0, 0.0),
1175 }),
1176 freedom: None,
1177 },
1178 surface: SketchSurface::Plane(Box::new(plane)),
1179 sketch_id: exec_state.next_uuid(),
1180 sketch: None,
1181 tag: None,
1182 node_path: None,
1183 meta: vec![],
1184 };
1185 KclValue::Segment {
1186 value: Box::new(AbstractSegment {
1187 repr: SegmentRepr::Solved {
1188 segment: Box::new(segment),
1189 },
1190 meta: vec![],
1191 }),
1192 }
1193 }
1194
1195 #[tokio::test(flavor = "multi_thread")]
1196 async fn segment_extrude_rejects_cap_tags() {
1197 let ctx = ExecutorContext::new_mock(None).await;
1198 let mut exec_state = ExecState::new(&ctx);
1199 let err = coerce_extrude_targets(
1200 vec![segment_value(&mut exec_state)],
1201 BodyType::Surface,
1202 Some(&TagDeclarator::new("cap_start")),
1203 None,
1204 &mut exec_state,
1205 &ctx,
1206 crate::SourceRange::default(),
1207 )
1208 .await
1209 .unwrap_err();
1210
1211 assert!(
1212 err.message()
1213 .contains("`tagStart` and `tagEnd` are not supported when extruding sketch segments"),
1214 "{err:?}"
1215 );
1216 ctx.close().await;
1217 }
1218}