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