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 {
434 entity_id: Some(plane_id),
435 entity_reference: None,
436 })
437 .extrude_method(extrude_method)
438 .body_type(body_type)
439 .build(),
440 )
441 }
442 Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
443 let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
444 ModelingCmd::from(
445 mcmd::ExtrudeToReference::builder()
446 .target(sketch_or_face_id.into())
447 .reference(ExtrudeReference::EntityReference {
448 entity_id: Some(edge_id),
449 entity_reference: None,
450 })
451 .extrude_method(extrude_method)
452 .body_type(body_type)
453 .build(),
454 )
455 }
456 Point3dAxis3dOrGeometryReference::Face(face_tag) => {
457 let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
458 ModelingCmd::from(
459 mcmd::ExtrudeToReference::builder()
460 .target(sketch_or_face_id.into())
461 .reference(ExtrudeReference::EntityReference {
462 entity_id: Some(face_id),
463 entity_reference: None,
464 })
465 .extrude_method(extrude_method)
466 .body_type(body_type)
467 .build(),
468 )
469 }
470 Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(
471 mcmd::ExtrudeToReference::builder()
472 .target(sketch_or_face_id.into())
473 .reference(ExtrudeReference::EntityReference {
474 entity_id: Some(sketch_ref.id),
475 entity_reference: None,
476 })
477 .extrude_method(extrude_method)
478 .body_type(body_type)
479 .build(),
480 ),
481 Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(
482 mcmd::ExtrudeToReference::builder()
483 .target(sketch_or_face_id.into())
484 .reference(ExtrudeReference::EntityReference {
485 entity_id: Some(solid.id),
486 entity_reference: None,
487 })
488 .extrude_method(extrude_method)
489 .body_type(body_type)
490 .build(),
491 ),
492 Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
493 let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
494 let tagged_edge_or_face_id = tagged_edge_or_face.id;
495 ModelingCmd::from(
496 mcmd::ExtrudeToReference::builder()
497 .target(sketch_or_face_id.into())
498 .reference(ExtrudeReference::EntityReference {
499 entity_id: Some(tagged_edge_or_face_id),
500 entity_reference: None,
501 })
502 .extrude_method(extrude_method)
503 .body_type(body_type)
504 .build(),
505 )
506 }
507 },
508 (Some(_), _, _, None, None) => {
509 return Err(KclError::new_semantic(KclErrorDetails::new(
510 "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
511 vec![args.source_range],
512 )));
513 }
514 (_, _, _, None, None) => {
515 return Err(KclError::new_semantic(KclErrorDetails::new(
516 "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
517 vec![args.source_range],
518 )));
519 }
520 (_, _, _, Some(_), Some(_)) => {
521 return Err(KclError::new_semantic(KclErrorDetails::new(
522 "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
523 vec![args.source_range],
524 )));
525 }
526 (_, _, _, _, _) => {
527 return Err(KclError::new_semantic(KclErrorDetails::new(
528 "Invalid combination of parameters for extrusion.".to_owned(),
529 vec![args.source_range],
530 )));
531 }
532 };
533
534 let being_extruded = match extrudable {
535 Extrudable::Sketch(..) => BeingExtruded::Sketch,
536 Extrudable::Face(face_tag) => {
537 let face_id = sketch_or_face_id;
538 let solid_id = match face_tag.geometry() {
539 Some(crate::execution::Geometry::Solid(solid)) => solid.id,
540 Some(crate::execution::Geometry::Sketch(sketch)) => match sketch.on {
541 SketchSurface::Face(face) => face.parent_solid.solid_id,
542 SketchSurface::Plane(_) => sketch.id,
543 },
544 None => face_id,
545 };
546 BeingExtruded::Face { face_id, solid_id }
547 }
548 };
549 if let Some(post_extr_sketch) = extrudable.as_sketch() {
550 let cmds = post_extr_sketch.build_sketch_mode_cmds(
551 exec_state,
552 ModelingCmdReq {
553 cmd_id: extrude_cmd_id.into(),
554 cmd,
555 },
556 );
557 exec_state
558 .batch_modeling_cmds(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), &cmds)
559 .await?;
560 solids.push(
561 do_post_extrude(
562 &post_extr_sketch,
563 extrude_cmd_id.into(),
564 false,
565 &NamedCapTags {
566 start: tag_start.as_ref(),
567 end: tag_end.as_ref(),
568 },
569 extrude_method,
570 exec_state,
571 &args,
572 None,
573 None,
574 body_type,
575 being_extruded,
576 )
577 .await?,
578 );
579 } else {
580 return Err(KclError::new_type(KclErrorDetails::new(
581 "Expected a sketch for extrusion".to_owned(),
582 vec![args.source_range],
583 )));
584 }
585 }
586
587 Ok(solids)
588}
589
590#[derive(Debug, Default)]
591pub(crate) struct NamedCapTags<'a> {
592 pub start: Option<&'a TagNode>,
593 pub end: Option<&'a TagNode>,
594}
595
596#[derive(Debug, Clone, Copy)]
597pub enum BeingExtruded {
598 Sketch,
599 Face { face_id: Uuid, solid_id: Uuid },
600}
601
602#[allow(clippy::too_many_arguments)]
603pub(crate) async fn do_post_extrude<'a>(
604 sketch: &Sketch,
605 extrude_cmd_id: ArtifactId,
606 sectional: bool,
607 named_cap_tags: &'a NamedCapTags<'a>,
608 extrude_method: ExtrudeMethod,
609 exec_state: &mut ExecState,
610 args: &Args,
611 edge_id: Option<Uuid>,
612 clone_id_map: Option<&HashMap<Uuid, Uuid>>, body_type: BodyType,
614 being_extruded: BeingExtruded,
615) -> Result<Solid, KclError> {
616 exec_state
620 .batch_modeling_cmd(
621 ModelingCmdMeta::from_args(exec_state, args),
622 ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(sketch.id).build()),
623 )
624 .await?;
625
626 let any_edge_id = if let Some(edge_id) = sketch.mirror {
627 edge_id
628 } else if let Some(id) = edge_id {
629 id
630 } else {
631 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
634 return Err(KclError::new_type(KclErrorDetails::new(
635 "Expected a non-empty sketch".to_owned(),
636 vec![args.source_range],
637 )));
638 };
639 any_edge_id
640 };
641
642 let mut extrusion_info_edge_id = any_edge_id;
644 if sketch.clone.is_some() && clone_id_map.is_some() {
645 extrusion_info_edge_id = if let Some(clone_map) = clone_id_map {
646 if let Some(new_edge_id) = clone_map.get(&extrusion_info_edge_id) {
647 *new_edge_id
648 } else {
649 extrusion_info_edge_id
650 }
651 } else {
652 any_edge_id
653 };
654 }
655
656 let mut sketch = sketch.clone();
657 match body_type {
658 BodyType::Solid => {
659 sketch.is_closed = ProfileClosed::Explicitly;
660 }
661 BodyType::Surface => {}
662 _other => {
663 }
666 }
667
668 match (extrude_method, being_extruded) {
669 (ExtrudeMethod::Merge, BeingExtruded::Face { .. }) => {
670 if let SketchSurface::Face(ref face) = sketch.on {
673 sketch.id = face.parent_solid.sketch_or_solid_id();
676 }
677 }
678 (ExtrudeMethod::New, BeingExtruded::Face { .. }) => {
679 sketch.id = extrude_cmd_id.into();
682 }
683 (ExtrudeMethod::New, BeingExtruded::Sketch) => {
684 }
687 (ExtrudeMethod::Merge, BeingExtruded::Sketch) => {
688 if let SketchSurface::Face(ref face) = sketch.on {
689 sketch.id = face.parent_solid.sketch_or_solid_id();
692 }
693 }
694 (other, _) => {
695 return Err(KclError::new_internal(KclErrorDetails::new(
697 format!("Zoo does not yet support creating bodies via {other:?}"),
698 vec![args.source_range],
699 )));
700 }
701 }
702
703 let sketch_id = if let Some(cloned_from) = sketch.clone
705 && clone_id_map.is_some()
706 {
707 cloned_from
708 } else {
709 sketch.id
710 };
711
712 let solid3d_info = exec_state
713 .send_modeling_cmd(
714 ModelingCmdMeta::from_args(exec_state, args),
715 ModelingCmd::from(
716 mcmd::Solid3dGetExtrusionFaceInfo::builder()
717 .edge_id(extrusion_info_edge_id)
718 .object_id(sketch_id)
719 .build(),
720 ),
721 )
722 .await?;
723
724 let face_infos = if let OkWebSocketResponseData::Modeling {
725 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
726 } = solid3d_info
727 {
728 data.faces
729 } else {
730 vec![]
731 };
732
733 if !args.ctx.settings.skip_artifact_graph {
735 if !sectional {
738 exec_state
739 .batch_modeling_cmd(
740 ModelingCmdMeta::from_args(exec_state, args),
741 ModelingCmd::from(
742 mcmd::Solid3dGetAdjacencyInfo::builder()
743 .object_id(sketch.id)
744 .edge_id(any_edge_id)
745 .build(),
746 ),
747 )
748 .await?;
749 }
750 }
751
752 let Faces {
753 sides: mut face_id_map,
754 start_cap_id,
755 end_cap_id,
756 } = analyze_faces(exec_state, args, face_infos).await;
757
758 if sketch.clone.is_some()
760 && let Some(clone_id_map) = clone_id_map
761 {
762 face_id_map = face_id_map
763 .into_iter()
764 .filter_map(|(k, v)| {
765 let fe_key = clone_id_map.get(&k)?;
766 let fe_value = clone_id_map.get(&(v?)).copied();
767 Some((*fe_key, fe_value))
768 })
769 .collect::<HashMap<Uuid, Option<Uuid>>>();
770 }
771
772 let no_engine_commands = args.ctx.no_engine_commands().await;
774 let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
775 let outer_surfaces = sketch.paths.iter().flat_map(|path| {
776 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
777 surface_of(path, *actual_face_id)
778 } else if no_engine_commands {
779 crate::log::logln!(
780 "No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
781 path.get_base().geo_meta.id
782 );
783 fake_extrude_surface(exec_state, path)
785 } else if sketch.clone.is_some()
786 && let Some(clone_map) = clone_id_map
787 {
788 let new_path = clone_map.get(&(path.get_base().geo_meta.id));
789
790 if let Some(new_path) = new_path {
791 match face_id_map.get(new_path) {
792 Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
793 _ => {
794 let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
795 if let Some(value) = value {
796 if value == new_path { Some(key) } else { None }
797 } else {
798 None
799 }
800 });
801 match actual_face_id {
802 Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
803 None => {
804 crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
805 None
806 }
807 }
808 }
809 }
810 } else {
811 None
812 }
813 } else {
814 crate::log::logln!(
815 "No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
816 path.get_base().geo_meta.id
817 );
818 None
819 }
820 });
821
822 new_value.extend(outer_surfaces);
823 let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
824 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
825 surface_of(path, *actual_face_id)
826 } else if no_engine_commands {
827 fake_extrude_surface(exec_state, path)
829 } else {
830 None
831 }
832 });
833 new_value.extend(inner_surfaces);
834
835 if let Some(tag_start) = named_cap_tags.start {
837 let Some(start_cap_id) = start_cap_id else {
838 return Err(KclError::new_type(KclErrorDetails::new(
839 format!(
840 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
841 tag_start.name, sketch.id
842 ),
843 vec![args.source_range],
844 )));
845 };
846
847 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
848 face_id: start_cap_id,
849 tag: Some(tag_start.clone()),
850 geo_meta: GeoMeta {
851 id: start_cap_id,
852 metadata: args.source_range.into(),
853 },
854 }));
855 }
856 if let Some(tag_end) = named_cap_tags.end {
857 let Some(end_cap_id) = end_cap_id else {
858 return Err(KclError::new_type(KclErrorDetails::new(
859 format!(
860 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
861 tag_end.name, sketch.id
862 ),
863 vec![args.source_range],
864 )));
865 };
866
867 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
868 face_id: end_cap_id,
869 tag: Some(tag_end.clone()),
870 geo_meta: GeoMeta {
871 id: end_cap_id,
872 metadata: args.source_range.into(),
873 },
874 }));
875 }
876
877 let meta = sketch.meta.clone();
878 let units = sketch.units;
879 let id = sketch.id;
880 let creator = match being_extruded {
881 BeingExtruded::Sketch => SolidCreator::Sketch(sketch),
882 BeingExtruded::Face { face_id, solid_id } => SolidCreator::Face(CreatorFace {
883 face_id,
884 solid_id,
885 sketch,
886 }),
887 };
888
889 Ok(Solid {
890 id,
891 value_id: extrude_cmd_id.into(),
892 artifact_id: extrude_cmd_id,
893 value: new_value,
894 meta,
895 units,
896 sectional,
897 creator,
898 start_cap_id,
899 end_cap_id,
900 edge_cuts: vec![],
901 })
902}
903
904#[derive(Default)]
905struct Faces {
906 sides: HashMap<Uuid, Option<Uuid>>,
908 end_cap_id: Option<Uuid>,
910 start_cap_id: Option<Uuid>,
912}
913
914async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
915 let mut faces = Faces {
916 sides: HashMap::with_capacity(face_infos.len()),
917 ..Default::default()
918 };
919 if args.ctx.no_engine_commands().await {
920 faces.start_cap_id = Some(exec_state.next_uuid());
922 faces.end_cap_id = Some(exec_state.next_uuid());
923 }
924 for face_info in face_infos {
925 match face_info.cap {
926 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
927 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
928 ExtrusionFaceCapType::Both => {
929 faces.end_cap_id = face_info.face_id;
930 faces.start_cap_id = face_info.face_id;
931 }
932 ExtrusionFaceCapType::None => {
933 if let Some(curve_id) = face_info.curve_id {
934 faces.sides.insert(curve_id, face_info.face_id);
935 }
936 }
937 other => {
938 exec_state.warn(
939 crate::CompilationIssue {
940 source_range: args.source_range,
941 message: format!("unknown extrusion face type {other:?}"),
942 suggestion: None,
943 severity: crate::errors::Severity::Warning,
944 tag: crate::errors::Tag::Unnecessary,
945 },
946 annotations::WARN_NOT_YET_SUPPORTED,
947 );
948 }
949 }
950 }
951 faces
952}
953fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
954 match path {
955 Path::Arc { .. }
956 | Path::TangentialArc { .. }
957 | Path::TangentialArcTo { .. }
958 | Path::Ellipse { .. }
960 | Path::Conic {.. }
961 | Path::Circle { .. }
962 | Path::CircleThreePoint { .. } => {
963 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
964 face_id: actual_face_id,
965 tag: path.get_base().tag.clone(),
966 geo_meta: GeoMeta {
967 id: path.get_base().geo_meta.id,
968 metadata: path.get_base().geo_meta.metadata,
969 },
970 });
971 Some(extrude_surface)
972 }
973 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
974 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
975 face_id: actual_face_id,
976 tag: path.get_base().tag.clone(),
977 geo_meta: GeoMeta {
978 id: path.get_base().geo_meta.id,
979 metadata: path.get_base().geo_meta.metadata,
980 },
981 });
982 Some(extrude_surface)
983 }
984 Path::ArcThreePoint { .. } => {
985 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
986 face_id: actual_face_id,
987 tag: path.get_base().tag.clone(),
988 geo_meta: GeoMeta {
989 id: path.get_base().geo_meta.id,
990 metadata: path.get_base().geo_meta.metadata,
991 },
992 });
993 Some(extrude_surface)
994 }
995 }
996}
997
998fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
999 match path {
1000 Path::Arc { .. }
1001 | Path::TangentialArc { .. }
1002 | Path::TangentialArcTo { .. }
1003 | Path::Ellipse { .. }
1005 | Path::Conic {.. }
1006 | Path::Circle { .. }
1007 | Path::CircleThreePoint { .. } => {
1008 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1009 face_id: actual_face_id,
1010 tag: path.get_base().tag.clone(),
1011 geo_meta: GeoMeta {
1012 id: clone_path_id,
1013 metadata: path.get_base().geo_meta.metadata,
1014 },
1015 });
1016 Some(extrude_surface)
1017 }
1018 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
1019 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1020 face_id: actual_face_id,
1021 tag: path.get_base().tag.clone(),
1022 geo_meta: GeoMeta {
1023 id: clone_path_id,
1024 metadata: path.get_base().geo_meta.metadata,
1025 },
1026 });
1027 Some(extrude_surface)
1028 }
1029 Path::ArcThreePoint { .. } => {
1030 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1031 face_id: actual_face_id,
1032 tag: path.get_base().tag.clone(),
1033 geo_meta: GeoMeta {
1034 id: clone_path_id,
1035 metadata: path.get_base().geo_meta.metadata,
1036 },
1037 });
1038 Some(extrude_surface)
1039 }
1040 }
1041}
1042
1043fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
1045 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1046 face_id: exec_state.next_uuid(),
1048 tag: path.get_base().tag.clone(),
1049 geo_meta: GeoMeta {
1050 id: path.get_base().geo_meta.id,
1051 metadata: path.get_base().geo_meta.metadata,
1052 },
1053 });
1054 Some(extrude_surface)
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use kittycad_modeling_cmds::units::UnitLength;
1060
1061 use super::*;
1062 use crate::execution::AbstractSegment;
1063 use crate::execution::Plane;
1064 use crate::execution::SegmentRepr;
1065 use crate::execution::types::NumericType;
1066 use crate::front::Expr;
1067 use crate::front::Number;
1068 use crate::front::ObjectId;
1069 use crate::front::Point2d;
1070 use crate::front::PointCtor;
1071 use crate::std::sketch::PlaneData;
1072
1073 fn point_expr(x: f64, y: f64) -> Point2d<Expr> {
1074 Point2d {
1075 x: Expr::Var(Number::from((x, UnitLength::Millimeters))),
1076 y: Expr::Var(Number::from((y, UnitLength::Millimeters))),
1077 }
1078 }
1079
1080 fn segment_value(exec_state: &mut ExecState) -> KclValue {
1081 let plane = Plane::from_plane_data_skipping_engine(PlaneData::XY, exec_state).unwrap();
1082 let segment = Segment {
1083 id: exec_state.next_uuid(),
1084 object_id: ObjectId(1),
1085 kind: SegmentKind::Point {
1086 position: [TyF64::new(0.0, NumericType::mm()), TyF64::new(0.0, NumericType::mm())],
1087 ctor: Box::new(PointCtor {
1088 position: point_expr(0.0, 0.0),
1089 }),
1090 freedom: None,
1091 },
1092 surface: SketchSurface::Plane(Box::new(plane)),
1093 sketch_id: exec_state.next_uuid(),
1094 sketch: None,
1095 tag: None,
1096 node_path: None,
1097 meta: vec![],
1098 };
1099 KclValue::Segment {
1100 value: Box::new(AbstractSegment {
1101 repr: SegmentRepr::Solved {
1102 segment: Box::new(segment),
1103 },
1104 meta: vec![],
1105 }),
1106 }
1107 }
1108
1109 #[tokio::test(flavor = "multi_thread")]
1110 async fn segment_extrude_rejects_cap_tags() {
1111 let ctx = ExecutorContext::new_mock(None).await;
1112 let mut exec_state = ExecState::new(&ctx);
1113 let err = coerce_extrude_targets(
1114 vec![segment_value(&mut exec_state)],
1115 BodyType::Surface,
1116 Some(&TagDeclarator::new("cap_start")),
1117 None,
1118 &mut exec_state,
1119 &ctx,
1120 crate::SourceRange::default(),
1121 )
1122 .await
1123 .unwrap_err();
1124
1125 assert!(
1126 err.message()
1127 .contains("`tagStart` and `tagEnd` are not supported when extruding sketch segments"),
1128 "{err:?}"
1129 );
1130 ctx.close().await;
1131 }
1132}