1use std::collections::HashMap;
4
5use anyhow::Result;
6use kcmc::shared::Point3d as KPoint3d; use kcmc::{
8 ModelingCmd, each_cmd as mcmd,
9 length_unit::LengthUnit,
10 ok_response::OkModelingCmdResponse,
11 output::ExtrusionFaceInfo,
12 shared::{ExtrudeReference, ExtrusionFaceCapType, Opposite},
13 websocket::{ModelingCmdReq, OkWebSocketResponseData},
14};
15use kittycad_modeling_cmds::{
16 self as kcmc,
17 shared::{Angle, ExtrudeMethod, Point2d},
18};
19use uuid::Uuid;
20
21use super::{DEFAULT_TOLERANCE_MM, args::TyF64, utils::point_to_mm};
22use crate::{
23 errors::{KclError, KclErrorDetails},
24 execution::{
25 ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta, Path, Sketch, SketchSurface, Solid,
26 types::{PrimitiveType, RuntimeType},
27 },
28 parsing::ast::types::TagNode,
29 std::{Args, axis_or_reference::Point3dAxis3dOrGeometryReference},
30};
31
32pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
34 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
35 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
36 let to = args.get_kw_arg_opt(
37 "to",
38 &RuntimeType::Union(vec![
39 RuntimeType::point3d(),
40 RuntimeType::Primitive(PrimitiveType::Axis3d),
41 RuntimeType::Primitive(PrimitiveType::Edge),
42 RuntimeType::plane(),
43 RuntimeType::Primitive(PrimitiveType::Face),
44 RuntimeType::sketch(),
45 RuntimeType::Primitive(PrimitiveType::Solid),
46 RuntimeType::tagged_edge(),
47 RuntimeType::tagged_face(),
48 ]),
49 exec_state,
50 )?;
51 let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
52 let bidirectional_length: Option<TyF64> =
53 args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
54 let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
55 let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
56 let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
57 let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
58 let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
59 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
60 let method: Option<String> = args.get_kw_arg_opt("method", &RuntimeType::string(), exec_state)?;
61
62 let result = inner_extrude(
63 sketches,
64 length,
65 to,
66 symmetric,
67 bidirectional_length,
68 tag_start,
69 tag_end,
70 twist_angle,
71 twist_angle_step,
72 twist_center,
73 tolerance,
74 method,
75 exec_state,
76 args,
77 )
78 .await?;
79
80 Ok(result.into())
81}
82
83#[allow(clippy::too_many_arguments)]
84async fn inner_extrude(
85 sketches: Vec<Sketch>,
86 length: Option<TyF64>,
87 to: Option<Point3dAxis3dOrGeometryReference>,
88 symmetric: Option<bool>,
89 bidirectional_length: Option<TyF64>,
90 tag_start: Option<TagNode>,
91 tag_end: Option<TagNode>,
92 twist_angle: Option<TyF64>,
93 twist_angle_step: Option<TyF64>,
94 twist_center: Option<[TyF64; 2]>,
95 tolerance: Option<TyF64>,
96 method: Option<String>,
97 exec_state: &mut ExecState,
98 args: Args,
99) -> Result<Vec<Solid>, KclError> {
100 let mut solids = Vec::new();
102 let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
103
104 let extrude_method = match method.as_deref() {
105 Some("new" | "NEW") => ExtrudeMethod::New,
106 Some("merge" | "MERGE") => ExtrudeMethod::Merge,
107 None => ExtrudeMethod::default(),
108 Some(other) => {
109 return Err(KclError::new_semantic(KclErrorDetails::new(
110 format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
111 vec![args.source_range],
112 )));
113 }
114 };
115
116 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
117 return Err(KclError::new_semantic(KclErrorDetails::new(
118 "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
119 .to_owned(),
120 vec![args.source_range],
121 )));
122 }
123
124 if (length.is_some() || twist_angle.is_some()) && to.is_some() {
125 return Err(KclError::new_semantic(KclErrorDetails::new(
126 "You cannot give `length` or `twist` params with the `to` param, you have to choose one or the other"
127 .to_owned(),
128 vec![args.source_range],
129 )));
130 }
131
132 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
133
134 let opposite = match (symmetric, bidirection) {
135 (Some(true), _) => Opposite::Symmetric,
136 (None, None) => Opposite::None,
137 (Some(false), None) => Opposite::None,
138 (None, Some(length)) => Opposite::Other(length),
139 (Some(false), Some(length)) => Opposite::Other(length),
140 };
141
142 for sketch in &sketches {
143 let id = exec_state.next_uuid();
144 let cmd = match (&twist_angle, &twist_angle_step, &twist_center, length.clone(), &to) {
145 (Some(angle), angle_step, center, Some(length), None) => {
146 let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
147 let total_rotation_angle = Angle::from_degrees(angle.to_degrees());
148 let angle_step_size = Angle::from_degrees(angle_step.clone().map(|a| a.to_degrees()).unwrap_or(15.0));
149 ModelingCmd::from(mcmd::TwistExtrude {
150 target: sketch.id.into(),
151 distance: LengthUnit(length.to_mm()),
152 faces: Default::default(),
153 center_2d: center,
154 total_rotation_angle,
155 angle_step_size,
156 tolerance,
157 })
158 }
159 (None, None, None, Some(length), None) => ModelingCmd::from(mcmd::Extrude {
160 target: sketch.id.into(),
161 distance: LengthUnit(length.to_mm()),
162 faces: Default::default(),
163 opposite: opposite.clone(),
164 extrude_method,
165 }),
166 (None, None, None, None, Some(to)) => match to {
167 Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(mcmd::ExtrudeToReference {
168 target: sketch.id.into(),
169 reference: ExtrudeReference::Point {
170 point: KPoint3d {
171 x: LengthUnit(point[0].to_mm()),
172 y: LengthUnit(point[1].to_mm()),
173 z: LengthUnit(point[2].to_mm()),
174 },
175 },
176 faces: Default::default(),
177 extrude_method,
178 }),
179 Point3dAxis3dOrGeometryReference::Axis { direction, origin } => {
180 ModelingCmd::from(mcmd::ExtrudeToReference {
181 target: sketch.id.into(),
182 reference: ExtrudeReference::Axis {
183 axis: KPoint3d {
184 x: direction[0].to_mm(),
185 y: direction[1].to_mm(),
186 z: direction[2].to_mm(),
187 },
188 point: KPoint3d {
189 x: LengthUnit(origin[0].to_mm()),
190 y: LengthUnit(origin[1].to_mm()),
191 z: LengthUnit(origin[2].to_mm()),
192 },
193 },
194 faces: Default::default(),
195 extrude_method,
196 })
197 }
198 Point3dAxis3dOrGeometryReference::Plane(plane) => {
199 let plane_id = if plane.value == crate::exec::PlaneType::Uninit {
200 if plane.info.origin.units.is_none() {
201 return Err(KclError::new_semantic(KclErrorDetails::new(
202 "Origin of plane has unknown units".to_string(),
203 vec![args.source_range],
204 )));
205 }
206 let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
207 plane.clone().info.into_plane_data(),
208 exec_state,
209 &args,
210 )
211 .await?;
212 sketch_plane.id
213 } else {
214 plane.id
215 };
216 ModelingCmd::from(mcmd::ExtrudeToReference {
217 target: sketch.id.into(),
218 reference: ExtrudeReference::EntityReference { entity_id: plane_id },
219 faces: Default::default(),
220 extrude_method,
221 })
222 }
223 Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
224 let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
225 ModelingCmd::from(mcmd::ExtrudeToReference {
226 target: sketch.id.into(),
227 reference: ExtrudeReference::EntityReference { entity_id: edge_id },
228 faces: Default::default(),
229 extrude_method,
230 })
231 }
232 Point3dAxis3dOrGeometryReference::Face(face_tag) => {
233 let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
234 ModelingCmd::from(mcmd::ExtrudeToReference {
235 target: sketch.id.into(),
236 reference: ExtrudeReference::EntityReference { entity_id: face_id },
237 faces: Default::default(),
238 extrude_method,
239 })
240 }
241 Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(mcmd::ExtrudeToReference {
242 target: sketch.id.into(),
243 reference: ExtrudeReference::EntityReference {
244 entity_id: sketch_ref.id,
245 },
246 faces: Default::default(),
247 extrude_method,
248 }),
249 Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(mcmd::ExtrudeToReference {
250 target: sketch.id.into(),
251 reference: ExtrudeReference::EntityReference { entity_id: solid.id },
252 faces: Default::default(),
253 extrude_method,
254 }),
255 Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
256 let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
257 let tagged_edge_or_face_id = tagged_edge_or_face.id;
258 ModelingCmd::from(mcmd::ExtrudeToReference {
259 target: sketch.id.into(),
260 reference: ExtrudeReference::EntityReference {
261 entity_id: tagged_edge_or_face_id,
262 },
263 faces: Default::default(),
264 extrude_method,
265 })
266 }
267 },
268 (Some(_), _, _, None, None) => {
269 return Err(KclError::new_semantic(KclErrorDetails::new(
270 "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
271 vec![args.source_range],
272 )));
273 }
274 (_, _, _, None, None) => {
275 return Err(KclError::new_semantic(KclErrorDetails::new(
276 "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
277 vec![args.source_range],
278 )));
279 }
280 (_, _, _, Some(_), Some(_)) => {
281 return Err(KclError::new_semantic(KclErrorDetails::new(
282 "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
283 vec![args.source_range],
284 )));
285 }
286 (_, _, _, _, _) => {
287 return Err(KclError::new_semantic(KclErrorDetails::new(
288 "Invalid combination of parameters for extrusion.".to_owned(),
289 vec![args.source_range],
290 )));
291 }
292 };
293 let cmds = sketch.build_sketch_mode_cmds(exec_state, ModelingCmdReq { cmd_id: id.into(), cmd });
294 exec_state
295 .batch_modeling_cmds(ModelingCmdMeta::from_args_id(&args, id), &cmds)
296 .await?;
297
298 solids.push(
299 do_post_extrude(
300 sketch,
301 id.into(),
302 false,
303 &NamedCapTags {
304 start: tag_start.as_ref(),
305 end: tag_end.as_ref(),
306 },
307 extrude_method,
308 exec_state,
309 &args,
310 None,
311 )
312 .await?,
313 );
314 }
315
316 Ok(solids)
317}
318
319#[derive(Debug, Default)]
320pub(crate) struct NamedCapTags<'a> {
321 pub start: Option<&'a TagNode>,
322 pub end: Option<&'a TagNode>,
323}
324
325#[allow(clippy::too_many_arguments)]
326pub(crate) async fn do_post_extrude<'a>(
327 sketch: &Sketch,
328 solid_id: ArtifactId,
329 sectional: bool,
330 named_cap_tags: &'a NamedCapTags<'a>,
331 extrude_method: ExtrudeMethod,
332 exec_state: &mut ExecState,
333 args: &Args,
334 edge_id: Option<Uuid>,
335) -> Result<Solid, KclError> {
336 exec_state
339 .batch_modeling_cmd(
340 args.into(),
341 ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
342 )
343 .await?;
344
345 let any_edge_id = if let Some(edge_id) = sketch.mirror {
346 edge_id
347 } else if let Some(id) = edge_id {
348 id
349 } else {
350 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
353 return Err(KclError::new_type(KclErrorDetails::new(
354 "Expected a non-empty sketch".to_owned(),
355 vec![args.source_range],
356 )));
357 };
358 any_edge_id
359 };
360
361 let mut sketch = sketch.clone();
362 sketch.is_closed = true;
363
364 if let SketchSurface::Face(ref face) = sketch.on {
366 if extrude_method != ExtrudeMethod::New {
368 sketch.id = face.solid.sketch.id;
369 }
370 }
371
372 let solid3d_info = exec_state
373 .send_modeling_cmd(
374 args.into(),
375 ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
376 edge_id: any_edge_id,
377 object_id: sketch.id,
378 }),
379 )
380 .await?;
381
382 let face_infos = if let OkWebSocketResponseData::Modeling {
383 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
384 } = solid3d_info
385 {
386 data.faces
387 } else {
388 vec![]
389 };
390
391 #[cfg(feature = "artifact-graph")]
393 {
394 if !sectional {
397 exec_state
398 .batch_modeling_cmd(
399 args.into(),
400 ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
401 object_id: sketch.id,
402 edge_id: any_edge_id,
403 }),
404 )
405 .await?;
406 }
407 }
408
409 let Faces {
410 sides: face_id_map,
411 start_cap_id,
412 end_cap_id,
413 } = analyze_faces(exec_state, args, face_infos).await;
414 let no_engine_commands = args.ctx.no_engine_commands().await;
416 let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
417 let outer_surfaces = sketch.paths.iter().flat_map(|path| {
418 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
419 surface_of(path, *actual_face_id)
420 } else if no_engine_commands {
421 fake_extrude_surface(exec_state, path)
423 } else {
424 None
425 }
426 });
427 new_value.extend(outer_surfaces);
428 let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
429 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
430 surface_of(path, *actual_face_id)
431 } else if no_engine_commands {
432 fake_extrude_surface(exec_state, path)
434 } else {
435 None
436 }
437 });
438 new_value.extend(inner_surfaces);
439
440 if let Some(tag_start) = named_cap_tags.start {
442 let Some(start_cap_id) = start_cap_id else {
443 return Err(KclError::new_type(KclErrorDetails::new(
444 format!(
445 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
446 tag_start.name, sketch.id
447 ),
448 vec![args.source_range],
449 )));
450 };
451
452 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
453 face_id: start_cap_id,
454 tag: Some(tag_start.clone()),
455 geo_meta: GeoMeta {
456 id: start_cap_id,
457 metadata: args.source_range.into(),
458 },
459 }));
460 }
461 if let Some(tag_end) = named_cap_tags.end {
462 let Some(end_cap_id) = end_cap_id else {
463 return Err(KclError::new_type(KclErrorDetails::new(
464 format!(
465 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
466 tag_end.name, sketch.id
467 ),
468 vec![args.source_range],
469 )));
470 };
471
472 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
473 face_id: end_cap_id,
474 tag: Some(tag_end.clone()),
475 geo_meta: GeoMeta {
476 id: end_cap_id,
477 metadata: args.source_range.into(),
478 },
479 }));
480 }
481
482 Ok(Solid {
483 id: sketch.id,
490 artifact_id: solid_id,
491 value: new_value,
492 meta: sketch.meta.clone(),
493 units: sketch.units,
494 sectional,
495 sketch,
496 start_cap_id,
497 end_cap_id,
498 edge_cuts: vec![],
499 })
500}
501
502#[derive(Default)]
503struct Faces {
504 sides: HashMap<Uuid, Option<Uuid>>,
506 end_cap_id: Option<Uuid>,
508 start_cap_id: Option<Uuid>,
510}
511
512async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
513 let mut faces = Faces {
514 sides: HashMap::with_capacity(face_infos.len()),
515 ..Default::default()
516 };
517 if args.ctx.no_engine_commands().await {
518 faces.start_cap_id = Some(exec_state.next_uuid());
520 faces.end_cap_id = Some(exec_state.next_uuid());
521 }
522 for face_info in face_infos {
523 match face_info.cap {
524 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
525 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
526 ExtrusionFaceCapType::Both => {
527 faces.end_cap_id = face_info.face_id;
528 faces.start_cap_id = face_info.face_id;
529 }
530 ExtrusionFaceCapType::None => {
531 if let Some(curve_id) = face_info.curve_id {
532 faces.sides.insert(curve_id, face_info.face_id);
533 }
534 }
535 }
536 }
537 faces
538}
539fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
540 match path {
541 Path::Arc { .. }
542 | Path::TangentialArc { .. }
543 | Path::TangentialArcTo { .. }
544 | Path::Ellipse { .. }
546 | Path::Conic {.. }
547 | Path::Circle { .. }
548 | Path::CircleThreePoint { .. } => {
549 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
550 face_id: actual_face_id,
551 tag: path.get_base().tag.clone(),
552 geo_meta: GeoMeta {
553 id: path.get_base().geo_meta.id,
554 metadata: path.get_base().geo_meta.metadata,
555 },
556 });
557 Some(extrude_surface)
558 }
559 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
560 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
561 face_id: actual_face_id,
562 tag: path.get_base().tag.clone(),
563 geo_meta: GeoMeta {
564 id: path.get_base().geo_meta.id,
565 metadata: path.get_base().geo_meta.metadata,
566 },
567 });
568 Some(extrude_surface)
569 }
570 Path::ArcThreePoint { .. } => {
571 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
572 face_id: actual_face_id,
573 tag: path.get_base().tag.clone(),
574 geo_meta: GeoMeta {
575 id: path.get_base().geo_meta.id,
576 metadata: path.get_base().geo_meta.metadata,
577 },
578 });
579 Some(extrude_surface)
580 }
581 }
582}
583
584fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
586 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
587 face_id: exec_state.next_uuid(),
589 tag: path.get_base().tag.clone(),
590 geo_meta: GeoMeta {
591 id: path.get_base().geo_meta.id,
592 metadata: path.get_base().geo_meta.metadata,
593 },
594 });
595 Some(extrude_surface)
596}