1use std::collections::HashMap;
4
5use anyhow::Result;
6use kcl_derive_docs::stdlib;
7use kcmc::{
8 each_cmd as mcmd,
9 length_unit::LengthUnit,
10 ok_response::OkModelingCmdResponse,
11 output::ExtrusionFaceInfo,
12 shared::{ExtrusionFaceCapType, Opposite},
13 websocket::{ModelingCmdReq, OkWebSocketResponseData},
14 ModelingCmd,
15};
16use kittycad_modeling_cmds::{self as kcmc};
17use uuid::Uuid;
18
19use super::args::TyF64;
20#[cfg(feature = "artifact-graph")]
21use crate::execution::ArtifactId;
22use crate::{
23 errors::{KclError, KclErrorDetails},
24 execution::{types::RuntimeType, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSurface, Solid},
25 parsing::ast::types::TagNode,
26 std::Args,
27};
28
29pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
31 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
32 let length: TyF64 = args.get_kw_arg_typed("length", &RuntimeType::length(), exec_state)?;
33 let symmetric = args.get_kw_arg_opt("symmetric")?;
34 let bidirectional_length: Option<TyF64> =
35 args.get_kw_arg_opt_typed("bidirectionalLength", &RuntimeType::length(), exec_state)?;
36 let tag_start = args.get_kw_arg_opt("tagStart")?;
37 let tag_end = args.get_kw_arg_opt("tagEnd")?;
38
39 let result = inner_extrude(
40 sketches,
41 length,
42 symmetric,
43 bidirectional_length,
44 tag_start,
45 tag_end,
46 exec_state,
47 args,
48 )
49 .await?;
50
51 Ok(result.into())
52}
53
54#[stdlib {
148 name = "extrude",
149 feature_tree_operation = true,
150 keywords = true,
151 unlabeled_first = true,
152 args = {
153 sketches = { docs = "Which sketch or sketches should be extruded"},
154 length = { docs = "How far to extrude the given sketches"},
155 symmetric = { docs = "If true, the extrusion will happen symmetrically around the sketch. Otherwise, the extrusion will happen on only one side of the sketch." },
156 bidirectional_length = { docs = "If specified, will also extrude in the opposite direction to 'distance' to the specified distance. If 'symmetric' is true, this value is ignored."},
157 tag_start = { docs = "A named tag for the face at the start of the extrusion, i.e. the original sketch" },
158 tag_end = { docs = "A named tag for the face at the end of the extrusion, i.e. the new face created by extruding the original sketch" },
159 },
160 tags = ["sketch"]
161}]
162#[allow(clippy::too_many_arguments)]
163async fn inner_extrude(
164 sketches: Vec<Sketch>,
165 length: TyF64,
166 symmetric: Option<bool>,
167 bidirectional_length: Option<TyF64>,
168 tag_start: Option<TagNode>,
169 tag_end: Option<TagNode>,
170 exec_state: &mut ExecState,
171 args: Args,
172) -> Result<Vec<Solid>, KclError> {
173 let mut solids = Vec::new();
175
176 if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
177 return Err(KclError::Semantic(KclErrorDetails {
178 source_ranges: vec![args.source_range],
179 message: "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
180 .to_owned(),
181 }));
182 }
183
184 let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
185
186 let opposite = match (symmetric, bidirection) {
187 (Some(true), _) => Opposite::Symmetric,
188 (None, None) => Opposite::None,
189 (Some(false), None) => Opposite::None,
190 (None, Some(length)) => Opposite::Other(length),
191 (Some(false), Some(length)) => Opposite::Other(length),
192 };
193
194 for sketch in &sketches {
195 let id = exec_state.next_uuid();
196 args.batch_modeling_cmds(&sketch.build_sketch_mode_cmds(
197 exec_state,
198 ModelingCmdReq {
199 cmd_id: id.into(),
200 cmd: ModelingCmd::from(mcmd::Extrude {
201 target: sketch.id.into(),
202 distance: LengthUnit(length.to_mm()),
203 faces: Default::default(),
204 opposite: opposite.clone(),
205 }),
206 },
207 ))
208 .await?;
209
210 solids.push(
211 do_post_extrude(
212 sketch,
213 #[cfg(feature = "artifact-graph")]
214 id.into(),
215 length.clone(),
216 false,
217 &NamedCapTags {
218 start: tag_start.as_ref(),
219 end: tag_end.as_ref(),
220 },
221 exec_state,
222 &args,
223 None,
224 )
225 .await?,
226 );
227 }
228
229 Ok(solids)
230}
231
232#[derive(Debug, Default)]
233pub(crate) struct NamedCapTags<'a> {
234 pub start: Option<&'a TagNode>,
235 pub end: Option<&'a TagNode>,
236}
237
238#[allow(clippy::too_many_arguments)]
239pub(crate) async fn do_post_extrude<'a>(
240 sketch: &Sketch,
241 #[cfg(feature = "artifact-graph")] solid_id: ArtifactId,
242 length: TyF64,
243 sectional: bool,
244 named_cap_tags: &'a NamedCapTags<'a>,
245 exec_state: &mut ExecState,
246 args: &Args,
247 edge_id: Option<Uuid>,
248) -> Result<Solid, KclError> {
249 args.batch_modeling_cmd(
252 exec_state.next_uuid(),
253 ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
254 )
255 .await?;
256
257 let any_edge_id = if let Some(id) = edge_id {
258 id
259 } else if let Some(edge_id) = sketch.mirror {
260 edge_id
261 } else {
262 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
265 return Err(KclError::Type(KclErrorDetails {
266 message: "Expected a non-empty sketch".to_string(),
267 source_ranges: vec![args.source_range],
268 }));
269 };
270 any_edge_id
271 };
272
273 let mut sketch = sketch.clone();
274
275 if let SketchSurface::Face(ref face) = sketch.on {
277 sketch.id = face.solid.sketch.id;
278 }
279
280 let solid3d_info = args
281 .send_modeling_cmd(
282 exec_state.next_uuid(),
283 ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
284 edge_id: any_edge_id,
285 object_id: sketch.id,
286 }),
287 )
288 .await?;
289
290 let face_infos = if let OkWebSocketResponseData::Modeling {
291 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
292 } = solid3d_info
293 {
294 data.faces
295 } else {
296 vec![]
297 };
298
299 #[cfg(feature = "artifact-graph")]
301 {
302 if !sectional {
305 args.batch_modeling_cmd(
306 exec_state.next_uuid(),
307 ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
308 object_id: sketch.id,
309 edge_id: any_edge_id,
310 }),
311 )
312 .await?;
313 }
314 }
315
316 let Faces {
317 sides: face_id_map,
318 start_cap_id,
319 end_cap_id,
320 } = analyze_faces(exec_state, args, face_infos).await;
321
322 let no_engine_commands = args.ctx.no_engine_commands().await;
324 let mut new_value: Vec<ExtrudeSurface> = sketch
325 .paths
326 .iter()
327 .flat_map(|path| {
328 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
329 match path {
330 Path::Arc { .. }
331 | Path::TangentialArc { .. }
332 | Path::TangentialArcTo { .. }
333 | Path::Circle { .. }
334 | Path::CircleThreePoint { .. } => {
335 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
336 face_id: *actual_face_id,
337 tag: path.get_base().tag.clone(),
338 geo_meta: GeoMeta {
339 id: path.get_base().geo_meta.id,
340 metadata: path.get_base().geo_meta.metadata,
341 },
342 });
343 Some(extrude_surface)
344 }
345 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
346 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
347 face_id: *actual_face_id,
348 tag: path.get_base().tag.clone(),
349 geo_meta: GeoMeta {
350 id: path.get_base().geo_meta.id,
351 metadata: path.get_base().geo_meta.metadata,
352 },
353 });
354 Some(extrude_surface)
355 }
356 Path::ArcThreePoint { .. } => {
357 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
358 face_id: *actual_face_id,
359 tag: path.get_base().tag.clone(),
360 geo_meta: GeoMeta {
361 id: path.get_base().geo_meta.id,
362 metadata: path.get_base().geo_meta.metadata,
363 },
364 });
365 Some(extrude_surface)
366 }
367 }
368 } else if no_engine_commands {
369 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
372 face_id: exec_state.next_uuid(),
374 tag: path.get_base().tag.clone(),
375 geo_meta: GeoMeta {
376 id: path.get_base().geo_meta.id,
377 metadata: path.get_base().geo_meta.metadata,
378 },
379 });
380 Some(extrude_surface)
381 } else {
382 None
383 }
384 })
385 .collect();
386
387 if let Some(tag_start) = named_cap_tags.start {
389 let Some(start_cap_id) = start_cap_id else {
390 return Err(KclError::Type(KclErrorDetails {
391 message: format!(
392 "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
393 tag_start.name, sketch.id
394 ),
395 source_ranges: vec![args.source_range],
396 }));
397 };
398
399 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
400 face_id: start_cap_id,
401 tag: Some(tag_start.clone()),
402 geo_meta: GeoMeta {
403 id: start_cap_id,
404 metadata: args.source_range.into(),
405 },
406 }));
407 }
408 if let Some(tag_end) = named_cap_tags.end {
409 let Some(end_cap_id) = end_cap_id else {
410 return Err(KclError::Type(KclErrorDetails {
411 message: format!(
412 "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
413 tag_end.name, sketch.id
414 ),
415 source_ranges: vec![args.source_range],
416 }));
417 };
418
419 new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
420 face_id: end_cap_id,
421 tag: Some(tag_end.clone()),
422 geo_meta: GeoMeta {
423 id: end_cap_id,
424 metadata: args.source_range.into(),
425 },
426 }));
427 }
428
429 Ok(Solid {
430 id: sketch.id,
434 #[cfg(feature = "artifact-graph")]
435 artifact_id: solid_id,
436 value: new_value,
437 meta: sketch.meta.clone(),
438 units: sketch.units,
439 height: length.to_length_units(sketch.units),
440 sectional,
441 sketch,
442 start_cap_id,
443 end_cap_id,
444 edge_cuts: vec![],
445 })
446}
447
448#[derive(Default)]
449struct Faces {
450 sides: HashMap<Uuid, Option<Uuid>>,
452 end_cap_id: Option<Uuid>,
454 start_cap_id: Option<Uuid>,
456}
457
458async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
459 let mut faces = Faces {
460 sides: HashMap::with_capacity(face_infos.len()),
461 ..Default::default()
462 };
463 if args.ctx.no_engine_commands().await {
464 faces.start_cap_id = Some(exec_state.next_uuid());
466 faces.end_cap_id = Some(exec_state.next_uuid());
467 }
468 for face_info in face_infos {
469 match face_info.cap {
470 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
471 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
472 ExtrusionFaceCapType::Both => {
473 faces.end_cap_id = face_info.face_id;
474 faces.start_cap_id = face_info.face_id;
475 }
476 ExtrusionFaceCapType::None => {
477 if let Some(curve_id) = face_info.curve_id {
478 faces.sides.insert(curve_id, face_info.face_id);
479 }
480 }
481 }
482 }
483 faces
484}