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