1use std::collections::HashMap;
4
5use anyhow::Result;
6use kcmc::{
7 ModelingCmd, each_cmd as mcmd,
8 length_unit::LengthUnit,
9 ok_response::OkModelingCmdResponse,
10 output::ExtrusionFaceInfo,
11 shared::{ExtrusionFaceCapType, Opposite},
12 websocket::{ModelingCmdReq, OkWebSocketResponseData},
13};
14use kittycad_modeling_cmds::{
15 self as kcmc,
16 shared::{Angle, ExtrudeMethod, Point2d},
17};
18use uuid::Uuid;
19
20use super::{DEFAULT_TOLERANCE_MM, args::TyF64, utils::point_to_mm};
21use crate::{
22 errors::{KclError, KclErrorDetails},
23 execution::{
24 ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta, Path, Sketch, SketchSurface, Solid,
25 types::RuntimeType,
26 },
27 parsing::ast::types::TagNode,
28 std::Args,
29};
30
31pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
33 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
34 let length: TyF64 = args.get_kw_arg("length", &RuntimeType::length(), exec_state)?;
35 let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
36 let bidirectional_length: Option<TyF64> =
37 args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
38 let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
39 let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
40 let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
41 let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
42 let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
43 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
44 let method: Option<String> = args.get_kw_arg_opt("method", &RuntimeType::string(), exec_state)?;
45
46 let result = inner_extrude(
47 sketches,
48 length,
49 symmetric,
50 bidirectional_length,
51 tag_start,
52 tag_end,
53 twist_angle,
54 twist_angle_step,
55 twist_center,
56 tolerance,
57 method,
58 exec_state,
59 args,
60 )
61 .await?;
62
63 Ok(result.into())
64}
65
66#[allow(clippy::too_many_arguments)]
67async fn inner_extrude(
68 sketches: Vec<Sketch>,
69 length: TyF64,
70 symmetric: Option<bool>,
71 bidirectional_length: Option<TyF64>,
72 tag_start: Option<TagNode>,
73 tag_end: Option<TagNode>,
74 twist_angle: Option<TyF64>,
75 twist_angle_step: Option<TyF64>,
76 twist_center: Option<[TyF64; 2]>,
77 tolerance: Option<TyF64>,
78 method: Option<String>,
79 exec_state: &mut ExecState,
80 args: Args,
81) -> Result<Vec<Solid>, KclError> {
82 let mut solids = Vec::new();
84 let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
85
86 let extrude_method = match method.as_deref() {
87 Some("new" | "NEW") => ExtrudeMethod::New,
88 Some("merge" | "MERGE") => ExtrudeMethod::Merge,
89 None => ExtrudeMethod::default(),
90 Some(other) => {
91 return Err(KclError::new_semantic(KclErrorDetails::new(
92 format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
93 vec![args.source_range],
94 )));
95 }
96 };
97
98 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
99 return Err(KclError::new_semantic(KclErrorDetails::new(
100 "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
101 .to_owned(),
102 vec![args.source_range],
103 )));
104 }
105
106 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
107
108 let opposite = match (symmetric, bidirection) {
109 (Some(true), _) => Opposite::Symmetric,
110 (None, None) => Opposite::None,
111 (Some(false), None) => Opposite::None,
112 (None, Some(length)) => Opposite::Other(length),
113 (Some(false), Some(length)) => Opposite::Other(length),
114 };
115
116 for sketch in &sketches {
117 let id = exec_state.next_uuid();
118 let cmd = match (&twist_angle, &twist_angle_step, &twist_center) {
119 (Some(angle), angle_step, center) => {
120 let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
121 let total_rotation_angle = Angle::from_degrees(angle.to_degrees());
122 let angle_step_size = Angle::from_degrees(angle_step.clone().map(|a| a.to_degrees()).unwrap_or(15.0));
123 ModelingCmd::from(mcmd::TwistExtrude {
124 target: sketch.id.into(),
125 distance: LengthUnit(length.to_mm()),
126 faces: Default::default(),
127 center_2d: center,
128 total_rotation_angle,
129 angle_step_size,
130 tolerance,
131 })
132 }
133 (None, _, _) => ModelingCmd::from(mcmd::Extrude {
134 target: sketch.id.into(),
135 distance: LengthUnit(length.to_mm()),
136 faces: Default::default(),
137 opposite: opposite.clone(),
138 extrude_method,
139 }),
140 };
141 let cmds = sketch.build_sketch_mode_cmds(exec_state, ModelingCmdReq { cmd_id: id.into(), cmd });
142 exec_state
143 .batch_modeling_cmds(ModelingCmdMeta::from_args_id(&args, id), &cmds)
144 .await?;
145
146 solids.push(
147 do_post_extrude(
148 sketch,
149 id.into(),
150 length.clone(),
151 false,
152 &NamedCapTags {
153 start: tag_start.as_ref(),
154 end: tag_end.as_ref(),
155 },
156 extrude_method,
157 exec_state,
158 &args,
159 None,
160 )
161 .await?,
162 );
163 }
164
165 Ok(solids)
166}
167
168#[derive(Debug, Default)]
169pub(crate) struct NamedCapTags<'a> {
170 pub start: Option<&'a TagNode>,
171 pub end: Option<&'a TagNode>,
172}
173
174#[allow(clippy::too_many_arguments)]
175pub(crate) async fn do_post_extrude<'a>(
176 sketch: &Sketch,
177 solid_id: ArtifactId,
178 length: TyF64,
179 sectional: bool,
180 named_cap_tags: &'a NamedCapTags<'a>,
181 extrude_method: ExtrudeMethod,
182 exec_state: &mut ExecState,
183 args: &Args,
184 edge_id: Option<Uuid>,
185) -> Result<Solid, KclError> {
186 exec_state
189 .batch_modeling_cmd(
190 args.into(),
191 ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
192 )
193 .await?;
194
195 let any_edge_id = if let Some(edge_id) = sketch.mirror {
196 edge_id
197 } else if let Some(id) = edge_id {
198 id
199 } else {
200 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
203 return Err(KclError::new_type(KclErrorDetails::new(
204 "Expected a non-empty sketch".to_owned(),
205 vec![args.source_range],
206 )));
207 };
208 any_edge_id
209 };
210
211 let mut sketch = sketch.clone();
212 sketch.is_closed = true;
213
214 if let SketchSurface::Face(ref face) = sketch.on {
216 if extrude_method != ExtrudeMethod::New {
218 sketch.id = face.solid.sketch.id;
219 }
220 }
221
222 let solid3d_info = exec_state
223 .send_modeling_cmd(
224 args.into(),
225 ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
226 edge_id: any_edge_id,
227 object_id: sketch.id,
228 }),
229 )
230 .await?;
231
232 let face_infos = if let OkWebSocketResponseData::Modeling {
233 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
234 } = solid3d_info
235 {
236 data.faces
237 } else {
238 vec![]
239 };
240
241 #[cfg(feature = "artifact-graph")]
243 {
244 if !sectional {
247 exec_state
248 .batch_modeling_cmd(
249 args.into(),
250 ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
251 object_id: sketch.id,
252 edge_id: any_edge_id,
253 }),
254 )
255 .await?;
256 }
257 }
258
259 let Faces {
260 sides: face_id_map,
261 start_cap_id,
262 end_cap_id,
263 } = analyze_faces(exec_state, args, face_infos).await;
264 let no_engine_commands = args.ctx.no_engine_commands().await;
266 let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
267 let outer_surfaces = sketch.paths.iter().flat_map(|path| {
268 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
269 surface_of(path, *actual_face_id)
270 } else if no_engine_commands {
271 fake_extrude_surface(exec_state, path)
273 } else {
274 None
275 }
276 });
277 new_value.extend(outer_surfaces);
278 let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
279 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
280 surface_of(path, *actual_face_id)
281 } else if no_engine_commands {
282 fake_extrude_surface(exec_state, path)
284 } else {
285 None
286 }
287 });
288 new_value.extend(inner_surfaces);
289
290 if let Some(tag_start) = named_cap_tags.start {
292 let Some(start_cap_id) = start_cap_id else {
293 return Err(KclError::new_type(KclErrorDetails::new(
294 format!(
295 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
296 tag_start.name, sketch.id
297 ),
298 vec![args.source_range],
299 )));
300 };
301
302 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
303 face_id: start_cap_id,
304 tag: Some(tag_start.clone()),
305 geo_meta: GeoMeta {
306 id: start_cap_id,
307 metadata: args.source_range.into(),
308 },
309 }));
310 }
311 if let Some(tag_end) = named_cap_tags.end {
312 let Some(end_cap_id) = end_cap_id else {
313 return Err(KclError::new_type(KclErrorDetails::new(
314 format!(
315 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
316 tag_end.name, sketch.id
317 ),
318 vec![args.source_range],
319 )));
320 };
321
322 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
323 face_id: end_cap_id,
324 tag: Some(tag_end.clone()),
325 geo_meta: GeoMeta {
326 id: end_cap_id,
327 metadata: args.source_range.into(),
328 },
329 }));
330 }
331
332 Ok(Solid {
333 id: sketch.id,
340 artifact_id: solid_id,
341 value: new_value,
342 meta: sketch.meta.clone(),
343 units: sketch.units,
344 height: length.to_length_units(sketch.units),
345 sectional,
346 sketch,
347 start_cap_id,
348 end_cap_id,
349 edge_cuts: vec![],
350 })
351}
352
353#[derive(Default)]
354struct Faces {
355 sides: HashMap<Uuid, Option<Uuid>>,
357 end_cap_id: Option<Uuid>,
359 start_cap_id: Option<Uuid>,
361}
362
363async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
364 let mut faces = Faces {
365 sides: HashMap::with_capacity(face_infos.len()),
366 ..Default::default()
367 };
368 if args.ctx.no_engine_commands().await {
369 faces.start_cap_id = Some(exec_state.next_uuid());
371 faces.end_cap_id = Some(exec_state.next_uuid());
372 }
373 for face_info in face_infos {
374 match face_info.cap {
375 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
376 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
377 ExtrusionFaceCapType::Both => {
378 faces.end_cap_id = face_info.face_id;
379 faces.start_cap_id = face_info.face_id;
380 }
381 ExtrusionFaceCapType::None => {
382 if let Some(curve_id) = face_info.curve_id {
383 faces.sides.insert(curve_id, face_info.face_id);
384 }
385 }
386 }
387 }
388 faces
389}
390fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
391 match path {
392 Path::Arc { .. }
393 | Path::TangentialArc { .. }
394 | Path::TangentialArcTo { .. }
395 | Path::Ellipse { .. }
397 | Path::Conic {.. }
398 | Path::Circle { .. }
399 | Path::CircleThreePoint { .. } => {
400 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
401 face_id: actual_face_id,
402 tag: path.get_base().tag.clone(),
403 geo_meta: GeoMeta {
404 id: path.get_base().geo_meta.id,
405 metadata: path.get_base().geo_meta.metadata,
406 },
407 });
408 Some(extrude_surface)
409 }
410 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
411 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
412 face_id: actual_face_id,
413 tag: path.get_base().tag.clone(),
414 geo_meta: GeoMeta {
415 id: path.get_base().geo_meta.id,
416 metadata: path.get_base().geo_meta.metadata,
417 },
418 });
419 Some(extrude_surface)
420 }
421 Path::ArcThreePoint { .. } => {
422 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
423 face_id: actual_face_id,
424 tag: path.get_base().tag.clone(),
425 geo_meta: GeoMeta {
426 id: path.get_base().geo_meta.id,
427 metadata: path.get_base().geo_meta.metadata,
428 },
429 });
430 Some(extrude_surface)
431 }
432 }
433}
434
435fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
437 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
438 face_id: exec_state.next_uuid(),
440 tag: path.get_base().tag.clone(),
441 geo_meta: GeoMeta {
442 id: path.get_base().geo_meta.id,
443 metadata: path.get_base().geo_meta.metadata,
444 },
445 });
446 Some(extrude_surface)
447}