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