1use std::collections::HashMap;
4
5use anyhow::Result;
6use kcl_derive_docs::stdlib;
7use kcmc::{
8 each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, output::ExtrusionFaceInfo,
9 shared::ExtrusionFaceCapType, websocket::OkWebSocketResponseData, ModelingCmd,
10};
11use kittycad_modeling_cmds as kcmc;
12use uuid::Uuid;
13
14use crate::{
15 errors::{KclError, KclErrorDetails},
16 execution::{
17 ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSet, SketchSurface, Solid,
18 SolidSet,
19 },
20 std::Args,
21};
22
23pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
25 let sketch_set = args.get_unlabeled_kw_arg("sketch_set")?;
26 let length = args.get_kw_arg("length")?;
27
28 let result = inner_extrude(sketch_set, length, exec_state, args).await?;
29
30 Ok(result.into())
31}
32
33#[stdlib {
80 name = "extrude",
81 feature_tree_operation = true,
82 keywords = true,
83 unlabeled_first = true,
84 args = {
85 sketch_set = { docs = "Which sketches should be extruded"},
86 length = { docs = "How far to extrude the given sketches"},
87 }
88}]
89async fn inner_extrude(
90 sketch_set: SketchSet,
91 length: f64,
92 exec_state: &mut ExecState,
93 args: Args,
94) -> Result<SolidSet, KclError> {
95 let id = exec_state.next_uuid();
96
97 let sketches: Vec<Sketch> = sketch_set.into();
99 let mut solids = Vec::new();
100 for sketch in &sketches {
101 args.batch_modeling_cmd(
104 exec_state.next_uuid(),
105 ModelingCmd::from(mcmd::EnableSketchMode {
106 animated: false,
107 ortho: false,
108 entity_id: sketch.on.id(),
109 adjust_camera: false,
110 planar_normal: if let SketchSurface::Plane(plane) = &sketch.on {
111 Some(plane.z_axis.into())
113 } else {
114 None
115 },
116 }),
117 )
118 .await?;
119
120 args.batch_modeling_cmd(
124 id,
125 ModelingCmd::from(mcmd::Extrude {
126 target: sketch.id.into(),
127 distance: LengthUnit(length),
128 faces: Default::default(),
129 }),
130 )
131 .await?;
132
133 args.batch_modeling_cmd(
135 exec_state.next_uuid(),
136 ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
137 )
138 .await?;
139 solids.push(do_post_extrude(sketch.clone(), id.into(), length, exec_state, args.clone()).await?);
140 }
141
142 Ok(solids.into())
143}
144
145pub(crate) async fn do_post_extrude(
146 sketch: Sketch,
147 solid_id: ArtifactId,
148 length: f64,
149 exec_state: &mut ExecState,
150 args: Args,
151) -> Result<Box<Solid>, KclError> {
152 args.batch_modeling_cmd(
155 exec_state.next_uuid(),
156 ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
157 )
158 .await?;
159
160 let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
163 return Err(KclError::Type(KclErrorDetails {
164 message: "Expected a non-empty sketch".to_string(),
165 source_ranges: vec![args.source_range],
166 }));
167 };
168
169 let mut sketch = sketch.clone();
170
171 if let SketchSurface::Face(ref face) = sketch.on {
173 sketch.id = face.solid.sketch.id;
174 }
175
176 let solid3d_info = args
177 .send_modeling_cmd(
178 exec_state.next_uuid(),
179 ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
180 edge_id: any_edge_id,
181 object_id: sketch.id,
182 }),
183 )
184 .await?;
185
186 let face_infos = if let OkWebSocketResponseData::Modeling {
187 modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
188 } = solid3d_info
189 {
190 data.faces
191 } else {
192 vec![]
193 };
194
195 for (curve_id, face_id) in face_infos
196 .iter()
197 .filter(|face_info| face_info.cap == ExtrusionFaceCapType::None)
198 .filter_map(|face_info| {
199 if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
200 Some((curve_id, face_id))
201 } else {
202 None
203 }
204 })
205 {
206 args.batch_modeling_cmd(
211 exec_state.next_uuid(),
212 ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
213 edge_id: curve_id,
214 object_id: sketch.id,
215 face_id,
216 }),
217 )
218 .await?;
219
220 args.batch_modeling_cmd(
221 exec_state.next_uuid(),
222 ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
223 edge_id: curve_id,
224 object_id: sketch.id,
225 face_id,
226 }),
227 )
228 .await?;
229 }
230
231 let Faces {
232 sides: face_id_map,
233 start_cap_id,
234 end_cap_id,
235 } = analyze_faces(exec_state, &args, face_infos).await;
236 let no_engine_commands = args.ctx.no_engine_commands().await;
238 let new_value = sketch
239 .paths
240 .iter()
241 .flat_map(|path| {
242 if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
243 match path {
244 Path::Arc { .. }
245 | Path::TangentialArc { .. }
246 | Path::TangentialArcTo { .. }
247 | Path::Circle { .. }
248 | Path::CircleThreePoint { .. } => {
249 let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
250 face_id: *actual_face_id,
251 tag: path.get_base().tag.clone(),
252 geo_meta: GeoMeta {
253 id: path.get_base().geo_meta.id,
254 metadata: path.get_base().geo_meta.metadata,
255 },
256 });
257 Some(extrude_surface)
258 }
259 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
260 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
261 face_id: *actual_face_id,
262 tag: path.get_base().tag.clone(),
263 geo_meta: GeoMeta {
264 id: path.get_base().geo_meta.id,
265 metadata: path.get_base().geo_meta.metadata,
266 },
267 });
268 Some(extrude_surface)
269 }
270 }
271 } else if no_engine_commands {
272 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
275 face_id: exec_state.next_uuid(),
277 tag: path.get_base().tag.clone(),
278 geo_meta: GeoMeta {
279 id: path.get_base().geo_meta.id,
280 metadata: path.get_base().geo_meta.metadata,
281 },
282 });
283 Some(extrude_surface)
284 } else {
285 None
286 }
287 })
288 .collect();
289
290 Ok(Box::new(Solid {
291 id: sketch.id,
295 artifact_id: solid_id,
296 value: new_value,
297 meta: sketch.meta.clone(),
298 units: sketch.units,
299 sketch,
300 height: length,
301 start_cap_id,
302 end_cap_id,
303 edge_cuts: vec![],
304 }))
305}
306
307#[derive(Default)]
308struct Faces {
309 sides: HashMap<Uuid, Option<Uuid>>,
311 end_cap_id: Option<Uuid>,
313 start_cap_id: Option<Uuid>,
315}
316
317async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
318 let mut faces = Faces {
319 sides: HashMap::with_capacity(face_infos.len()),
320 ..Default::default()
321 };
322 if args.ctx.no_engine_commands().await {
323 faces.start_cap_id = Some(exec_state.next_uuid());
325 faces.end_cap_id = Some(exec_state.next_uuid());
326 }
327 for face_info in face_infos {
328 match face_info.cap {
329 ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
330 ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
331 ExtrusionFaceCapType::Both => {
332 faces.end_cap_id = face_info.face_id;
333 faces.start_cap_id = face_info.face_id;
334 }
335 ExtrusionFaceCapType::None => {
336 if let Some(curve_id) = face_info.curve_id {
337 faces.sides.insert(curve_id, face_info.face_id);
338 }
339 }
340 }
341 }
342 faces
343}