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