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