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
179pub async 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 let kind_of_extrude = match body_type {
216 BodyType::Solid => "solid extrude",
217 BodyType::Surface => "surface extrude",
218 _ => "non-surface extrude",
219 };
220 return Err(KclError::new_semantic(KclErrorDetails::new(
221 format!(
222 "You're trying to perform a {kind_of_extrude} on an edge, but edges can only be extruded with surface extrudes. To do a solid extrude, select a closed sketch region instead. To extrude these edges, do a surface extrude by using `bodyType = SURFACE` instead."
223 ),
224 vec![source_range],
225 )));
226 }
227
228 if tag_start.is_some() || tag_end.is_some() {
229 return Err(KclError::new_semantic(KclErrorDetails::new(
230 "`tagStart` and `tagEnd` are not supported when extruding sketch segments. Segment surface extrudes do not create start or end caps."
231 .to_owned(),
232 vec![source_range],
233 )));
234 }
235
236 let synthetic_sketch = build_segment_surface_sketch(segments, exec_state, ctx, source_range).await?;
237 return Ok(vec![Extrudable::from(synthetic_sketch)]);
238 }
239
240 Ok(extrudables)
241}
242
243pub(crate) async fn build_segment_surface_sketch(
244 mut segments: Vec<Segment>,
245 exec_state: &mut ExecState,
246 ctx: &ExecutorContext,
247 source_range: crate::SourceRange,
248) -> Result<Sketch, KclError> {
249 let Some(first_segment) = segments.first() else {
250 return Err(KclError::new_semantic(KclErrorDetails::new(
251 "Expected at least one sketch segment.".to_owned(),
252 vec![source_range],
253 )));
254 };
255
256 let sketch_id = first_segment.sketch_id;
257 let sketch_surface = first_segment.surface.clone();
258 for segment in &segments {
259 if segment.sketch_id != sketch_id {
260 return Err(KclError::new_semantic(KclErrorDetails::new(
261 "All sketch segments passed to this operation must come from the same sketch.".to_owned(),
262 vec![source_range],
263 )));
264 }
265
266 if segment.surface != sketch_surface {
267 return Err(KclError::new_semantic(KclErrorDetails::new(
268 "All sketch segments passed to this operation must lie on the same sketch surface.".to_owned(),
269 vec![source_range],
270 )));
271 }
272
273 if matches!(segment.kind, SegmentKind::Point { .. }) {
274 return Err(KclError::new_semantic(KclErrorDetails::new(
275 "Point segments cannot be used here. Select line, arc, or circle segments instead.".to_owned(),
276 vec![source_range],
277 )));
278 }
279
280 if segment.is_construction() {
281 return Err(KclError::new_semantic(KclErrorDetails::new(
282 "Construction segments cannot be used here. Select non-construction sketch segments instead."
283 .to_owned(),
284 vec![source_range],
285 )));
286 }
287 }
288
289 let synthetic_sketch_id = exec_state.next_uuid();
290 let segment_tags = IndexMap::from_iter(segments.iter().filter_map(|segment| {
291 segment
292 .tag
293 .as_ref()
294 .map(|tag| (segment.object_id, TagDeclarator::new(&tag.value)))
295 }));
296
297 for segment in &mut segments {
298 segment.id = exec_state.next_uuid();
299 segment.sketch_id = synthetic_sketch_id;
300 segment.sketch = None;
301 }
302
303 create_segments_in_engine(
304 &sketch_surface,
305 synthetic_sketch_id,
306 &mut segments,
307 &segment_tags,
308 ctx,
309 exec_state,
310 source_range,
311 )
312 .await?
313 .ok_or_else(|| {
314 KclError::new_semantic(KclErrorDetails::new(
315 "Expected at least one usable sketch segment.".to_owned(),
316 vec![source_range],
317 ))
318 })
319}
320
321#[allow(clippy::too_many_arguments)]
322async fn inner_extrude(
323 extrudables: Vec<Extrudable>,
324 length: Option<TyF64>,
325 to: Option<Point3dAxis3dOrGeometryReference>,
326 symmetric: Option<bool>,
327 direction: Option<Point3dOrEdgeReference>,
328 bidirectional_length: Option<TyF64>,
329 tag_start: Option<TagNode>,
330 tag_end: Option<TagNode>,
331 draft_angle: Option<TyF64>,
332 twist_angle: Option<TyF64>,
333 twist_angle_step: Option<TyF64>,
334 twist_center: Option<[TyF64; 2]>,
335 tolerance: Option<TyF64>,
336 method: Option<String>,
337 hide_seams: Option<bool>,
338 body_type: Option<BodyType>,
339 exec_state: &mut ExecState,
340 args: Args,
341) -> Result<Vec<Solid>, KclError> {
342 let body_type = body_type.unwrap_or_default();
343
344 if matches!(body_type, BodyType::Solid) && extrudables.iter().any(|sk| matches!(sk.is_closed(), ProfileClosed::No))
345 {
346 return Err(KclError::new_semantic(KclErrorDetails::new(
347 "Cannot solid extrude an open profile. Either close the profile, or use a surface extrude.".to_owned(),
348 vec![args.source_range],
349 )));
350 }
351
352 if draft_angle.is_some() && twist_angle.is_some() {
353 return Err(KclError::new_semantic(KclErrorDetails::new(
354 "Zoo currently does not support adding both draft angle and twist angle to an extrude simultaneously"
355 .to_owned(),
356 vec![args.source_range],
357 )));
358 }
359
360 if direction.is_some() && twist_angle.is_some() {
361 return Err(KclError::new_semantic(KclErrorDetails::new(
362 "Zoo currently does not support adding both direction and twist angle to an extrude simultaneously"
363 .to_owned(),
364 vec![args.source_range],
365 )));
366 }
367
368 let mut solids = Vec::new();
370 let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
371
372 let extrude_method = match method.as_deref() {
373 Some("new" | "NEW") => ExtrudeMethod::New,
374 Some("merge" | "MERGE") => ExtrudeMethod::Merge,
375 None => ExtrudeMethod::default(),
376 Some(other) => {
377 return Err(KclError::new_semantic(KclErrorDetails::new(
378 format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
379 vec![args.source_range],
380 )));
381 }
382 };
383
384 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
385 return Err(KclError::new_semantic(KclErrorDetails::new(
386 "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
387 .to_owned(),
388 vec![args.source_range],
389 )));
390 }
391
392 if (length.is_some() || twist_angle.is_some()) && to.is_some() {
393 return Err(KclError::new_semantic(KclErrorDetails::new(
394 "You cannot give `length` or `twist` params with the `to` param, you have to choose one or the other"
395 .to_owned(),
396 vec![args.source_range],
397 )));
398 }
399
400 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
401
402 let opposite = match (symmetric, bidirection) {
403 (Some(true), _) => Opposite::Symmetric,
404 (None, None) => Opposite::None,
405 (Some(false), None) => Opposite::None,
406 (None, Some(length)) => Opposite::Other(length),
407 (Some(false), Some(length)) => Opposite::Other(length),
408 };
409
410 for extrudable in &extrudables {
411 let extrude_cmd_id = exec_state.next_uuid();
412 let sketch_or_face_id = extrudable.id_to_extrude(exec_state, &args, false).await?;
413 let cmd = match (
414 &twist_angle,
415 &twist_angle_step,
416 &twist_center,
417 length.clone(),
418 &to,
419 &direction,
420 ) {
421 (Some(angle), angle_step, center, Some(length), None, None) => {
422 let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
423 let total_rotation_angle = Angle::from_degrees(angle.to_degrees(exec_state, args.source_range));
424 let angle_step_size = Angle::from_degrees(
425 angle_step
426 .clone()
427 .map(|a| a.to_degrees(exec_state, args.source_range))
428 .unwrap_or(15.0),
429 );
430 ModelingCmd::from(
431 mcmd::TwistExtrude::builder()
432 .target(sketch_or_face_id.into())
433 .distance(LengthUnit(length.to_mm()))
434 .center_2d(center)
435 .total_rotation_angle(total_rotation_angle)
436 .angle_step_size(angle_step_size)
437 .tolerance(tolerance)
438 .body_type(body_type)
439 .build(),
440 )
441 }
442 (None, None, None, Some(length), None, None) => ModelingCmd::from(
443 mcmd::Extrude::builder()
444 .target(sketch_or_face_id.into())
445 .distance(LengthUnit(length.to_mm()))
446 .opposite(opposite.clone())
447 .maybe_draft_angle(
448 draft_angle
449 .clone()
450 .map(|a| Angle::from_degrees(a.to_degrees(exec_state, args.source_range))),
451 )
452 .extrude_method(extrude_method)
453 .body_type(body_type)
454 .maybe_merge_coplanar_faces(hide_seams)
455 .build(),
456 ),
457 (None, None, None, Some(length), None, Some(dir)) => {
458 let direction3d = match dir {
459 Point3dOrEdgeReference::Point(p) => DirectionType::Axis {
460 direction: KPoint3d {
461 x: p[0].n,
462 y: p[1].n,
463 z: p[2].n,
464 },
465 },
466 Point3dOrEdgeReference::Edge(edge) => match edge {
467 crate::std::fillet::EdgeReference::Uuid(uuid) => DirectionType::Edge { id: *uuid },
468 crate::std::fillet::EdgeReference::Tag(tag) => DirectionType::Edge {
469 id: match tag.get_cur_info() {
470 Some(info) => info.id,
471 None => {
472 return Err(KclError::new_semantic(KclErrorDetails::new(
473 "Failed to get current info for tag".to_string(),
474 vec![args.source_range],
475 )));
476 }
477 },
478 },
479 },
480 };
481 ModelingCmd::from(
482 mcmd::Extrude::builder()
483 .target(sketch_or_face_id.into())
484 .distance(LengthUnit(length.to_mm()))
485 .opposite(opposite.clone())
486 .maybe_draft_angle(
487 draft_angle
488 .clone()
489 .map(|a| Angle::from_degrees(a.to_degrees(exec_state, args.source_range))),
490 )
491 .extrude_method(extrude_method)
492 .body_type(body_type)
493 .maybe_merge_coplanar_faces(hide_seams)
494 .direction(direction3d)
495 .build(),
496 )
497 }
498 (None, None, None, None, Some(to), None) => match to {
499 Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(
500 mcmd::ExtrudeToReference::builder()
501 .target(sketch_or_face_id.into())
502 .reference(ExtrudeReference::Point {
503 point: KPoint3d {
504 x: LengthUnit(point[0].to_mm()),
505 y: LengthUnit(point[1].to_mm()),
506 z: LengthUnit(point[2].to_mm()),
507 },
508 })
509 .extrude_method(extrude_method)
510 .body_type(body_type)
511 .build(),
512 ),
513 Point3dAxis3dOrGeometryReference::Axis { direction, origin } => ModelingCmd::from(
514 mcmd::ExtrudeToReference::builder()
515 .target(sketch_or_face_id.into())
516 .reference(ExtrudeReference::Axis {
517 axis: KPoint3d {
518 x: direction[0].to_mm(),
519 y: direction[1].to_mm(),
520 z: direction[2].to_mm(),
521 },
522 point: KPoint3d {
523 x: LengthUnit(origin[0].to_mm()),
524 y: LengthUnit(origin[1].to_mm()),
525 z: LengthUnit(origin[2].to_mm()),
526 },
527 })
528 .extrude_method(extrude_method)
529 .body_type(body_type)
530 .build(),
531 ),
532 Point3dAxis3dOrGeometryReference::Plane(plane) => {
533 let plane_id = if plane.is_uninitialized() {
534 if plane.info.origin.units.is_none() {
535 return Err(KclError::new_semantic(KclErrorDetails::new(
536 "Origin of plane has unknown units".to_string(),
537 vec![args.source_range],
538 )));
539 }
540 let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
541 plane.clone().info.into_plane_data(),
542 exec_state,
543 &args,
544 )
545 .await?;
546 sketch_plane.id
547 } else {
548 plane.id
549 };
550 ModelingCmd::from(
551 mcmd::ExtrudeToReference::builder()
552 .target(sketch_or_face_id.into())
553 .reference(ExtrudeReference::EntityReference {
554 entity_id: Some(plane_id),
555 entity_reference: None,
556 })
557 .extrude_method(extrude_method)
558 .body_type(body_type)
559 .build(),
560 )
561 }
562 Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
563 let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
564 ModelingCmd::from(
565 mcmd::ExtrudeToReference::builder()
566 .target(sketch_or_face_id.into())
567 .reference(ExtrudeReference::EntityReference {
568 entity_id: Some(edge_id),
569 entity_reference: None,
570 })
571 .extrude_method(extrude_method)
572 .body_type(body_type)
573 .build(),
574 )
575 }
576 Point3dAxis3dOrGeometryReference::Face(face_tag) => {
577 let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
578 ModelingCmd::from(
579 mcmd::ExtrudeToReference::builder()
580 .target(sketch_or_face_id.into())
581 .reference(ExtrudeReference::EntityReference {
582 entity_id: Some(face_id),
583 entity_reference: None,
584 })
585 .extrude_method(extrude_method)
586 .body_type(body_type)
587 .build(),
588 )
589 }
590 Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(
591 mcmd::ExtrudeToReference::builder()
592 .target(sketch_or_face_id.into())
593 .reference(ExtrudeReference::EntityReference {
594 entity_id: Some(sketch_ref.id),
595 entity_reference: None,
596 })
597 .extrude_method(extrude_method)
598 .body_type(body_type)
599 .build(),
600 ),
601 Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(
602 mcmd::ExtrudeToReference::builder()
603 .target(sketch_or_face_id.into())
604 .reference(ExtrudeReference::EntityReference {
605 entity_id: Some(solid.id),
606 entity_reference: None,
607 })
608 .extrude_method(extrude_method)
609 .body_type(body_type)
610 .build(),
611 ),
612 Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
613 let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
614 let tagged_edge_or_face_id = tagged_edge_or_face.id;
615 ModelingCmd::from(
616 mcmd::ExtrudeToReference::builder()
617 .target(sketch_or_face_id.into())
618 .reference(ExtrudeReference::EntityReference {
619 entity_id: Some(tagged_edge_or_face_id),
620 entity_reference: None,
621 })
622 .extrude_method(extrude_method)
623 .body_type(body_type)
624 .build(),
625 )
626 }
627 Point3dAxis3dOrGeometryReference::EdgeToReference(spec) => {
628 let inner = edge::resolve_edge_specifier_with_face_tags(spec, exec_state, &args).await?;
629 ModelingCmd::from(
630 mcmd::ExtrudeToReference::builder()
631 .target(sketch_or_face_id.into())
632 .reference(ExtrudeReference::EntityReference {
633 entity_id: None,
634 entity_reference: Some(EntityReference::Edge {
635 inner,
636 topology_fallback: None,
637 }),
638 })
639 .extrude_method(extrude_method)
640 .body_type(body_type)
641 .build(),
642 )
643 }
644 },
645 (Some(_), _, _, None, None, None) => {
646 return Err(KclError::new_semantic(KclErrorDetails::new(
647 "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
648 vec![args.source_range],
649 )));
650 }
651 (_, _, _, None, None, None) => {
652 return Err(KclError::new_semantic(KclErrorDetails::new(
653 "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
654 vec![args.source_range],
655 )));
656 }
657 (_, _, _, Some(_), Some(_), None) => {
658 return Err(KclError::new_semantic(KclErrorDetails::new(
659 "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
660 vec![args.source_range],
661 )));
662 }
663 (_, _, _, _, _, _) => {
664 return Err(KclError::new_semantic(KclErrorDetails::new(
665 "Invalid combination of parameters for extrusion.".to_owned(),
666 vec![args.source_range],
667 )));
668 }
669 };
670
671 let being_extruded = match extrudable {
672 Extrudable::Sketch(..) => BeingExtruded::Sketch,
673 Extrudable::FaceTag(face_tag) => {
674 let face_id = sketch_or_face_id;
675 let solid_id = match face_tag.geometry() {
676 Some(crate::execution::Geometry::Solid(solid)) => solid.id,
677 Some(crate::execution::Geometry::Sketch(sketch)) => match sketch.on {
678 SketchSurface::Face(face) => face.parent_solid.solid_id,
679 SketchSurface::Plane(_) => sketch.id,
680 },
681 None => face_id,
682 };
683 BeingExtruded::Face { face_id, solid_id }
684 }
685 Extrudable::Face(face) => BeingExtruded::Face {
686 face_id: face.id,
687 solid_id: face.parent_solid.solid_id,
688 },
689 };
690 if let Some(post_extr_sketch) = extrudable.as_sketch() {
691 let cmds = post_extr_sketch.build_sketch_mode_cmds(
692 exec_state,
693 ModelingCmdReq {
694 cmd_id: extrude_cmd_id.into(),
695 cmd,
696 },
697 );
698 exec_state
699 .batch_modeling_cmds(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), &cmds)
700 .await?;
701 solids.push(
702 do_post_extrude(
703 &post_extr_sketch,
704 extrude_cmd_id.into(),
705 false,
706 &NamedCapTags {
707 start: tag_start.as_ref(),
708 end: tag_end.as_ref(),
709 },
710 extrude_method,
711 exec_state,
712 &args,
713 None,
714 None,
715 body_type,
716 being_extruded,
717 )
718 .await?,
719 );
720 } else {
721 return Err(KclError::new_type(KclErrorDetails::new(
722 "Expected a sketch for extrusion".to_owned(),
723 vec![args.source_range],
724 )));
725 }
726 }
727
728 Ok(solids)
729}
730
731#[derive(Debug, Default)]
732pub(crate) struct NamedCapTags<'a> {
733 pub start: Option<&'a TagNode>,
734 pub end: Option<&'a TagNode>,
735}
736
737#[derive(Debug, Clone, Copy)]
738pub enum BeingExtruded {
739 Sketch,
740 Face { face_id: Uuid, solid_id: Uuid },
741}
742
743fn get_extrusion_info_edge_id(sketch: &Sketch, any_edge_id: Uuid, clone_id_map: Option<&HashMap<Uuid, Uuid>>) -> Uuid {
747 if sketch.clone.is_none() {
750 return any_edge_id;
751 }
752 let Some(clone_map) = clone_id_map else {
753 return any_edge_id;
754 };
755
756 if clone_map.contains_key(&any_edge_id) {
761 return any_edge_id;
762 }
763
764 if let Some((old_edge_id, _)) = clone_map.iter().find(|(_, new_edge_id)| **new_edge_id == any_edge_id) {
769 return *old_edge_id;
770 }
771
772 any_edge_id
776}
777
778#[allow(clippy::too_many_arguments)]
779pub(crate) async fn do_post_extrude<'a>(
780 sketch: &Sketch,
781 extrude_cmd_id: ArtifactId,
782 sectional: bool,
783 named_cap_tags: &'a NamedCapTags<'a>,
784 extrude_method: ExtrudeMethod,
785 exec_state: &mut ExecState,
786 args: &Args,
787 edge_id: Option<Uuid>,
788 clone_id_map: Option<&HashMap<Uuid, Uuid>>, body_type: BodyType,
790 being_extruded: BeingExtruded,
791) -> Result<Solid, KclError> {
792 exec_state
796 .batch_modeling_cmd(
797 ModelingCmdMeta::from_args(exec_state, args),
798 ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(sketch.id).build()),
799 )
800 .await?;
801
802 let any_edge_id = if let Some(edge_id) = sketch.mirror {
803 edge_id
804 } else if let Some(id) = edge_id {
805 id
806 } else {
807 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
810 return Err(KclError::new_type(KclErrorDetails::new(
811 "Expected a non-empty sketch".to_owned(),
812 vec![args.source_range],
813 )));
814 };
815 any_edge_id
816 };
817
818 let extrusion_info_edge_id = get_extrusion_info_edge_id(sketch, any_edge_id, clone_id_map);
821 let mut sketch = sketch.clone();
822 match body_type {
823 BodyType::Solid => {
824 sketch.is_closed = ProfileClosed::Explicitly;
825 }
826 BodyType::Surface => {}
827 _other => {
828 }
831 }
832
833 match (extrude_method, being_extruded) {
834 (ExtrudeMethod::Merge, BeingExtruded::Face { .. }) => {
835 if let SketchSurface::Face(ref face) = sketch.on {
838 sketch.id = face.parent_solid.sketch_or_solid_id();
841 }
842 }
843 (ExtrudeMethod::New, BeingExtruded::Face { .. }) => {
844 sketch.id = extrude_cmd_id.into();
847 }
848 (ExtrudeMethod::New, BeingExtruded::Sketch) => {
849 }
852 (ExtrudeMethod::Merge, BeingExtruded::Sketch) => {
853 if let SketchSurface::Face(ref face) = sketch.on {
854 sketch.id = face.parent_solid.sketch_or_solid_id();
857 }
858 }
859 (other, _) => {
860 return Err(KclError::new_internal(KclErrorDetails::new(
862 format!("Zoo does not yet support creating bodies via {other:?}"),
863 vec![args.source_range],
864 )));
865 }
866 }
867
868 let sketch_id = if let Some(cloned_from) = sketch.clone
870 && clone_id_map.is_some()
871 {
872 cloned_from
873 } else {
874 sketch.id
875 };
876
877 let solid3d_info = exec_state
878 .send_modeling_cmd(
879 ModelingCmdMeta::from_args(exec_state, args),
880 ModelingCmd::from(
881 mcmd::Solid3dGetExtrusionFaceInfo::builder()
882 .edge_id(extrusion_info_edge_id)
883 .object_id(sketch_id)
884 .build(),
885 ),
886 )
887 .await?;
888
889 let face_infos = if let OkWebSocketResponseData::Modeling {
890 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
891 } = solid3d_info
892 {
893 data.faces
894 } else {
895 vec![]
896 };
897
898 if !args.ctx.settings.skip_artifact_graph {
900 if !sectional {
903 exec_state
904 .batch_modeling_cmd(
905 ModelingCmdMeta::from_args(exec_state, args),
906 ModelingCmd::from(
907 mcmd::Solid3dGetAdjacencyInfo::builder()
908 .object_id(sketch_id)
909 .edge_id(extrusion_info_edge_id)
910 .build(),
911 ),
912 )
913 .await?;
914 }
915 }
916
917 let Faces {
918 sides: mut face_id_map,
919 start_cap_id,
920 end_cap_id,
921 } = analyze_faces(exec_state, args, face_infos).await;
922
923 if sketch.clone.is_some()
925 && let Some(clone_id_map) = clone_id_map
926 {
927 face_id_map = face_id_map
928 .into_iter()
929 .filter_map(|(k, v)| {
930 let fe_key = clone_id_map.get(&k)?;
931 let fe_value = clone_id_map.get(&(v?)).copied();
932 Some((*fe_key, fe_value))
933 })
934 .collect::<HashMap<Uuid, Option<Uuid>>>();
935 }
936
937 let no_engine_commands = args.ctx.no_engine_commands().await;
939 let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
940 let outer_surfaces = sketch.paths.iter().flat_map(|path| {
941 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
942 surface_of(path, *actual_face_id)
943 } else if no_engine_commands {
944 crate::log::logln!(
945 "No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
946 path.get_base().geo_meta.id
947 );
948 fake_extrude_surface(exec_state, path)
950 } else if sketch.clone.is_some()
951 && let Some(clone_map) = clone_id_map
952 {
953 let new_path = clone_map.get(&(path.get_base().geo_meta.id));
954
955 if let Some(new_path) = new_path {
956 match face_id_map.get(new_path) {
957 Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
958 _ => {
959 let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
960 if let Some(value) = value {
961 if value == new_path { Some(key) } else { None }
962 } else {
963 None
964 }
965 });
966 match actual_face_id {
967 Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
968 None => {
969 crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
970 None
971 }
972 }
973 }
974 }
975 } else {
976 None
977 }
978 } else {
979 crate::log::logln!(
980 "No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
981 path.get_base().geo_meta.id
982 );
983 None
984 }
985 });
986
987 new_value.extend(outer_surfaces);
988 let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
989 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
990 surface_of(path, *actual_face_id)
991 } else if no_engine_commands {
992 fake_extrude_surface(exec_state, path)
994 } else {
995 None
996 }
997 });
998 new_value.extend(inner_surfaces);
999
1000 if let Some(tag_start) = named_cap_tags.start {
1002 let Some(start_cap_id) = start_cap_id else {
1003 return Err(KclError::new_type(KclErrorDetails::new(
1004 format!(
1005 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
1006 tag_start.name, sketch.id
1007 ),
1008 vec![args.source_range],
1009 )));
1010 };
1011
1012 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1013 face_id: start_cap_id,
1014 tag: Some(tag_start.clone()),
1015 geo_meta: GeoMeta {
1016 id: start_cap_id,
1017 metadata: args.source_range.into(),
1018 },
1019 }));
1020 }
1021 if let Some(tag_end) = named_cap_tags.end {
1022 let Some(end_cap_id) = end_cap_id else {
1023 return Err(KclError::new_type(KclErrorDetails::new(
1024 format!(
1025 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
1026 tag_end.name, sketch.id
1027 ),
1028 vec![args.source_range],
1029 )));
1030 };
1031
1032 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1033 face_id: end_cap_id,
1034 tag: Some(tag_end.clone()),
1035 geo_meta: GeoMeta {
1036 id: end_cap_id,
1037 metadata: args.source_range.into(),
1038 },
1039 }));
1040 }
1041
1042 let meta = sketch.meta.clone();
1043 let units = sketch.units;
1044 let id = sketch.id;
1045 let creator = match being_extruded {
1046 BeingExtruded::Sketch => SolidCreator::Sketch(sketch),
1047 BeingExtruded::Face { face_id, solid_id } => SolidCreator::Face(CreatorFace {
1048 face_id,
1049 solid_id,
1050 sketch,
1051 }),
1052 };
1053
1054 Ok(Solid {
1055 id,
1056 value_id: extrude_cmd_id.into(),
1057 artifact_id: extrude_cmd_id,
1058 value: new_value,
1059 meta,
1060 units,
1061 sectional,
1062 creator,
1063 start_cap_id,
1064 end_cap_id,
1065 edge_cuts: vec![],
1066 pending_edge_cut_ids: vec![],
1067 })
1068}
1069
1070#[derive(Debug, Default)]
1071struct Faces {
1072 sides: HashMap<Uuid, Option<Uuid>>,
1074 end_cap_id: Option<Uuid>,
1076 start_cap_id: Option<Uuid>,
1078}
1079
1080async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
1081 let mut faces = Faces {
1082 sides: HashMap::with_capacity(face_infos.len()),
1083 ..Default::default()
1084 };
1085 if args.ctx.no_engine_commands().await {
1086 faces.start_cap_id = Some(exec_state.next_uuid());
1088 faces.end_cap_id = Some(exec_state.next_uuid());
1089 }
1090 for face_info in face_infos {
1091 match face_info.cap {
1092 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
1093 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
1094 ExtrusionFaceCapType::Both => {
1095 faces.end_cap_id = face_info.face_id;
1096 faces.start_cap_id = face_info.face_id;
1097 }
1098 ExtrusionFaceCapType::None => {
1099 if let Some(curve_id) = face_info.curve_id {
1100 faces.sides.insert(curve_id, face_info.face_id);
1101 }
1102 }
1103 other => {
1104 exec_state.warn(
1105 crate::CompilationIssue {
1106 source_range: args.source_range,
1107 message: format!("unknown extrusion face type {other:?}"),
1108 suggestion: None,
1109 severity: crate::errors::Severity::Warning,
1110 tag: crate::errors::Tag::Unnecessary,
1111 },
1112 annotations::WARN_NOT_YET_SUPPORTED,
1113 );
1114 }
1115 }
1116 }
1117 faces
1118}
1119fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
1120 match path {
1121 Path::Arc { .. }
1122 | Path::TangentialArc { .. }
1123 | Path::TangentialArcTo { .. }
1124 | Path::Ellipse { .. }
1126 | Path::Conic {.. }
1127 | Path::Circle { .. }
1128 | Path::CircleThreePoint { .. } => {
1129 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1130 face_id: actual_face_id,
1131 tag: path.get_base().tag.clone(),
1132 geo_meta: GeoMeta {
1133 id: path.get_base().geo_meta.id,
1134 metadata: path.get_base().geo_meta.metadata,
1135 },
1136 });
1137 Some(extrude_surface)
1138 }
1139 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
1140 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1141 face_id: actual_face_id,
1142 tag: path.get_base().tag.clone(),
1143 geo_meta: GeoMeta {
1144 id: path.get_base().geo_meta.id,
1145 metadata: path.get_base().geo_meta.metadata,
1146 },
1147 });
1148 Some(extrude_surface)
1149 }
1150 Path::ArcThreePoint { .. } => {
1151 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1152 face_id: actual_face_id,
1153 tag: path.get_base().tag.clone(),
1154 geo_meta: GeoMeta {
1155 id: path.get_base().geo_meta.id,
1156 metadata: path.get_base().geo_meta.metadata,
1157 },
1158 });
1159 Some(extrude_surface)
1160 }
1161 }
1162}
1163
1164fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
1165 match path {
1166 Path::Arc { .. }
1167 | Path::TangentialArc { .. }
1168 | Path::TangentialArcTo { .. }
1169 | Path::Ellipse { .. }
1171 | Path::Conic {.. }
1172 | Path::Circle { .. }
1173 | Path::CircleThreePoint { .. } => {
1174 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1175 face_id: actual_face_id,
1176 tag: path.get_base().tag.clone(),
1177 geo_meta: GeoMeta {
1178 id: clone_path_id,
1179 metadata: path.get_base().geo_meta.metadata,
1180 },
1181 });
1182 Some(extrude_surface)
1183 }
1184 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
1185 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1186 face_id: actual_face_id,
1187 tag: path.get_base().tag.clone(),
1188 geo_meta: GeoMeta {
1189 id: clone_path_id,
1190 metadata: path.get_base().geo_meta.metadata,
1191 },
1192 });
1193 Some(extrude_surface)
1194 }
1195 Path::ArcThreePoint { .. } => {
1196 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1197 face_id: actual_face_id,
1198 tag: path.get_base().tag.clone(),
1199 geo_meta: GeoMeta {
1200 id: clone_path_id,
1201 metadata: path.get_base().geo_meta.metadata,
1202 },
1203 });
1204 Some(extrude_surface)
1205 }
1206 }
1207}
1208
1209fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
1211 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1212 face_id: exec_state.next_uuid(),
1214 tag: path.get_base().tag.clone(),
1215 geo_meta: GeoMeta {
1216 id: path.get_base().geo_meta.id,
1217 metadata: path.get_base().geo_meta.metadata,
1218 },
1219 });
1220 Some(extrude_surface)
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225 use kittycad_modeling_cmds::units::UnitLength;
1226
1227 use super::*;
1228 use crate::execution::AbstractSegment;
1229 use crate::execution::Plane;
1230 use crate::execution::SegmentRepr;
1231 use crate::execution::types::NumericType;
1232 use crate::front::Expr;
1233 use crate::front::Number;
1234 use crate::front::ObjectId;
1235 use crate::front::Point2d;
1236 use crate::front::PointCtor;
1237 use crate::std::sketch::PlaneData;
1238
1239 fn point_expr(x: f64, y: f64) -> Point2d<Expr> {
1240 Point2d {
1241 x: Expr::Var(Number::from((x, UnitLength::Millimeters))),
1242 y: Expr::Var(Number::from((y, UnitLength::Millimeters))),
1243 }
1244 }
1245
1246 fn segment_value(exec_state: &mut ExecState) -> KclValue {
1247 let plane = Plane::from_plane_data_skipping_engine(PlaneData::XY, exec_state).unwrap();
1248 let segment = Segment {
1249 id: exec_state.next_uuid(),
1250 object_id: ObjectId(1),
1251 kind: SegmentKind::Point {
1252 position: [TyF64::new(0.0, NumericType::mm()), TyF64::new(0.0, NumericType::mm())],
1253 ctor: Box::new(PointCtor {
1254 position: point_expr(0.0, 0.0),
1255 }),
1256 freedom: None,
1257 },
1258 surface: SketchSurface::Plane(Box::new(plane)),
1259 sketch_id: exec_state.next_uuid(),
1260 sketch: None,
1261 tag: None,
1262 node_path: None,
1263 meta: vec![],
1264 };
1265 KclValue::Segment {
1266 value: Box::new(AbstractSegment {
1267 repr: SegmentRepr::Solved {
1268 segment: Box::new(segment),
1269 },
1270 meta: vec![],
1271 }),
1272 }
1273 }
1274
1275 #[tokio::test(flavor = "multi_thread")]
1276 async fn segment_extrude_rejects_cap_tags() {
1277 let ctx = ExecutorContext::new_mock(None).await;
1278 let mut exec_state = ExecState::new(&ctx);
1279 let err = coerce_extrude_targets(
1280 vec![segment_value(&mut exec_state)],
1281 BodyType::Surface,
1282 Some(&TagDeclarator::new("cap_start")),
1283 None,
1284 &mut exec_state,
1285 &ctx,
1286 crate::SourceRange::default(),
1287 )
1288 .await
1289 .unwrap_err();
1290
1291 assert!(
1292 err.message()
1293 .contains("`tagStart` and `tagEnd` are not supported when extruding sketch segments"),
1294 "{err:?}"
1295 );
1296 ctx.close().await;
1297 }
1298}