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