1use std::collections::HashMap;
4
5use anyhow::Result;
6use kcmc::ModelingCmd;
7use kcmc::each_cmd as mcmd;
8use kcmc::length_unit::LengthUnit;
9use kcmc::ok_response::OkModelingCmdResponse;
10use kcmc::output::ExtrusionFaceInfo;
11use kcmc::shared::ExtrudeReference;
12use kcmc::shared::ExtrusionFaceCapType;
13use kcmc::shared::Opposite;
14use kcmc::shared::Point3d as KPoint3d; use kcmc::websocket::ModelingCmdReq;
16use kcmc::websocket::OkWebSocketResponseData;
17use kittycad_modeling_cmds::shared::Angle;
18use kittycad_modeling_cmds::shared::BodyType;
19use kittycad_modeling_cmds::shared::ExtrudeMethod;
20use kittycad_modeling_cmds::shared::Point2d;
21use kittycad_modeling_cmds::{self as kcmc};
22use uuid::Uuid;
23
24use super::DEFAULT_TOLERANCE_MM;
25use super::args::TyF64;
26use super::utils::point_to_mm;
27use crate::errors::KclError;
28use crate::errors::KclErrorDetails;
29use crate::execution::ArtifactId;
30use crate::execution::CreatorFace;
31use crate::execution::ExecState;
32use crate::execution::Extrudable;
33use crate::execution::ExtrudeSurface;
34use crate::execution::GeoMeta;
35use crate::execution::KclValue;
36use crate::execution::ModelingCmdMeta;
37use crate::execution::Path;
38use crate::execution::ProfileClosed;
39use crate::execution::Sketch;
40use crate::execution::SketchSurface;
41use crate::execution::Solid;
42use crate::execution::SolidCreator;
43use crate::execution::annotations;
44use crate::execution::types::ArrayLen;
45use crate::execution::types::PrimitiveType;
46use crate::execution::types::RuntimeType;
47use crate::parsing::ast::types::TagNode;
48use crate::std::Args;
49use crate::std::axis_or_reference::Point3dAxis3dOrGeometryReference;
50
51pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
53 let sketches: Vec<Extrudable> = args.get_unlabeled_kw_arg(
54 "sketches",
55 &RuntimeType::Array(
56 Box::new(RuntimeType::Union(vec![
57 RuntimeType::sketch(),
58 RuntimeType::face(),
59 RuntimeType::tagged_face(),
60 ])),
61 ArrayLen::Minimum(1),
62 ),
63 exec_state,
64 )?;
65
66 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
67 let to = args.get_kw_arg_opt(
68 "to",
69 &RuntimeType::Union(vec![
70 RuntimeType::point3d(),
71 RuntimeType::Primitive(PrimitiveType::Axis3d),
72 RuntimeType::Primitive(PrimitiveType::Edge),
73 RuntimeType::plane(),
74 RuntimeType::Primitive(PrimitiveType::Face),
75 RuntimeType::sketch(),
76 RuntimeType::Primitive(PrimitiveType::Solid),
77 RuntimeType::tagged_edge(),
78 RuntimeType::tagged_face(),
79 ]),
80 exec_state,
81 )?;
82 let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
83 let bidirectional_length: Option<TyF64> =
84 args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
85 let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
86 let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
87 let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
88 let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
89 let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
90 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
91 let method: Option<String> = args.get_kw_arg_opt("method", &RuntimeType::string(), exec_state)?;
92 let hide_seams: Option<bool> = args.get_kw_arg_opt("hideSeams", &RuntimeType::bool(), exec_state)?;
93 let body_type: Option<BodyType> = args.get_kw_arg_opt("bodyType", &RuntimeType::string(), exec_state)?;
94
95 let result = inner_extrude(
96 sketches,
97 length,
98 to,
99 symmetric,
100 bidirectional_length,
101 tag_start,
102 tag_end,
103 twist_angle,
104 twist_angle_step,
105 twist_center,
106 tolerance,
107 method,
108 hide_seams,
109 body_type,
110 exec_state,
111 args,
112 )
113 .await?;
114
115 Ok(result.into())
116}
117
118#[allow(clippy::too_many_arguments)]
119async fn inner_extrude(
120 extrudables: Vec<Extrudable>,
121 length: Option<TyF64>,
122 to: Option<Point3dAxis3dOrGeometryReference>,
123 symmetric: Option<bool>,
124 bidirectional_length: Option<TyF64>,
125 tag_start: Option<TagNode>,
126 tag_end: Option<TagNode>,
127 twist_angle: Option<TyF64>,
128 twist_angle_step: Option<TyF64>,
129 twist_center: Option<[TyF64; 2]>,
130 tolerance: Option<TyF64>,
131 method: Option<String>,
132 hide_seams: Option<bool>,
133 body_type: Option<BodyType>,
134 exec_state: &mut ExecState,
135 args: Args,
136) -> Result<Vec<Solid>, KclError> {
137 let body_type = body_type.unwrap_or_default();
138
139 if matches!(body_type, BodyType::Solid) && extrudables.iter().any(|sk| matches!(sk.is_closed(), ProfileClosed::No))
140 {
141 return Err(KclError::new_semantic(KclErrorDetails::new(
142 "Cannot solid extrude an open profile. Either close the profile, or use a surface extrude.".to_owned(),
143 vec![args.source_range],
144 )));
145 }
146
147 let mut solids = Vec::new();
149 let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
150
151 let extrude_method = match method.as_deref() {
152 Some("new" | "NEW") => ExtrudeMethod::New,
153 Some("merge" | "MERGE") => ExtrudeMethod::Merge,
154 None => ExtrudeMethod::default(),
155 Some(other) => {
156 return Err(KclError::new_semantic(KclErrorDetails::new(
157 format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
158 vec![args.source_range],
159 )));
160 }
161 };
162
163 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
164 return Err(KclError::new_semantic(KclErrorDetails::new(
165 "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
166 .to_owned(),
167 vec![args.source_range],
168 )));
169 }
170
171 if (length.is_some() || twist_angle.is_some()) && to.is_some() {
172 return Err(KclError::new_semantic(KclErrorDetails::new(
173 "You cannot give `length` or `twist` params with the `to` param, you have to choose one or the other"
174 .to_owned(),
175 vec![args.source_range],
176 )));
177 }
178
179 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
180
181 let opposite = match (symmetric, bidirection) {
182 (Some(true), _) => Opposite::Symmetric,
183 (None, None) => Opposite::None,
184 (Some(false), None) => Opposite::None,
185 (None, Some(length)) => Opposite::Other(length),
186 (Some(false), Some(length)) => Opposite::Other(length),
187 };
188
189 for extrudable in &extrudables {
190 let extrude_cmd_id = exec_state.next_uuid();
191 let sketch_or_face_id = extrudable.id_to_extrude(exec_state, &args, false).await?;
192 let cmd = match (&twist_angle, &twist_angle_step, &twist_center, length.clone(), &to) {
193 (Some(angle), angle_step, center, Some(length), None) => {
194 let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
195 let total_rotation_angle = Angle::from_degrees(angle.to_degrees(exec_state, args.source_range));
196 let angle_step_size = Angle::from_degrees(
197 angle_step
198 .clone()
199 .map(|a| a.to_degrees(exec_state, args.source_range))
200 .unwrap_or(15.0),
201 );
202 ModelingCmd::from(
203 mcmd::TwistExtrude::builder()
204 .target(sketch_or_face_id.into())
205 .distance(LengthUnit(length.to_mm()))
206 .center_2d(center)
207 .total_rotation_angle(total_rotation_angle)
208 .angle_step_size(angle_step_size)
209 .tolerance(tolerance)
210 .body_type(body_type)
211 .build(),
212 )
213 }
214 (None, None, None, Some(length), None) => ModelingCmd::from(
215 mcmd::Extrude::builder()
216 .target(sketch_or_face_id.into())
217 .distance(LengthUnit(length.to_mm()))
218 .opposite(opposite.clone())
219 .extrude_method(extrude_method)
220 .body_type(body_type)
221 .maybe_merge_coplanar_faces(hide_seams)
222 .build(),
223 ),
224 (None, None, None, None, Some(to)) => match to {
225 Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(
226 mcmd::ExtrudeToReference::builder()
227 .target(sketch_or_face_id.into())
228 .reference(ExtrudeReference::Point {
229 point: KPoint3d {
230 x: LengthUnit(point[0].to_mm()),
231 y: LengthUnit(point[1].to_mm()),
232 z: LengthUnit(point[2].to_mm()),
233 },
234 })
235 .extrude_method(extrude_method)
236 .body_type(body_type)
237 .build(),
238 ),
239 Point3dAxis3dOrGeometryReference::Axis { direction, origin } => ModelingCmd::from(
240 mcmd::ExtrudeToReference::builder()
241 .target(sketch_or_face_id.into())
242 .reference(ExtrudeReference::Axis {
243 axis: KPoint3d {
244 x: direction[0].to_mm(),
245 y: direction[1].to_mm(),
246 z: direction[2].to_mm(),
247 },
248 point: KPoint3d {
249 x: LengthUnit(origin[0].to_mm()),
250 y: LengthUnit(origin[1].to_mm()),
251 z: LengthUnit(origin[2].to_mm()),
252 },
253 })
254 .extrude_method(extrude_method)
255 .body_type(body_type)
256 .build(),
257 ),
258 Point3dAxis3dOrGeometryReference::Plane(plane) => {
259 let plane_id = if plane.is_uninitialized() {
260 if plane.info.origin.units.is_none() {
261 return Err(KclError::new_semantic(KclErrorDetails::new(
262 "Origin of plane has unknown units".to_string(),
263 vec![args.source_range],
264 )));
265 }
266 let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
267 plane.clone().info.into_plane_data(),
268 exec_state,
269 &args,
270 )
271 .await?;
272 sketch_plane.id
273 } else {
274 plane.id
275 };
276 ModelingCmd::from(
277 mcmd::ExtrudeToReference::builder()
278 .target(sketch_or_face_id.into())
279 .reference(ExtrudeReference::EntityReference { entity_id: plane_id })
280 .extrude_method(extrude_method)
281 .body_type(body_type)
282 .build(),
283 )
284 }
285 Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
286 let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
287 ModelingCmd::from(
288 mcmd::ExtrudeToReference::builder()
289 .target(sketch_or_face_id.into())
290 .reference(ExtrudeReference::EntityReference { entity_id: edge_id })
291 .extrude_method(extrude_method)
292 .body_type(body_type)
293 .build(),
294 )
295 }
296 Point3dAxis3dOrGeometryReference::Face(face_tag) => {
297 let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
298 ModelingCmd::from(
299 mcmd::ExtrudeToReference::builder()
300 .target(sketch_or_face_id.into())
301 .reference(ExtrudeReference::EntityReference { entity_id: face_id })
302 .extrude_method(extrude_method)
303 .body_type(body_type)
304 .build(),
305 )
306 }
307 Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(
308 mcmd::ExtrudeToReference::builder()
309 .target(sketch_or_face_id.into())
310 .reference(ExtrudeReference::EntityReference {
311 entity_id: sketch_ref.id,
312 })
313 .extrude_method(extrude_method)
314 .body_type(body_type)
315 .build(),
316 ),
317 Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(
318 mcmd::ExtrudeToReference::builder()
319 .target(sketch_or_face_id.into())
320 .reference(ExtrudeReference::EntityReference { entity_id: solid.id })
321 .extrude_method(extrude_method)
322 .body_type(body_type)
323 .build(),
324 ),
325 Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
326 let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
327 let tagged_edge_or_face_id = tagged_edge_or_face.id;
328 ModelingCmd::from(
329 mcmd::ExtrudeToReference::builder()
330 .target(sketch_or_face_id.into())
331 .reference(ExtrudeReference::EntityReference {
332 entity_id: tagged_edge_or_face_id,
333 })
334 .extrude_method(extrude_method)
335 .body_type(body_type)
336 .build(),
337 )
338 }
339 },
340 (Some(_), _, _, None, None) => {
341 return Err(KclError::new_semantic(KclErrorDetails::new(
342 "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
343 vec![args.source_range],
344 )));
345 }
346 (_, _, _, None, None) => {
347 return Err(KclError::new_semantic(KclErrorDetails::new(
348 "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
349 vec![args.source_range],
350 )));
351 }
352 (_, _, _, Some(_), Some(_)) => {
353 return Err(KclError::new_semantic(KclErrorDetails::new(
354 "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
355 vec![args.source_range],
356 )));
357 }
358 (_, _, _, _, _) => {
359 return Err(KclError::new_semantic(KclErrorDetails::new(
360 "Invalid combination of parameters for extrusion.".to_owned(),
361 vec![args.source_range],
362 )));
363 }
364 };
365
366 let being_extruded = match extrudable {
367 Extrudable::Sketch(..) => BeingExtruded::Sketch,
368 Extrudable::Face(face_tag) => {
369 let face_id = sketch_or_face_id;
370 let solid_id = match face_tag.geometry() {
371 Some(crate::execution::Geometry::Solid(solid)) => solid.id,
372 Some(crate::execution::Geometry::Sketch(sketch)) => match sketch.on {
373 SketchSurface::Face(face) => face.solid.id,
374 SketchSurface::Plane(_) => sketch.id,
375 },
376 None => face_id,
377 };
378 BeingExtruded::Face { face_id, solid_id }
379 }
380 };
381 if let Some(post_extr_sketch) = extrudable.as_sketch() {
382 let cmds = post_extr_sketch.build_sketch_mode_cmds(
383 exec_state,
384 ModelingCmdReq {
385 cmd_id: extrude_cmd_id.into(),
386 cmd,
387 },
388 );
389 exec_state
390 .batch_modeling_cmds(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), &cmds)
391 .await?;
392 solids.push(
393 do_post_extrude(
394 &post_extr_sketch,
395 extrude_cmd_id.into(),
396 false,
397 &NamedCapTags {
398 start: tag_start.as_ref(),
399 end: tag_end.as_ref(),
400 },
401 extrude_method,
402 exec_state,
403 &args,
404 None,
405 None,
406 body_type,
407 being_extruded,
408 )
409 .await?,
410 );
411 } else {
412 return Err(KclError::new_type(KclErrorDetails::new(
413 "Expected a sketch for extrusion".to_owned(),
414 vec![args.source_range],
415 )));
416 }
417 }
418
419 Ok(solids)
420}
421
422#[derive(Debug, Default)]
423pub(crate) struct NamedCapTags<'a> {
424 pub start: Option<&'a TagNode>,
425 pub end: Option<&'a TagNode>,
426}
427
428#[derive(Debug, Clone, Copy)]
429pub enum BeingExtruded {
430 Sketch,
431 Face { face_id: Uuid, solid_id: Uuid },
432}
433
434#[allow(clippy::too_many_arguments)]
435pub(crate) async fn do_post_extrude<'a>(
436 sketch: &Sketch,
437 extrude_cmd_id: ArtifactId,
438 sectional: bool,
439 named_cap_tags: &'a NamedCapTags<'a>,
440 extrude_method: ExtrudeMethod,
441 exec_state: &mut ExecState,
442 args: &Args,
443 edge_id: Option<Uuid>,
444 clone_id_map: Option<&HashMap<Uuid, Uuid>>, body_type: BodyType,
446 being_extruded: BeingExtruded,
447) -> Result<Solid, KclError> {
448 exec_state
452 .batch_modeling_cmd(
453 ModelingCmdMeta::from_args(exec_state, args),
454 ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(sketch.id).build()),
455 )
456 .await?;
457
458 let any_edge_id = if let Some(edge_id) = sketch.mirror {
459 edge_id
460 } else if let Some(id) = edge_id {
461 id
462 } else {
463 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
466 return Err(KclError::new_type(KclErrorDetails::new(
467 "Expected a non-empty sketch".to_owned(),
468 vec![args.source_range],
469 )));
470 };
471 any_edge_id
472 };
473
474 let mut extrusion_info_edge_id = any_edge_id;
476 if sketch.clone.is_some() && clone_id_map.is_some() {
477 extrusion_info_edge_id = if let Some(clone_map) = clone_id_map {
478 if let Some(new_edge_id) = clone_map.get(&extrusion_info_edge_id) {
479 *new_edge_id
480 } else {
481 extrusion_info_edge_id
482 }
483 } else {
484 any_edge_id
485 };
486 }
487
488 let mut sketch = sketch.clone();
489 match body_type {
490 BodyType::Solid => {
491 sketch.is_closed = ProfileClosed::Explicitly;
492 }
493 BodyType::Surface => {}
494 _other => {
495 }
498 }
499
500 match (extrude_method, being_extruded) {
501 (ExtrudeMethod::Merge, BeingExtruded::Face { .. }) => {
502 if let SketchSurface::Face(ref face) = sketch.on {
505 sketch.id = face.solid.sketch_id().unwrap_or(face.solid.id);
508 }
509 }
510 (ExtrudeMethod::New, BeingExtruded::Face { .. }) => {
511 sketch.id = extrude_cmd_id.into();
514 }
515 (ExtrudeMethod::New, BeingExtruded::Sketch) => {
516 }
519 (ExtrudeMethod::Merge, BeingExtruded::Sketch) => {
520 if let SketchSurface::Face(ref face) = sketch.on {
521 sketch.id = face.solid.sketch_id().unwrap_or(face.solid.id);
524 }
525 }
526 (other, _) => {
527 return Err(KclError::new_internal(KclErrorDetails::new(
529 format!("Zoo does not yet support creating bodies via {other:?}"),
530 vec![args.source_range],
531 )));
532 }
533 }
534
535 let sketch_id = if let Some(cloned_from) = sketch.clone
537 && clone_id_map.is_some()
538 {
539 cloned_from
540 } else {
541 sketch.id
542 };
543
544 let solid3d_info = exec_state
545 .send_modeling_cmd(
546 ModelingCmdMeta::from_args(exec_state, args),
547 ModelingCmd::from(
548 mcmd::Solid3dGetExtrusionFaceInfo::builder()
549 .edge_id(extrusion_info_edge_id)
550 .object_id(sketch_id)
551 .build(),
552 ),
553 )
554 .await?;
555
556 let face_infos = if let OkWebSocketResponseData::Modeling {
557 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
558 } = solid3d_info
559 {
560 data.faces
561 } else {
562 vec![]
563 };
564
565 #[cfg(feature = "artifact-graph")]
567 {
568 if !sectional {
571 exec_state
572 .batch_modeling_cmd(
573 ModelingCmdMeta::from_args(exec_state, args),
574 ModelingCmd::from(
575 mcmd::Solid3dGetAdjacencyInfo::builder()
576 .object_id(sketch.id)
577 .edge_id(any_edge_id)
578 .build(),
579 ),
580 )
581 .await?;
582 }
583 }
584
585 let Faces {
586 sides: mut face_id_map,
587 start_cap_id,
588 end_cap_id,
589 } = analyze_faces(exec_state, args, face_infos).await;
590
591 if sketch.clone.is_some()
593 && let Some(clone_id_map) = clone_id_map
594 {
595 face_id_map = face_id_map
596 .into_iter()
597 .filter_map(|(k, v)| {
598 let fe_key = clone_id_map.get(&k)?;
599 let fe_value = clone_id_map.get(&(v?)).copied();
600 Some((*fe_key, fe_value))
601 })
602 .collect::<HashMap<Uuid, Option<Uuid>>>();
603 }
604
605 let no_engine_commands = args.ctx.no_engine_commands().await;
607 let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
608 let outer_surfaces = sketch.paths.iter().flat_map(|path| {
609 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
610 surface_of(path, *actual_face_id)
611 } else if no_engine_commands {
612 crate::log::logln!(
613 "No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
614 path.get_base().geo_meta.id
615 );
616 fake_extrude_surface(exec_state, path)
618 } else if sketch.clone.is_some()
619 && let Some(clone_map) = clone_id_map
620 {
621 let new_path = clone_map.get(&(path.get_base().geo_meta.id));
622
623 if let Some(new_path) = new_path {
624 match face_id_map.get(new_path) {
625 Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
626 _ => {
627 let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
628 if let Some(value) = value {
629 if value == new_path { Some(key) } else { None }
630 } else {
631 None
632 }
633 });
634 match actual_face_id {
635 Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
636 None => {
637 crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
638 None
639 }
640 }
641 }
642 }
643 } else {
644 None
645 }
646 } else {
647 crate::log::logln!(
648 "No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
649 path.get_base().geo_meta.id
650 );
651 None
652 }
653 });
654
655 new_value.extend(outer_surfaces);
656 let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
657 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
658 surface_of(path, *actual_face_id)
659 } else if no_engine_commands {
660 fake_extrude_surface(exec_state, path)
662 } else {
663 None
664 }
665 });
666 new_value.extend(inner_surfaces);
667
668 if let Some(tag_start) = named_cap_tags.start {
670 let Some(start_cap_id) = start_cap_id else {
671 return Err(KclError::new_type(KclErrorDetails::new(
672 format!(
673 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
674 tag_start.name, sketch.id
675 ),
676 vec![args.source_range],
677 )));
678 };
679
680 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
681 face_id: start_cap_id,
682 tag: Some(tag_start.clone()),
683 geo_meta: GeoMeta {
684 id: start_cap_id,
685 metadata: args.source_range.into(),
686 },
687 }));
688 }
689 if let Some(tag_end) = named_cap_tags.end {
690 let Some(end_cap_id) = end_cap_id else {
691 return Err(KclError::new_type(KclErrorDetails::new(
692 format!(
693 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
694 tag_end.name, sketch.id
695 ),
696 vec![args.source_range],
697 )));
698 };
699
700 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
701 face_id: end_cap_id,
702 tag: Some(tag_end.clone()),
703 geo_meta: GeoMeta {
704 id: end_cap_id,
705 metadata: args.source_range.into(),
706 },
707 }));
708 }
709
710 let meta = sketch.meta.clone();
711 let units = sketch.units;
712 let id = sketch.id;
713 let creator = match being_extruded {
722 BeingExtruded::Sketch => SolidCreator::Sketch(sketch),
723 BeingExtruded::Face { face_id, solid_id } => SolidCreator::Face(CreatorFace {
724 face_id,
725 solid_id,
726 sketch,
727 }),
728 };
729
730 Ok(Solid {
731 id,
732 artifact_id: extrude_cmd_id,
733 value: new_value,
734 meta,
735 units,
736 sectional,
737 creator,
738 start_cap_id,
739 end_cap_id,
740 edge_cuts: vec![],
741 })
742}
743
744#[derive(Default)]
745struct Faces {
746 sides: HashMap<Uuid, Option<Uuid>>,
748 end_cap_id: Option<Uuid>,
750 start_cap_id: Option<Uuid>,
752}
753
754async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
755 let mut faces = Faces {
756 sides: HashMap::with_capacity(face_infos.len()),
757 ..Default::default()
758 };
759 if args.ctx.no_engine_commands().await {
760 faces.start_cap_id = Some(exec_state.next_uuid());
762 faces.end_cap_id = Some(exec_state.next_uuid());
763 }
764 for face_info in face_infos {
765 match face_info.cap {
766 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
767 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
768 ExtrusionFaceCapType::Both => {
769 faces.end_cap_id = face_info.face_id;
770 faces.start_cap_id = face_info.face_id;
771 }
772 ExtrusionFaceCapType::None => {
773 if let Some(curve_id) = face_info.curve_id {
774 faces.sides.insert(curve_id, face_info.face_id);
775 }
776 }
777 other => {
778 exec_state.warn(
779 crate::CompilationError {
780 source_range: args.source_range,
781 message: format!("unknown extrusion face type {other:?}"),
782 suggestion: None,
783 severity: crate::errors::Severity::Warning,
784 tag: crate::errors::Tag::Unnecessary,
785 },
786 annotations::WARN_NOT_YET_SUPPORTED,
787 );
788 }
789 }
790 }
791 faces
792}
793fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
794 match path {
795 Path::Arc { .. }
796 | Path::TangentialArc { .. }
797 | Path::TangentialArcTo { .. }
798 | Path::Ellipse { .. }
800 | Path::Conic {.. }
801 | Path::Circle { .. }
802 | Path::CircleThreePoint { .. } => {
803 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
804 face_id: actual_face_id,
805 tag: path.get_base().tag.clone(),
806 geo_meta: GeoMeta {
807 id: path.get_base().geo_meta.id,
808 metadata: path.get_base().geo_meta.metadata,
809 },
810 });
811 Some(extrude_surface)
812 }
813 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
814 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
815 face_id: actual_face_id,
816 tag: path.get_base().tag.clone(),
817 geo_meta: GeoMeta {
818 id: path.get_base().geo_meta.id,
819 metadata: path.get_base().geo_meta.metadata,
820 },
821 });
822 Some(extrude_surface)
823 }
824 Path::ArcThreePoint { .. } => {
825 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
826 face_id: actual_face_id,
827 tag: path.get_base().tag.clone(),
828 geo_meta: GeoMeta {
829 id: path.get_base().geo_meta.id,
830 metadata: path.get_base().geo_meta.metadata,
831 },
832 });
833 Some(extrude_surface)
834 }
835 }
836}
837
838fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
839 match path {
840 Path::Arc { .. }
841 | Path::TangentialArc { .. }
842 | Path::TangentialArcTo { .. }
843 | Path::Ellipse { .. }
845 | Path::Conic {.. }
846 | Path::Circle { .. }
847 | Path::CircleThreePoint { .. } => {
848 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
849 face_id: actual_face_id,
850 tag: path.get_base().tag.clone(),
851 geo_meta: GeoMeta {
852 id: clone_path_id,
853 metadata: path.get_base().geo_meta.metadata,
854 },
855 });
856 Some(extrude_surface)
857 }
858 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
859 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
860 face_id: actual_face_id,
861 tag: path.get_base().tag.clone(),
862 geo_meta: GeoMeta {
863 id: clone_path_id,
864 metadata: path.get_base().geo_meta.metadata,
865 },
866 });
867 Some(extrude_surface)
868 }
869 Path::ArcThreePoint { .. } => {
870 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
871 face_id: actual_face_id,
872 tag: path.get_base().tag.clone(),
873 geo_meta: GeoMeta {
874 id: clone_path_id,
875 metadata: path.get_base().geo_meta.metadata,
876 },
877 });
878 Some(extrude_surface)
879 }
880 }
881}
882
883fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
885 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
886 face_id: exec_state.next_uuid(),
888 tag: path.get_base().tag.clone(),
889 geo_meta: GeoMeta {
890 id: path.get_base().geo_meta.id,
891 metadata: path.get_base().geo_meta.metadata,
892 },
893 });
894 Some(extrude_surface)
895}