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