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::CreatorEdge;
35use crate::execution::CreatorFace;
36use crate::execution::ExecState;
37use crate::execution::ExecutorContext;
38use crate::execution::Extrudable;
39use crate::execution::ExtrudePlane;
40use crate::execution::ExtrudeSurface;
41use crate::execution::GeoMeta;
42use crate::execution::KclValue;
43use crate::execution::ModelingCmdMeta;
44use crate::execution::Path;
45use crate::execution::ProfileClosed;
46use crate::execution::Segment;
47use crate::execution::SegmentKind;
48use crate::execution::Sketch;
49use crate::execution::SketchSurface;
50use crate::execution::Solid;
51use crate::execution::SolidCreator;
52use crate::execution::annotations;
53use crate::execution::types::ArrayLen;
54use crate::execution::types::PrimitiveType;
55use crate::execution::types::RuntimeType;
56use crate::parsing::ast::types::TagDeclarator;
57use crate::parsing::ast::types::TagNode;
58use crate::std::Args;
59use crate::std::axis_or_reference::Point3dAxis3dOrGeometryReference;
60use crate::std::axis_or_reference::Point3dOrEdgeReference;
61use crate::std::edge::{self};
62use crate::std::solver::create_segments_in_engine;
63
64pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
66 let sketch_values: Vec<KclValue> = args.get_unlabeled_kw_arg(
67 "sketches",
68 &RuntimeType::Array(
69 Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
70 ArrayLen::Minimum(1),
71 ),
72 exec_state,
73 )?;
74
75 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
76 let to_raw = args.get_kw_arg_opt(
77 "to",
78 &RuntimeType::Union(vec![
79 RuntimeType::point3d(),
80 RuntimeType::Primitive(PrimitiveType::Axis3d),
81 RuntimeType::Primitive(PrimitiveType::Edge),
82 RuntimeType::plane(),
83 RuntimeType::Primitive(PrimitiveType::Face),
84 RuntimeType::sketch(),
85 RuntimeType::Primitive(PrimitiveType::Solid),
86 RuntimeType::tagged_edge(),
87 RuntimeType::tagged_face(),
88 RuntimeType::Primitive(PrimitiveType::Any),
89 ]),
90 exec_state,
91 )?;
92 let to = match to_raw {
93 None => None,
94 Some(v) => {
95 let inner = if let KclValue::Object { value: ref obj, .. } = v {
96 if edge::is_edge_specifier_object(&v) {
97 Point3dAxis3dOrGeometryReference::EdgeToReference(edge::parse_edge_specifier_object(obj, &args)?)
98 } else {
99 Point3dAxis3dOrGeometryReference::from_kcl_val(&v).ok_or_else(|| {
100 KclError::new_type(KclErrorDetails::new(
101 "Invalid value for `to`".to_owned(),
102 vec![args.source_range],
103 ))
104 })?
105 }
106 } else {
107 Point3dAxis3dOrGeometryReference::from_kcl_val(&v).ok_or_else(|| {
108 KclError::new_type(KclErrorDetails::new(
109 "Invalid value for `to`".to_owned(),
110 vec![args.source_range],
111 ))
112 })?
113 };
114 Some(inner)
115 }
116 };
117 let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
118 let bidirectional_length: Option<TyF64> =
119 args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
120 let direction_raw = args.get_kw_arg_opt("direction", &RuntimeType::any(), exec_state)?;
121 let direction = match direction_raw {
122 None => None,
123 Some(v) => {
124 let inner = if edge::is_edge_specifier_object(&v) {
125 Point3dOrEdgeReference::EdgeSpecifier(edge::parse_edge_specifier_value(&v, &args)?)
126 } else {
127 Point3dOrEdgeReference::from_kcl_val(&v).ok_or_else(|| {
128 KclError::new_type(KclErrorDetails::new(
129 "Invalid value for `direction`".to_owned(),
130 vec![args.source_range],
131 ))
132 })?
133 };
134 Some(inner)
135 }
136 };
137 let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
138 let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
139 let draft_angle: Option<TyF64> = args.get_kw_arg_opt("draftAngle", &RuntimeType::degrees(), exec_state)?;
140 let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
141 let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
142 let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
143 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
144 let method: Option<String> = args.get_kw_arg_opt("method", &RuntimeType::string(), exec_state)?;
145 let hide_seams: Option<bool> = args.get_kw_arg_opt("hideSeams", &RuntimeType::bool(), exec_state)?;
146 let body_type: Option<BodyType> = args.get_kw_arg_opt("bodyType", &RuntimeType::string(), exec_state)?;
147 let sketches = coerce_extrude_targets(
148 sketch_values,
149 body_type.unwrap_or_default(),
150 tag_start.as_ref(),
151 tag_end.as_ref(),
152 exec_state,
153 &args.ctx,
154 args.source_range,
155 )
156 .await?;
157
158 let result = inner_extrude(
159 sketches,
160 length,
161 to,
162 symmetric,
163 direction,
164 bidirectional_length,
165 tag_start,
166 tag_end,
167 draft_angle,
168 twist_angle,
169 twist_angle_step,
170 twist_center,
171 tolerance,
172 method,
173 hide_seams,
174 body_type,
175 exec_state,
176 args,
177 )
178 .await?;
179
180 Ok(result.into())
181}
182
183pub async fn coerce_extrude_targets(
184 sketch_values: Vec<KclValue>,
185 body_type: BodyType,
186 tag_start: Option<&TagNode>,
187 tag_end: Option<&TagNode>,
188 exec_state: &mut ExecState,
189 ctx: &ExecutorContext,
190 source_range: crate::SourceRange,
191) -> Result<Vec<Extrudable>, KclError> {
192 let mut extrudables = Vec::new();
193 let mut segments = Vec::new();
194
195 for value in sketch_values {
196 if let Some(segment) = value.clone().into_segment() {
197 segments.push(segment);
198 continue;
199 }
200
201 if edge::is_edge_specifier_object(&value) {
202 extrudables.push(Extrudable::EdgeSpecifier(edge::parse_edge_specifier_value_at(
203 &value,
204 source_range,
205 )?));
206 continue;
207 }
208
209 let Some(extrudable) = Extrudable::from_kcl_val(&value) else {
210 return Err(KclError::new_type(KclErrorDetails::new(
211 "Expected sketches, faces, tagged faces, or solved sketch segments for extrusion.".to_owned(),
212 vec![source_range],
213 )));
214 };
215 extrudables.push(extrudable);
216 }
217
218 if !segments.is_empty() && !extrudables.is_empty() {
219 return Err(KclError::new_semantic(KclErrorDetails::new(
220 "Cannot extrude sketch segments together with sketches or faces in the same call. Use separate `extrude()` calls.".to_owned(),
221 vec![source_range],
222 )));
223 }
224
225 if !segments.is_empty() {
226 if !matches!(body_type, BodyType::Surface) {
227 let kind_of_extrude = match body_type {
228 BodyType::Solid => "solid extrude",
229 BodyType::Surface => "surface extrude",
230 _ => "non-surface extrude",
231 };
232 return Err(KclError::new_semantic(KclErrorDetails::new(
233 format!(
234 "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."
235 ),
236 vec![source_range],
237 )));
238 }
239
240 if tag_start.is_some() || tag_end.is_some() {
241 return Err(KclError::new_semantic(KclErrorDetails::new(
242 "`tagStart` and `tagEnd` are not supported when extruding sketch segments. Segment surface extrudes do not create start or end caps."
243 .to_owned(),
244 vec![source_range],
245 )));
246 }
247
248 let synthetic_sketch = build_segment_surface_sketch(segments, exec_state, ctx, source_range).await?;
249 return Ok(vec![Extrudable::from(synthetic_sketch)]);
250 }
251
252 let has_edge = extrudables.iter().any(|e| {
257 matches!(
258 e,
259 Extrudable::Edge(_) | Extrudable::EdgeTag(_) | Extrudable::EdgeSpecifier(_)
260 )
261 });
262 if has_edge {
263 let has_non_edge = extrudables.iter().any(|e| {
264 !matches!(
265 e,
266 Extrudable::Edge(_) | Extrudable::EdgeTag(_) | Extrudable::EdgeSpecifier(_)
267 )
268 });
269 if has_non_edge {
270 return Err(KclError::new_semantic(KclErrorDetails::new(
271 "Cannot extrude edges together with sketches or faces in the same call. Use separate `extrude()` calls.".to_owned(),
272 vec![source_range],
273 )));
274 }
275
276 if !matches!(body_type, BodyType::Surface) {
277 let kind_of_extrude = match body_type {
278 BodyType::Solid => "solid extrude",
279 BodyType::Surface => "surface extrude",
280 _ => "non-surface extrude",
281 };
282 return Err(KclError::new_semantic(KclErrorDetails::new(
283 format!(
284 "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."
285 ),
286 vec![source_range],
287 )));
288 }
289
290 if tag_start.is_some() || tag_end.is_some() {
291 return Err(KclError::new_semantic(KclErrorDetails::new(
292 "`tagStart` and `tagEnd` are not supported when extruding edges. Edge surface extrudes do not create start or end caps."
293 .to_owned(),
294 vec![source_range],
295 )));
296 }
297 }
298
299 Ok(extrudables)
300}
301
302pub(crate) async fn build_segment_surface_sketch(
303 mut segments: Vec<Segment>,
304 exec_state: &mut ExecState,
305 ctx: &ExecutorContext,
306 source_range: crate::SourceRange,
307) -> Result<Sketch, KclError> {
308 let Some(first_segment) = segments.first() else {
309 return Err(KclError::new_semantic(KclErrorDetails::new(
310 "Expected at least one sketch segment.".to_owned(),
311 vec![source_range],
312 )));
313 };
314
315 let sketch_id = first_segment.sketch_id;
316 let sketch_surface = first_segment.surface.clone();
317 for segment in &segments {
318 if segment.sketch_id != sketch_id {
319 return Err(KclError::new_semantic(KclErrorDetails::new(
320 "All sketch segments passed to this operation must come from the same sketch.".to_owned(),
321 vec![source_range],
322 )));
323 }
324
325 if segment.surface != sketch_surface {
326 return Err(KclError::new_semantic(KclErrorDetails::new(
327 "All sketch segments passed to this operation must lie on the same sketch surface.".to_owned(),
328 vec![source_range],
329 )));
330 }
331
332 if matches!(segment.kind, SegmentKind::Point { .. }) {
333 return Err(KclError::new_semantic(KclErrorDetails::new(
334 "Point segments cannot be used here. Select line, arc, or circle segments instead.".to_owned(),
335 vec![source_range],
336 )));
337 }
338
339 if segment.is_construction() {
340 return Err(KclError::new_semantic(KclErrorDetails::new(
341 "Construction segments cannot be used here. Select non-construction sketch segments instead."
342 .to_owned(),
343 vec![source_range],
344 )));
345 }
346 }
347
348 let synthetic_sketch_id = exec_state.next_uuid();
349 let segment_tags = IndexMap::from_iter(segments.iter().filter_map(|segment| {
350 segment
351 .tag
352 .as_ref()
353 .map(|tag| (segment.object_id, TagDeclarator::new(&tag.value)))
354 }));
355
356 for segment in &mut segments {
357 segment.id = exec_state.next_uuid();
358 segment.sketch_id = synthetic_sketch_id;
359 segment.sketch = None;
360 }
361
362 create_segments_in_engine(
363 &sketch_surface,
364 synthetic_sketch_id,
365 &mut segments,
366 &segment_tags,
367 ctx,
368 exec_state,
369 source_range,
370 )
371 .await?
372 .ok_or_else(|| {
373 KclError::new_semantic(KclErrorDetails::new(
374 "Expected at least one usable sketch segment.".to_owned(),
375 vec![source_range],
376 ))
377 })
378}
379
380#[allow(clippy::too_many_arguments)]
381async fn inner_extrude(
382 extrudables: Vec<Extrudable>,
383 length: Option<TyF64>,
384 to: Option<Point3dAxis3dOrGeometryReference>,
385 symmetric: Option<bool>,
386 direction: Option<Point3dOrEdgeReference>,
387 bidirectional_length: Option<TyF64>,
388 tag_start: Option<TagNode>,
389 tag_end: Option<TagNode>,
390 draft_angle: Option<TyF64>,
391 twist_angle: Option<TyF64>,
392 twist_angle_step: Option<TyF64>,
393 twist_center: Option<[TyF64; 2]>,
394 tolerance: Option<TyF64>,
395 method: Option<String>,
396 hide_seams: Option<bool>,
397 body_type: Option<BodyType>,
398 exec_state: &mut ExecState,
399 args: Args,
400) -> Result<Vec<Solid>, KclError> {
401 let body_type = body_type.unwrap_or_default();
402
403 if matches!(body_type, BodyType::Solid) && extrudables.iter().any(|sk| matches!(sk.is_closed(), ProfileClosed::No))
404 {
405 return Err(KclError::new_semantic(KclErrorDetails::new(
406 "Cannot solid extrude an open profile. Either close the profile, or use a surface extrude.".to_owned(),
407 vec![args.source_range],
408 )));
409 }
410
411 if draft_angle.is_some() && twist_angle.is_some() {
412 return Err(KclError::new_semantic(KclErrorDetails::new(
413 "Zoo currently does not support adding both draft angle and twist angle to an extrude simultaneously"
414 .to_owned(),
415 vec![args.source_range],
416 )));
417 }
418
419 if direction.is_some() && twist_angle.is_some() {
420 return Err(KclError::new_semantic(KclErrorDetails::new(
421 "Zoo currently does not support adding both direction and twist angle to an extrude simultaneously"
422 .to_owned(),
423 vec![args.source_range],
424 )));
425 }
426
427 let mut solids = Vec::new();
429 let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
430
431 let extrude_method = match method.as_deref() {
432 Some("new" | "NEW") => ExtrudeMethod::New,
433 Some("merge" | "MERGE") => ExtrudeMethod::Merge,
434 None => ExtrudeMethod::default(),
435 Some(other) => {
436 return Err(KclError::new_semantic(KclErrorDetails::new(
437 format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
438 vec![args.source_range],
439 )));
440 }
441 };
442
443 if matches!(method.as_deref(), Some("merge" | "MERGE")) {
444 let parentless_sketches = extrudables
445 .iter()
446 .filter(|extrudable| {
447 matches!(extrudable, Extrudable::Sketch(sketch) if matches!(sketch.on, SketchSurface::Plane(_)))
448 })
449 .count();
450 if parentless_sketches > 0 {
451 let message = if parentless_sketches == 1 {
452 "This plane-based sketch has no parent body for `method = MERGE`, so it will create a separate body. Sketch on an existing face or use `union` if one body is intended."
453 .to_owned()
454 } else {
455 format!(
456 "{parentless_sketches} plane-based sketches have no parent body for `method = MERGE`, so each will create a separate body. Sketch on an existing face or use `union` if one body is intended."
457 )
458 };
459 exec_state.warn(
460 crate::CompilationIssue {
461 source_range: args
462 .labeled
463 .get("method")
464 .map_or(args.source_range, |arg| arg.source_range),
465 message,
466 suggestion: None,
467 severity: crate::errors::Severity::Warning,
468 tag: crate::errors::Tag::Unnecessary,
469 },
470 annotations::WARN_PARENTLESS_MERGE,
471 );
472 }
473 }
474
475 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
476 return Err(KclError::new_semantic(KclErrorDetails::new(
477 "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
478 .to_owned(),
479 vec![args.source_range],
480 )));
481 }
482
483 if (length.is_some() || twist_angle.is_some()) && to.is_some() {
484 return Err(KclError::new_semantic(KclErrorDetails::new(
485 "You cannot give `length` or `twist` params with the `to` param, you have to choose one or the other"
486 .to_owned(),
487 vec![args.source_range],
488 )));
489 }
490
491 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
492
493 let opposite = match (symmetric, bidirection) {
494 (Some(true), _) => Opposite::Symmetric,
495 (None, None) => Opposite::None,
496 (Some(false), None) => Opposite::None,
497 (None, Some(length)) => Opposite::Other(length),
498 (Some(false), Some(length)) => Opposite::Other(length),
499 };
500
501 for extrudable in &extrudables {
502 let is_edge = match extrudable {
503 Extrudable::Sketch(..) => false,
504 Extrudable::FaceTag(_) => false,
505 Extrudable::Face(_) => false,
506 Extrudable::EdgeTag(_) => true,
507 Extrudable::Edge(_) => true,
508 Extrudable::EdgeSpecifier(_) => true,
509 };
510 let extrude_cmd_id = exec_state.next_uuid();
511 let (sketch_or_face_id, target_reference) = match extrudable {
512 Extrudable::EdgeSpecifier(spec) => (
513 None,
514 Some(edge::resolve_edge_specifier_with_face_tags(spec, None, exec_state, &args).await?),
515 ),
516 _ => (Some(extrudable.id_to_extrude(exec_state, &args, false).await?), None),
517 };
518 if to.is_some() && sketch_or_face_id.is_none() {
519 return Err(KclError::new_semantic(KclErrorDetails::new(
520 "Edge specifiers cannot be extruded to a reference".to_owned(),
521 vec![args.source_range],
522 )));
523 }
524 let concrete_target = || {
525 sketch_or_face_id.ok_or_else(|| {
526 KclError::new_semantic(KclErrorDetails::new(
527 "This extrusion requires a concrete target UUID".to_owned(),
528 vec![args.source_range],
529 ))
530 })
531 };
532 if is_edge
533 && let Some(edge_id) = sketch_or_face_id
534 && let Some(target_source_range) = args.unlabeled_kw_arg_unconverted().map(|arg| arg.source_range)
535 && let Some(pending) = exec_state.pending_edge_refactor_meta(edge_id, target_source_range)
536 && let Ok(meta) =
537 edge::get_refactor_meta_for_edge(exec_state, edge_id, &args, pending.source_range, pending.stdlib_fn)
538 .await
539 {
540 exec_state.record_edge_refactor_meta(meta);
541 }
542 let cmd = match (
543 &twist_angle,
544 &twist_angle_step,
545 &twist_center,
546 length.clone(),
547 &to,
548 &direction,
549 ) {
550 (Some(angle), angle_step, center, Some(length), None, None) => {
551 let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
552 let total_rotation_angle = Angle::from_degrees(angle.to_degrees(exec_state, args.source_range));
553 let angle_step_size = Angle::from_degrees(
554 angle_step
555 .clone()
556 .map(|a| a.to_degrees(exec_state, args.source_range))
557 .unwrap_or(15.0),
558 );
559 ModelingCmd::from(
560 mcmd::TwistExtrude::builder()
561 .target(
562 sketch_or_face_id
563 .ok_or_else(|| {
564 KclError::new_semantic(KclErrorDetails::new(
565 "Edge specifiers cannot be used with twist extrusion".to_owned(),
566 vec![args.source_range],
567 ))
568 })?
569 .into(),
570 )
571 .distance(LengthUnit(length.to_mm()))
572 .center_2d(center)
573 .total_rotation_angle(total_rotation_angle)
574 .angle_step_size(angle_step_size)
575 .tolerance(tolerance)
576 .body_type(body_type)
577 .build(),
578 )
579 }
580 (None, None, None, Some(length), None, None) => ModelingCmd::from(
581 mcmd::Extrude::builder()
582 .maybe_target(sketch_or_face_id.map(Into::into))
583 .maybe_target_reference(target_reference.clone())
584 .distance(LengthUnit(length.to_mm()))
585 .opposite(opposite.clone())
586 .maybe_draft_angle(
587 draft_angle
588 .clone()
589 .map(|a| Angle::from_degrees(a.to_degrees(exec_state, args.source_range))),
590 )
591 .extrude_method(extrude_method)
592 .body_type(body_type)
593 .maybe_merge_coplanar_faces(hide_seams)
594 .build(),
595 ),
596 (None, None, None, Some(length), None, Some(dir)) => {
597 let (direction3d, direction_edge_id) = match dir {
598 Point3dOrEdgeReference::Point(p) => (
599 Some(DirectionType::Axis {
600 direction: KPoint3d {
601 x: p[0].n,
602 y: p[1].n,
603 z: p[2].n,
604 },
605 }),
606 None,
607 ),
608 Point3dOrEdgeReference::Edge(edge) => {
609 let edge_id = match edge {
610 crate::std::fillet::EdgeReference::Uuid(uuid) => *uuid,
611 crate::std::fillet::EdgeReference::Tag(tag) => match tag.get_cur_info() {
612 Some(info) => info.id,
613 None => {
614 return Err(KclError::new_semantic(KclErrorDetails::new(
615 "Failed to get current info for tag".to_string(),
616 vec![args.source_range],
617 )));
618 }
619 },
620 };
621 (Some(DirectionType::Edge { id: edge_id }), Some(edge_id))
622 }
623 Point3dOrEdgeReference::EdgeSpecifier(_) => (None, None),
624 };
625 if let Some(edge_id) = direction_edge_id
626 && let Some(direction_source_range) = args.labeled.get("direction").map(|arg| arg.source_range)
627 && let Some(pending) = exec_state.pending_edge_refactor_meta(edge_id, direction_source_range)
628 && let Ok(meta) = edge::get_refactor_meta_for_edge(
629 exec_state,
630 edge_id,
631 &args,
632 pending.source_range,
633 pending.stdlib_fn,
634 )
635 .await
636 {
637 exec_state.record_edge_refactor_meta(meta);
638 }
639 let direction_reference = match dir {
640 Point3dOrEdgeReference::EdgeSpecifier(spec) => {
641 Some(edge::resolve_edge_specifier_with_face_tags(spec, None, exec_state, &args).await?)
642 }
643 _ => None,
644 };
645 ModelingCmd::from(
646 mcmd::Extrude::builder()
647 .maybe_target(sketch_or_face_id.map(Into::into))
648 .maybe_target_reference(target_reference.clone())
649 .distance(LengthUnit(length.to_mm()))
650 .opposite(opposite.clone())
651 .maybe_draft_angle(
652 draft_angle
653 .clone()
654 .map(|a| Angle::from_degrees(a.to_degrees(exec_state, args.source_range))),
655 )
656 .extrude_method(extrude_method)
657 .body_type(body_type)
658 .maybe_merge_coplanar_faces(hide_seams)
659 .maybe_direction(direction3d)
660 .maybe_direction_reference(direction_reference)
661 .build(),
662 )
663 }
664 (None, None, None, None, Some(to), None) => match to {
665 Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(
666 mcmd::ExtrudeToReference::builder()
667 .target(concrete_target()?.into())
668 .reference(ExtrudeReference::Point {
669 point: KPoint3d {
670 x: LengthUnit(point[0].to_mm()),
671 y: LengthUnit(point[1].to_mm()),
672 z: LengthUnit(point[2].to_mm()),
673 },
674 })
675 .extrude_method(extrude_method)
676 .body_type(body_type)
677 .build(),
678 ),
679 Point3dAxis3dOrGeometryReference::Axis { direction, origin } => ModelingCmd::from(
680 mcmd::ExtrudeToReference::builder()
681 .target(concrete_target()?.into())
682 .reference(ExtrudeReference::Axis {
683 axis: KPoint3d {
684 x: direction[0].to_mm(),
685 y: direction[1].to_mm(),
686 z: direction[2].to_mm(),
687 },
688 point: KPoint3d {
689 x: LengthUnit(origin[0].to_mm()),
690 y: LengthUnit(origin[1].to_mm()),
691 z: LengthUnit(origin[2].to_mm()),
692 },
693 })
694 .extrude_method(extrude_method)
695 .body_type(body_type)
696 .build(),
697 ),
698 Point3dAxis3dOrGeometryReference::Plane(plane) => {
699 let plane_id = if plane.is_uninitialized() {
700 if plane.info.origin.units.is_none() {
701 return Err(KclError::new_semantic(KclErrorDetails::new(
702 "Origin of plane has unknown units".to_string(),
703 vec![args.source_range],
704 )));
705 }
706 let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
707 plane.clone().info.into_plane_data(),
708 exec_state,
709 &args,
710 )
711 .await?;
712 sketch_plane.id
713 } else {
714 plane.id
715 };
716 ModelingCmd::from(
717 mcmd::ExtrudeToReference::builder()
718 .target(concrete_target()?.into())
719 .reference(ExtrudeReference::EntityReference {
720 entity_id: Some(plane_id),
721 entity_reference: None,
722 })
723 .extrude_method(extrude_method)
724 .body_type(body_type)
725 .build(),
726 )
727 }
728 Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
729 let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
730 ModelingCmd::from(
731 mcmd::ExtrudeToReference::builder()
732 .target(concrete_target()?.into())
733 .reference(ExtrudeReference::EntityReference {
734 entity_id: Some(edge_id),
735 entity_reference: None,
736 })
737 .extrude_method(extrude_method)
738 .body_type(body_type)
739 .build(),
740 )
741 }
742 Point3dAxis3dOrGeometryReference::Face(face_tag) => {
743 let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
744 ModelingCmd::from(
745 mcmd::ExtrudeToReference::builder()
746 .target(concrete_target()?.into())
747 .reference(ExtrudeReference::EntityReference {
748 entity_id: Some(face_id),
749 entity_reference: None,
750 })
751 .extrude_method(extrude_method)
752 .body_type(body_type)
753 .build(),
754 )
755 }
756 Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(
757 mcmd::ExtrudeToReference::builder()
758 .target(concrete_target()?.into())
759 .reference(ExtrudeReference::EntityReference {
760 entity_id: Some(sketch_ref.id),
761 entity_reference: None,
762 })
763 .extrude_method(extrude_method)
764 .body_type(body_type)
765 .build(),
766 ),
767 Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(
768 mcmd::ExtrudeToReference::builder()
769 .target(concrete_target()?.into())
770 .reference(ExtrudeReference::EntityReference {
771 entity_id: Some(solid.id),
772 entity_reference: None,
773 })
774 .extrude_method(extrude_method)
775 .body_type(body_type)
776 .build(),
777 ),
778 Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
779 let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
780 let tagged_edge_or_face_id = tagged_edge_or_face.id;
781 ModelingCmd::from(
782 mcmd::ExtrudeToReference::builder()
783 .target(concrete_target()?.into())
784 .reference(ExtrudeReference::EntityReference {
785 entity_id: Some(tagged_edge_or_face_id),
786 entity_reference: None,
787 })
788 .extrude_method(extrude_method)
789 .body_type(body_type)
790 .build(),
791 )
792 }
793 Point3dAxis3dOrGeometryReference::EdgeToReference(spec) => {
794 let inner = edge::resolve_edge_specifier_with_face_tags(spec, None, exec_state, &args).await?;
795 ModelingCmd::from(
796 mcmd::ExtrudeToReference::builder()
797 .target(concrete_target()?.into())
798 .reference(ExtrudeReference::EntityReference {
799 entity_id: None,
800 entity_reference: Some(EntityReference::Edge {
801 inner,
802 topology_fallback: None,
803 }),
804 })
805 .extrude_method(extrude_method)
806 .body_type(body_type)
807 .build(),
808 )
809 }
810 },
811 (Some(_), _, _, None, None, None) => {
812 return Err(KclError::new_semantic(KclErrorDetails::new(
813 "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
814 vec![args.source_range],
815 )));
816 }
817 (_, _, _, None, None, None) => {
818 return Err(KclError::new_semantic(KclErrorDetails::new(
819 "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
820 vec![args.source_range],
821 )));
822 }
823 (_, _, _, Some(_), Some(_), None) => {
824 return Err(KclError::new_semantic(KclErrorDetails::new(
825 "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
826 vec![args.source_range],
827 )));
828 }
829 (_, _, _, _, _, _) => {
830 return Err(KclError::new_semantic(KclErrorDetails::new(
831 "Invalid combination of parameters for extrusion.".to_owned(),
832 vec![args.source_range],
833 )));
834 }
835 };
836
837 let being_extruded = match extrudable {
838 Extrudable::Sketch(..) => BeingExtruded::Sketch,
839 Extrudable::FaceTag(face_tag) => {
840 let face_id = concrete_target()?;
841 let solid_id = match face_tag.geometry() {
842 Some(crate::execution::Geometry::Solid(solid)) => solid.id,
843 Some(crate::execution::Geometry::Sketch(sketch)) => match sketch.on {
844 SketchSurface::Face(face) => face.parent_solid.solid_id,
845 SketchSurface::Plane(_) => sketch.id,
846 },
847 None => face_id,
848 };
849 BeingExtruded::Face { face_id, solid_id }
850 }
851 Extrudable::Face(face) => BeingExtruded::Face {
852 face_id: face.id,
853 solid_id: face.parent_solid.solid_id,
854 },
855 Extrudable::EdgeTag(_) => BeingExtruded::Edge,
856 Extrudable::Edge(_) => BeingExtruded::Edge,
857 Extrudable::EdgeSpecifier(_) => BeingExtruded::Edge,
858 };
859 if let Some(post_extr_sketch) = extrudable.as_sketch() {
860 let cmds = post_extr_sketch.build_sketch_mode_cmds(
861 exec_state,
862 ModelingCmdReq {
863 cmd_id: extrude_cmd_id.into(),
864 cmd,
865 },
866 );
867 exec_state
868 .batch_modeling_cmds(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), &cmds)
869 .await?;
870 solids.push(
871 do_post_extrude(
872 &post_extr_sketch,
873 extrude_cmd_id.into(),
874 false,
875 &NamedCapTags {
876 start: tag_start.as_ref(),
877 end: tag_end.as_ref(),
878 },
879 extrude_method,
880 exec_state,
881 &args,
882 None,
883 None,
884 body_type,
885 being_extruded,
886 )
887 .await?,
888 );
889 } else if is_edge {
890 match extrude_method {
892 ExtrudeMethod::New => {
893 }
895 ExtrudeMethod::Merge => {
896 return Err(KclError::new_semantic(KclErrorDetails::new(
897 "Cannot use method MERGE with surface extrude of an edge".to_owned(),
898 vec![args.source_range],
899 )));
900 }
901 _ => {
902 return Err(KclError::new_internal(KclErrorDetails::new(
903 format!("Unknown extrude method: {extrude_method:?}"),
904 vec![args.source_range],
905 )));
906 }
907 }
908
909 exec_state
911 .batch_modeling_cmd(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), cmd)
912 .await?;
913 let edge_tag = match extrudable {
915 Extrudable::Sketch(_) => None,
916 Extrudable::FaceTag(_) => None,
917 Extrudable::Face(_) => None,
918 Extrudable::EdgeTag(tag) => Some(TagDeclarator::new(&tag.value)),
919 Extrudable::Edge(_) => None,
920 Extrudable::EdgeSpecifier(_) => None,
921 };
922 solids.push(after_surface_creation(extrude_cmd_id.into(), edge_tag, exec_state, &args).await?);
923 } else {
924 return Err(KclError::new_type(KclErrorDetails::new(
925 "Expected a sketch for extrusion".to_owned(),
926 vec![args.source_range],
927 )));
928 }
929 }
930
931 Ok(solids)
932}
933
934#[derive(Debug, Default)]
935pub(crate) struct NamedCapTags<'a> {
936 pub start: Option<&'a TagNode>,
937 pub end: Option<&'a TagNode>,
938}
939
940#[derive(Debug, Clone, Copy)]
941pub enum BeingExtruded {
942 Sketch,
943 Face { face_id: Uuid, solid_id: Uuid },
944 Edge,
945}
946
947fn get_extrusion_info_edge_id(
951 sketch: &Sketch,
952 any_edge_id: Uuid,
953 clone_id_map: Option<&HashMap<Uuid, Uuid>>,
954) -> Option<Uuid> {
955 if sketch.clone.is_none() {
958 return Some(any_edge_id);
959 }
960 let Some(clone_map) = clone_id_map else {
961 return Some(any_edge_id);
962 };
963
964 if clone_map.contains_key(&any_edge_id) {
969 return Some(any_edge_id);
970 }
971
972 if let Some((old_edge_id, _)) = clone_map.iter().find(|(_, new_edge_id)| **new_edge_id == any_edge_id) {
977 return Some(*old_edge_id);
978 }
979
980 None
984}
985
986pub(crate) async fn after_surface_creation(
989 extrude_cmd_id: ArtifactId,
990 edge_tag: Option<crate::parsing::ast::types::Node<TagDeclarator>>,
991 exec_state: &mut ExecState,
992 args: &Args,
993) -> Result<Solid, KclError> {
994 let body_id = extrude_cmd_id.into();
995
996 exec_state
1000 .batch_modeling_cmd(
1001 ModelingCmdMeta::from_args(exec_state, args),
1002 ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(body_id).build()),
1003 )
1004 .await?;
1005
1006 let (face_id, edge_id) = if args.ctx.no_engine_commands().await {
1007 (exec_state.next_uuid(), exec_state.next_uuid())
1008 } else {
1009 let response = exec_state
1011 .send_modeling_cmd(
1012 ModelingCmdMeta::from_args(exec_state, args),
1013 ModelingCmd::from(mcmd::EntityGetAllChildUuids::builder().entity_id(body_id).build()),
1014 )
1015 .await?;
1016 let OkWebSocketResponseData::Modeling {
1017 modeling_response: OkModelingCmdResponse::EntityGetAllChildUuids(ref all_child_ids_resp),
1018 } = response
1019 else {
1020 return Err(KclError::new_engine(KclErrorDetails::new(
1021 format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
1022 vec![args.source_range],
1023 )));
1024 };
1025 let entity_ids = &all_child_ids_resp.entity_ids;
1026 let Some(face_id) = entity_ids.first().copied() else {
1027 return Err(KclError::new_internal(KclErrorDetails::new(
1028 format!("Expected EntityGetAllChildUuids response to have at least 1 ID: {response:?}",),
1029 vec![args.source_range],
1030 )));
1031 };
1032 let Some(edge_id) = entity_ids.get(1).copied() else {
1033 return Err(KclError::new_internal(KclErrorDetails::new(
1034 format!("Expected EntityGetAllChildUuids response to have at least 2 IDs: {response:?}"),
1035 vec![args.source_range],
1036 )));
1037 };
1038 (face_id, edge_id)
1039 };
1040
1041 let extrude_surface = ExtrudeSurface::ExtrudePlane(ExtrudePlane {
1043 face_id,
1044 tag: edge_tag,
1045 geo_meta: GeoMeta {
1046 id: face_id,
1047 metadata: args.source_range.into(),
1048 },
1049 });
1050 let new_value = vec![extrude_surface];
1051
1052 Ok(Solid {
1053 id: body_id,
1054 value_id: body_id,
1055 topology_id: body_id,
1056 pattern_source_artifact_id: None,
1057 best_guess_body_type: Some(BodyType::Surface),
1058 artifact_id: extrude_cmd_id,
1059 value: new_value,
1060 faces: Default::default(),
1061 meta: vec![args.source_range.into()],
1062 units: kcl_api::UnitLength::Millimeters,
1065 sectional: false,
1066 creator: SolidCreator::Edge(CreatorEdge { edge_id, body_id }),
1067 start_cap_id: None,
1068 end_cap_id: None,
1069 edge_cuts: Vec::new(),
1070 pending_edge_cut_ids: Vec::new(),
1071 })
1072}
1073
1074#[allow(clippy::too_many_arguments)]
1075pub(crate) async fn do_post_extrude<'a>(
1076 sketch: &Sketch,
1077 extrude_cmd_id: ArtifactId,
1078 sectional: bool,
1079 named_cap_tags: &'a NamedCapTags<'a>,
1080 extrude_method: ExtrudeMethod,
1081 exec_state: &mut ExecState,
1082 args: &Args,
1083 edge_id: Option<Uuid>,
1084 clone_id_map: Option<&HashMap<Uuid, Uuid>>, body_type: BodyType,
1086 being_extruded: BeingExtruded,
1087) -> Result<Solid, KclError> {
1088 exec_state
1092 .batch_modeling_cmd(
1093 ModelingCmdMeta::from_args(exec_state, args),
1094 ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(sketch.id).build()),
1095 )
1096 .await?;
1097
1098 let any_edge_id = if let Some(edge_id) = sketch.mirror {
1099 edge_id
1100 } else if let Some(id) = edge_id {
1101 id
1102 } else {
1103 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
1106 return Err(KclError::new_type(KclErrorDetails::new(
1107 "Expected a non-empty sketch".to_owned(),
1108 vec![args.source_range],
1109 )));
1110 };
1111 any_edge_id
1112 };
1113
1114 let extrusion_info_edge_id = get_extrusion_info_edge_id(sketch, any_edge_id, clone_id_map);
1117
1118 let mut sketch = sketch.clone();
1119 match body_type {
1120 BodyType::Solid => {
1121 sketch.is_closed = ProfileClosed::Explicitly;
1122 }
1123 BodyType::Surface => {}
1124 _other => {
1125 }
1128 }
1129
1130 match (extrude_method, being_extruded) {
1131 (ExtrudeMethod::Merge, BeingExtruded::Face { .. }) => {
1132 if let SketchSurface::Face(ref face) = sketch.on {
1135 sketch.id = face.parent_solid.sketch_or_solid_id();
1138 }
1139 }
1140 (ExtrudeMethod::New, BeingExtruded::Face { .. }) => {
1141 sketch.id = extrude_cmd_id.into();
1144 }
1145 (ExtrudeMethod::New, BeingExtruded::Sketch) => {
1146 }
1149 (ExtrudeMethod::Merge, BeingExtruded::Sketch) => {
1150 if let SketchSurface::Face(ref face) = sketch.on {
1151 sketch.id = face.parent_solid.sketch_or_solid_id();
1154 }
1155 }
1156 (other, _) => {
1157 return Err(KclError::new_internal(KclErrorDetails::new(
1159 format!("Zoo does not yet support creating bodies via {other:?}"),
1160 vec![args.source_range],
1161 )));
1162 }
1163 }
1164
1165 let sketch_id = if let Some(cloned_from) = sketch.clone
1167 && clone_id_map.is_some()
1168 {
1169 cloned_from
1170 } else {
1171 sketch.id
1172 };
1173
1174 let solid3d_info = exec_state
1175 .send_modeling_cmd(
1176 ModelingCmdMeta::from_args(exec_state, args),
1177 ModelingCmd::from(
1178 mcmd::Solid3dGetExtrusionFaceInfo::builder()
1179 .maybe_edge_id(extrusion_info_edge_id)
1180 .object_id(sketch_id)
1181 .build(),
1182 ),
1183 )
1184 .await?;
1185
1186 let face_infos = if let OkWebSocketResponseData::Modeling {
1187 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
1188 } = solid3d_info
1189 {
1190 data.faces
1191 } else {
1192 vec![]
1193 };
1194
1195 if !args.ctx.settings.skip_artifact_graph {
1197 if !sectional {
1200 exec_state
1201 .batch_modeling_cmd(
1202 ModelingCmdMeta::from_args(exec_state, args),
1203 ModelingCmd::from(
1204 mcmd::Solid3dGetAdjacencyInfo::builder()
1205 .object_id(sketch_id)
1206 .maybe_edge_id(extrusion_info_edge_id)
1207 .build(),
1208 ),
1209 )
1210 .await?;
1211 }
1212 }
1213
1214 let Faces {
1215 sides: mut face_id_map,
1216 mut start_cap_id,
1217 mut end_cap_id,
1218 } = analyze_faces(exec_state, args, face_infos).await;
1219
1220 if sketch.clone.is_some()
1222 && let Some(clone_id_map) = clone_id_map
1223 {
1224 face_id_map = face_id_map
1225 .into_iter()
1226 .filter_map(|(k, v)| {
1227 let fe_key = clone_id_map.get(&k)?;
1228 let fe_value = clone_id_map.get(&(v?)).copied();
1229 Some((*fe_key, fe_value))
1230 })
1231 .collect::<HashMap<Uuid, Option<Uuid>>>();
1232 start_cap_id = start_cap_id.and_then(|id| clone_id_map.get(&id).copied());
1235 end_cap_id = end_cap_id.and_then(|id| clone_id_map.get(&id).copied());
1236 }
1237
1238 let no_engine_commands = args.ctx.no_engine_commands().await;
1240 let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
1241 let outer_surfaces = sketch.paths.iter().flat_map(|path| {
1242 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
1243 surface_of(path, *actual_face_id)
1244 } else if no_engine_commands {
1245 crate::log::logln!(
1246 "No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
1247 path.get_base().geo_meta.id
1248 );
1249 fake_extrude_surface(exec_state, path)
1251 } else if sketch.clone.is_some()
1252 && let Some(clone_map) = clone_id_map
1253 {
1254 let new_path = clone_map.get(&(path.get_base().geo_meta.id));
1255
1256 if let Some(new_path) = new_path {
1257 match face_id_map.get(new_path) {
1258 Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
1259 _ => {
1260 let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
1261 if let Some(value) = value {
1262 if value == new_path { Some(key) } else { None }
1263 } else {
1264 None
1265 }
1266 });
1267 match actual_face_id {
1268 Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
1269 None => {
1270 crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
1271 None
1272 }
1273 }
1274 }
1275 }
1276 } else {
1277 None
1278 }
1279 } else {
1280 crate::log::logln!(
1281 "No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
1282 path.get_base().geo_meta.id
1283 );
1284 None
1285 }
1286 });
1287
1288 new_value.extend(outer_surfaces);
1289 let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
1290 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
1291 surface_of(path, *actual_face_id)
1292 } else if no_engine_commands {
1293 fake_extrude_surface(exec_state, path)
1295 } else {
1296 None
1297 }
1298 });
1299 new_value.extend(inner_surfaces);
1300
1301 if let Some(tag_start) = named_cap_tags.start {
1305 if let Some(start_cap_id) = start_cap_id {
1306 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1307 face_id: start_cap_id,
1308 tag: Some(tag_start.clone()),
1309 geo_meta: GeoMeta {
1310 id: start_cap_id,
1311 metadata: args.source_range.into(),
1312 },
1313 }));
1314 } else if clone_id_map.is_none() {
1315 return Err(KclError::new_type(KclErrorDetails::new(
1316 format!(
1317 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
1318 tag_start.name, sketch.id
1319 ),
1320 vec![args.source_range],
1321 )));
1322 }
1323 }
1324 if let Some(tag_end) = named_cap_tags.end {
1325 if let Some(end_cap_id) = end_cap_id {
1326 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1327 face_id: end_cap_id,
1328 tag: Some(tag_end.clone()),
1329 geo_meta: GeoMeta {
1330 id: end_cap_id,
1331 metadata: args.source_range.into(),
1332 },
1333 }));
1334 } else if clone_id_map.is_none() {
1335 return Err(KclError::new_type(KclErrorDetails::new(
1336 format!(
1337 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
1338 tag_end.name, sketch.id
1339 ),
1340 vec![args.source_range],
1341 )));
1342 }
1343 }
1344
1345 let meta = sketch.meta.clone();
1346 let units = sketch.units;
1347 let id = sketch.id;
1348 let topology_id = sketch.original_id;
1349 let creator = match being_extruded {
1350 BeingExtruded::Sketch => SolidCreator::Sketch(sketch),
1351 BeingExtruded::Face { face_id, solid_id } => SolidCreator::Face(CreatorFace {
1352 face_id,
1353 solid_id,
1354 sketch,
1355 }),
1356 BeingExtruded::Edge => {
1357 let message = "Expected an edge to have been extruded via another code path";
1358 debug_assert!(false, "{message}");
1359 return Err(KclError::new_internal(KclErrorDetails::new(
1360 message.to_owned(),
1361 vec![args.source_range],
1362 )));
1363 }
1364 };
1365
1366 Ok(Solid {
1367 id,
1368 value_id: extrude_cmd_id.into(),
1369 topology_id,
1370 pattern_source_artifact_id: None,
1371 best_guess_body_type: Some(body_type),
1372 artifact_id: extrude_cmd_id,
1373 value: new_value,
1374 faces: Default::default(),
1375 meta,
1376 units,
1377 sectional,
1378 creator,
1379 start_cap_id,
1380 end_cap_id,
1381 edge_cuts: vec![],
1382 pending_edge_cut_ids: vec![],
1383 })
1384}
1385
1386#[derive(Debug, Default)]
1387struct Faces {
1388 sides: HashMap<Uuid, Option<Uuid>>,
1390 end_cap_id: Option<Uuid>,
1392 start_cap_id: Option<Uuid>,
1394}
1395
1396async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
1397 let mut faces = Faces {
1398 sides: HashMap::with_capacity(face_infos.len()),
1399 ..Default::default()
1400 };
1401 if args.ctx.no_engine_commands().await {
1402 faces.start_cap_id = Some(exec_state.next_uuid());
1404 faces.end_cap_id = Some(exec_state.next_uuid());
1405 }
1406 for face_info in face_infos {
1407 match face_info.cap {
1408 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
1409 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
1410 ExtrusionFaceCapType::Both => {
1411 faces.end_cap_id = face_info.face_id;
1412 faces.start_cap_id = face_info.face_id;
1413 }
1414 ExtrusionFaceCapType::None => {
1415 if let Some(curve_id) = face_info.curve_id {
1416 faces.sides.insert(curve_id, face_info.face_id);
1417 }
1418 }
1419 other => {
1420 exec_state.warn(
1421 crate::CompilationIssue {
1422 source_range: args.source_range,
1423 message: format!("unknown extrusion face type {other:?}"),
1424 suggestion: None,
1425 severity: crate::errors::Severity::Warning,
1426 tag: crate::errors::Tag::Unnecessary,
1427 },
1428 annotations::WARN_NOT_YET_SUPPORTED,
1429 );
1430 }
1431 }
1432 }
1433 faces
1434}
1435fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
1436 match path {
1437 Path::Arc { .. }
1438 | Path::TangentialArc { .. }
1439 | Path::TangentialArcTo { .. }
1440 | Path::Ellipse { .. }
1442 | Path::Conic {.. }
1443 | Path::Circle { .. }
1444 | Path::CircleThreePoint { .. } => {
1445 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1446 face_id: actual_face_id,
1447 tag: path.get_base().tag.clone(),
1448 geo_meta: GeoMeta {
1449 id: path.get_base().geo_meta.id,
1450 metadata: path.get_base().geo_meta.metadata,
1451 },
1452 });
1453 Some(extrude_surface)
1454 }
1455 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
1456 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1457 face_id: actual_face_id,
1458 tag: path.get_base().tag.clone(),
1459 geo_meta: GeoMeta {
1460 id: path.get_base().geo_meta.id,
1461 metadata: path.get_base().geo_meta.metadata,
1462 },
1463 });
1464 Some(extrude_surface)
1465 }
1466 Path::ArcThreePoint { .. } => {
1467 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1468 face_id: actual_face_id,
1469 tag: path.get_base().tag.clone(),
1470 geo_meta: GeoMeta {
1471 id: path.get_base().geo_meta.id,
1472 metadata: path.get_base().geo_meta.metadata,
1473 },
1474 });
1475 Some(extrude_surface)
1476 }
1477 }
1478}
1479
1480fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
1481 match path {
1482 Path::Arc { .. }
1483 | Path::TangentialArc { .. }
1484 | Path::TangentialArcTo { .. }
1485 | Path::Ellipse { .. }
1487 | Path::Conic {.. }
1488 | Path::Circle { .. }
1489 | Path::CircleThreePoint { .. } => {
1490 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1491 face_id: actual_face_id,
1492 tag: path.get_base().tag.clone(),
1493 geo_meta: GeoMeta {
1494 id: clone_path_id,
1495 metadata: path.get_base().geo_meta.metadata,
1496 },
1497 });
1498 Some(extrude_surface)
1499 }
1500 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
1501 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1502 face_id: actual_face_id,
1503 tag: path.get_base().tag.clone(),
1504 geo_meta: GeoMeta {
1505 id: clone_path_id,
1506 metadata: path.get_base().geo_meta.metadata,
1507 },
1508 });
1509 Some(extrude_surface)
1510 }
1511 Path::ArcThreePoint { .. } => {
1512 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1513 face_id: actual_face_id,
1514 tag: path.get_base().tag.clone(),
1515 geo_meta: GeoMeta {
1516 id: clone_path_id,
1517 metadata: path.get_base().geo_meta.metadata,
1518 },
1519 });
1520 Some(extrude_surface)
1521 }
1522 }
1523}
1524
1525fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
1527 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1528 face_id: exec_state.next_uuid(),
1530 tag: path.get_base().tag.clone(),
1531 geo_meta: GeoMeta {
1532 id: path.get_base().geo_meta.id,
1533 metadata: path.get_base().geo_meta.metadata,
1534 },
1535 });
1536 Some(extrude_surface)
1537}
1538
1539#[cfg(test)]
1540mod tests {
1541 use kcl_api::UnitLength;
1542
1543 use super::*;
1544 use crate::execution::AbstractSegment;
1545 use crate::execution::Plane;
1546 use crate::execution::SegmentRepr;
1547 use crate::execution::parse_execute;
1548 use crate::execution::types::NumericType;
1549 use crate::execution::types::NumericTypeExt;
1550 use crate::front::Expr;
1551 use crate::front::Number;
1552 use crate::front::ObjectId;
1553 use crate::front::Point2d;
1554 use crate::front::PointCtor;
1555 use crate::std::sketch::PlaneData;
1556
1557 fn point_expr(x: f64, y: f64) -> Point2d<Expr> {
1558 Point2d {
1559 x: Expr::Var(Number::from((x, UnitLength::Millimeters))),
1560 y: Expr::Var(Number::from((y, UnitLength::Millimeters))),
1561 }
1562 }
1563
1564 fn segment_value(exec_state: &mut ExecState) -> KclValue {
1565 let plane = Plane::from_plane_data_skipping_engine(PlaneData::XY, exec_state).unwrap();
1566 let segment = Segment {
1567 id: exec_state.next_uuid(),
1568 object_id: ObjectId(1),
1569 kind: SegmentKind::Point {
1570 position: [TyF64::new(0.0, NumericType::mm()), TyF64::new(0.0, NumericType::mm())],
1571 ctor: Box::new(PointCtor {
1572 position: point_expr(0.0, 0.0),
1573 }),
1574 freedom: None,
1575 },
1576 surface: SketchSurface::Plane(Box::new(plane)),
1577 sketch_id: exec_state.next_uuid(),
1578 sketch: None,
1579 tag: None,
1580 node_path: None,
1581 meta: vec![],
1582 };
1583 KclValue::Segment {
1584 value: Box::new(AbstractSegment {
1585 repr: SegmentRepr::Solved {
1586 segment: Box::new(segment),
1587 },
1588 meta: vec![],
1589 }),
1590 }
1591 }
1592
1593 #[tokio::test(flavor = "multi_thread")]
1594 async fn extrude_accepts_negative_bidirectional_length_in_mock_exec() {
1595 let code = r#"
1596profile001 = startSketchOn(XY)
1597 |> startProfile(at = [0, 0])
1598 |> line(end = [1, 0])
1599 |> line(end = [0, 1])
1600 |> close()
1601
1602extrude(profile001, length = 1, bidirectionalLength = -1)
1603"#;
1604
1605 let result = parse_execute(code).await.unwrap();
1606 let extrude = result
1607 .root_module_artifact_commands()
1608 .iter()
1609 .find_map(|artifact_command| match &artifact_command.command {
1610 ModelingCmd::Extrude(extrude) => Some(extrude),
1611 _ => None,
1612 })
1613 .expect("expected an extrude command");
1614
1615 assert_eq!(extrude.opposite, Opposite::Other(LengthUnit(-1.0)));
1616 }
1617
1618 #[tokio::test(flavor = "multi_thread")]
1619 async fn explicit_merge_warns_for_plane_based_sketches() {
1620 let code = r#"
1621@settings(kclVersion = 2.0)
1622
1623profiles = sketch(on = XY) {
1624 circle1 = circle(start = [var 1mm, var 0mm], center = [var 0mm, var 0mm])
1625 circle2 = circle(start = [var 4mm, var 0mm], center = [var 3mm, var 0mm])
1626 circle3 = circle(start = [var 7mm, var 0mm], center = [var 6mm, var 0mm])
1627}
1628
1629leftRegion = region(segments = [profiles.circle1])
1630rightRegion = region(segments = [profiles.circle2])
1631extrude([leftRegion, rightRegion], length = 1mm, method = MERGE)
1632
1633defaultRegion = region(segments = [profiles.circle3])
1634extrude(defaultRegion, length = 1mm)
1635"#;
1636
1637 let result = parse_execute(code).await.unwrap();
1638 let warnings: Vec<_> = result
1639 .issues()
1640 .iter()
1641 .filter(|issue| issue.severity == crate::errors::Severity::Warning)
1642 .collect();
1643
1644 assert_eq!(warnings.len(), 1, "expected one warning, got {warnings:#?}");
1645 assert!(
1646 warnings[0].message.contains(
1647 "2 plane-based sketches have no parent body for `method = MERGE`, so each will create a separate body"
1648 ),
1649 "{}",
1650 warnings[0].message
1651 );
1652 }
1653
1654 #[tokio::test(flavor = "multi_thread")]
1655 async fn extrude_rejects_an_empty_target_array() {
1656 let err = parse_execute("nothing = extrude([], length = 5)").await.unwrap_err();
1657
1658 assert!(matches!(err, KclError::Argument { .. }), "{err:?}");
1659 assert!(err.message().contains("requires one or more"), "{err:?}");
1660 }
1661
1662 #[tokio::test(flavor = "multi_thread")]
1663 async fn edge_extrude_succeeds_in_mock_exec() {
1664 let code = r#"
1665@settings(kclVersion = 2.0)
1666
1667sketch001 = sketch(on = XZ) {
1668 line1 = line(start = [0mm, 0mm], end = [1mm, 0mm])
1669}
1670extrude001 = extrude(sketch001.line1, length = 5, bodyType = SURFACE)
1671extrude(
1672 getOppositeEdge(extrude001.sketch.tags.line1),
1673 length = 1,
1674 method = NEW,
1675 bodyType = SURFACE,
1676)
1677"#;
1678
1679 parse_execute(code).await.unwrap();
1680 }
1681
1682 #[tokio::test(flavor = "multi_thread")]
1683 async fn edge_specifier_target_cannot_be_extruded_to_a_reference() {
1684 let code = r#"
1685@settings(kclVersion = 2.0, experimentalFeatures = allow)
1686
1687profile = startSketchOn(XY)
1688 |> startProfile(at = [0, 0])
1689 |> line(end = [1, 0], tag = $sideFace)
1690 |> line(end = [0, 1])
1691 |> line(end = [-1, 0])
1692 |> close()
1693body = extrude(profile, length = 1, tagEnd = $endFace)
1694
1695extrude(
1696 { sideFaces = [sideFace, endFace] },
1697 to = offsetPlane(XY, offset = 10),
1698 bodyType = SURFACE,
1699 method = NEW,
1700)
1701"#;
1702
1703 let err = parse_execute(code).await.unwrap_err();
1704
1705 assert!(matches!(err, KclError::Semantic { .. }), "{err:?}");
1706 assert!(
1707 err.message()
1708 .contains("Edge specifiers cannot be extruded to a reference"),
1709 "{err:?}"
1710 );
1711 }
1712
1713 #[tokio::test(flavor = "multi_thread")]
1714 async fn segment_extrude_rejects_cap_tags() {
1715 let ctx = ExecutorContext::new_mock(None).await;
1716 let mut exec_state = ExecState::new(&ctx);
1717 let err = coerce_extrude_targets(
1718 vec![segment_value(&mut exec_state)],
1719 BodyType::Surface,
1720 Some(&TagDeclarator::new("cap_start")),
1721 None,
1722 &mut exec_state,
1723 &ctx,
1724 crate::SourceRange::default(),
1725 )
1726 .await
1727 .unwrap_err();
1728
1729 assert!(
1730 err.message()
1731 .contains("`tagStart` and `tagEnd` are not supported when extruding sketch segments"),
1732 "{err:?}"
1733 );
1734 ctx.close().await;
1735 }
1736
1737 fn edge_value(exec_state: &mut ExecState) -> KclValue {
1740 KclValue::Uuid {
1741 value: exec_state.next_uuid(),
1742 meta: vec![],
1743 }
1744 }
1745
1746 #[tokio::test(flavor = "multi_thread")]
1747 async fn edge_extrude_rejects_solid_body_type() {
1748 let ctx = ExecutorContext::new_mock(None).await;
1749 let mut exec_state = ExecState::new(&ctx);
1750 let edge = edge_value(&mut exec_state);
1751 let err = coerce_extrude_targets(
1752 vec![edge],
1753 BodyType::Solid,
1754 None,
1755 None,
1756 &mut exec_state,
1757 &ctx,
1758 crate::SourceRange::default(),
1759 )
1760 .await
1761 .unwrap_err();
1762
1763 assert!(
1764 err.message()
1765 .contains("edges can only be extruded with surface extrudes"),
1766 "{err:?}"
1767 );
1768 ctx.close().await;
1769 }
1770
1771 #[tokio::test(flavor = "multi_thread")]
1772 async fn edge_extrude_rejects_cap_tags() {
1773 let ctx = ExecutorContext::new_mock(None).await;
1774 let mut exec_state = ExecState::new(&ctx);
1775 let edge = edge_value(&mut exec_state);
1776 let err = coerce_extrude_targets(
1777 vec![edge],
1778 BodyType::Surface,
1779 Some(&TagDeclarator::new("cap_start")),
1780 None,
1781 &mut exec_state,
1782 &ctx,
1783 crate::SourceRange::default(),
1784 )
1785 .await
1786 .unwrap_err();
1787
1788 assert!(
1789 err.message()
1790 .contains("`tagStart` and `tagEnd` are not supported when extruding edges"),
1791 "{err:?}"
1792 );
1793 ctx.close().await;
1794 }
1795
1796 #[tokio::test(flavor = "multi_thread")]
1797 async fn edge_extrude_rejects_mixing_with_face() {
1798 let ctx = ExecutorContext::new_mock(None).await;
1799 let mut exec_state = ExecState::new(&ctx);
1800 let edge = edge_value(&mut exec_state);
1801 let face = KclValue::String {
1803 value: "START".to_owned(),
1804 meta: vec![],
1805 };
1806 let err = coerce_extrude_targets(
1807 vec![edge, face],
1808 BodyType::Surface,
1809 None,
1810 None,
1811 &mut exec_state,
1812 &ctx,
1813 crate::SourceRange::default(),
1814 )
1815 .await
1816 .unwrap_err();
1817
1818 assert!(
1819 err.message()
1820 .contains("Cannot extrude edges together with sketches or faces"),
1821 "{err:?}"
1822 );
1823 ctx.close().await;
1824 }
1825}