Skip to main content

kcl_lib/std/
surfaces.rs

1//! Standard library appearance.
2
3use std::collections::HashSet;
4
5use anyhow::Result;
6use kcmc::ModelingCmd;
7use kcmc::each_cmd as mcmd;
8use kittycad_modeling_cmds::length_unit::LengthUnit;
9use kittycad_modeling_cmds::ok_response::OkModelingCmdResponse;
10use kittycad_modeling_cmds::output as mout;
11use kittycad_modeling_cmds::shared::BodyType;
12use kittycad_modeling_cmds::shared::FractionOfEdge;
13use kittycad_modeling_cmds::shared::SurfaceEdgeReference;
14use kittycad_modeling_cmds::websocket::OkWebSocketResponseData;
15use kittycad_modeling_cmds::{self as kcmc};
16
17use crate::errors::KclError;
18use crate::errors::KclErrorDetails;
19use crate::execution::BoundedEdge;
20use crate::execution::ConsumedSolidOperation;
21use crate::execution::ExecState;
22use crate::execution::KclValue;
23use crate::execution::ModelingCmdMeta;
24use crate::execution::Solid;
25use crate::execution::SolidCreator;
26use crate::execution::types::ArrayLen;
27use crate::execution::types::PrimitiveType;
28use crate::execution::types::RuntimeType;
29use crate::std::Args;
30use crate::std::DEFAULT_TOLERANCE_MM;
31use crate::std::args::TyF64;
32use crate::std::edge;
33use crate::std::sketch::FaceTag;
34use crate::std::solid_consumption::record_consumed_solids;
35
36/// Flips the orientation of a surface, swapping which side is the front and which is the reverse.
37pub async fn flip_surface(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
38    let surface = args.get_unlabeled_kw_arg("surface", &RuntimeType::solids(), exec_state)?;
39    let out = inner_flip_surface(surface, exec_state, args).await?;
40    Ok(out.into())
41}
42
43async fn inner_flip_surface(
44    surfaces: Vec<Solid>,
45    exec_state: &mut ExecState,
46    args: Args,
47) -> Result<Vec<Solid>, KclError> {
48    for surface in &surfaces {
49        exec_state
50            .batch_modeling_cmd(
51                ModelingCmdMeta::from_args(exec_state, &args),
52                ModelingCmd::from(mcmd::Solid3dFlip::builder().object_id(surface.id).build()),
53            )
54            .await?;
55    }
56
57    Ok(surfaces)
58}
59
60/// Check if this object is a solid or not.
61pub async fn is_solid(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
62    let argument = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
63    let meta = vec![crate::execution::Metadata {
64        source_range: args.source_range,
65    }];
66
67    let res = inner_is_equal_body_type(argument, exec_state, args, BodyType::Solid).await?;
68    Ok(KclValue::Bool { value: res, meta })
69}
70
71/// Check if this object is a surface or not.
72pub async fn is_surface(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
73    let argument = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
74    let meta = vec![crate::execution::Metadata {
75        source_range: args.source_range,
76    }];
77
78    let res = inner_is_equal_body_type(argument, exec_state, args, BodyType::Surface).await?;
79    Ok(KclValue::Bool { value: res, meta })
80}
81
82async fn inner_is_equal_body_type(
83    surface: Solid,
84    exec_state: &mut ExecState,
85    args: Args,
86    expected: BodyType,
87) -> Result<bool, KclError> {
88    let meta = ModelingCmdMeta::from_args(exec_state, &args);
89    let cmd = ModelingCmd::from(mcmd::Solid3dGetBodyType::builder().object_id(surface.id).build());
90
91    let response = exec_state.send_modeling_cmd(meta, cmd).await?;
92
93    let OkWebSocketResponseData::Modeling {
94        modeling_response: OkModelingCmdResponse::Solid3dGetBodyType(body),
95    } = response
96    else {
97        return Err(KclError::new_semantic(KclErrorDetails::new(
98            format!(
99                "Engine returned invalid response, it should have returned Solid3dGetBodyType but it returned {response:#?}"
100            ),
101            vec![args.source_range],
102        )));
103    };
104
105    Ok(expected == body.body_type)
106}
107
108pub async fn delete_face(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
109    let body = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
110    let faces: Option<Vec<FaceTag>> = args.get_kw_arg_opt(
111        "faces",
112        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
113        exec_state,
114    )?;
115    let face_indices: Option<Vec<TyF64>> = args.get_kw_arg_opt(
116        "faceIndices",
117        &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Minimum(1)),
118        exec_state,
119    )?;
120    let face_indices = if let Some(face_indices) = face_indices {
121        let faces = face_indices
122            .into_iter()
123            .map(|num| {
124                crate::try_f64_to_u32(num.n).ok_or_else(|| {
125                    KclError::new_semantic(KclErrorDetails::new(
126                        format!("Face indices must be whole numbers, got {}", num.n),
127                        vec![args.source_range],
128                    ))
129                })
130            })
131            .collect::<Result<Vec<_>, _>>()?;
132        Some(faces)
133    } else {
134        None
135    };
136    inner_delete_face(body, faces, face_indices, exec_state, args)
137        .await
138        .map(Box::new)
139        .map(|value| KclValue::Solid { value })
140}
141
142async fn inner_delete_face(
143    body: Solid,
144    tagged_faces: Option<Vec<FaceTag>>,
145    face_indices: Option<Vec<u32>>,
146    exec_state: &mut ExecState,
147    args: Args,
148) -> Result<Solid, KclError> {
149    // Validate args:
150    // User has to give us SOMETHING to delete.
151    if tagged_faces.is_none() && face_indices.is_none() {
152        return Err(KclError::new_semantic(KclErrorDetails::new(
153            "You must use either the `faces` or the `faceIndices` parameter".to_string(),
154            vec![args.source_range],
155        )));
156    }
157
158    // Early return for mock response, just return the same solid.
159    // If we tracked faces, we would remove some faces... but we don't really.
160    let no_engine_commands = args.ctx.no_engine_commands().await;
161    if no_engine_commands {
162        return Ok(body);
163    }
164
165    // Chamfers and fillets are batched until the end of the file so they do not
166    // invalidate source edge IDs too early. If deleteFace targets one of those
167    // generated faces, the edge cut must be flushed before the delete command
168    // references it.
169    exec_state
170        .flush_batch_for_solids(
171            ModelingCmdMeta::from_args(exec_state, &args),
172            std::slice::from_ref(&body),
173        )
174        .await?;
175
176    // Combine the list of faces, both tagged and indexed.
177    let tagged_faces = tagged_faces.unwrap_or_default();
178    let face_indices = face_indices.unwrap_or_default();
179    // Get the face's ID
180    let mut face_ids = HashSet::with_capacity(face_indices.len() + tagged_faces.len());
181
182    for tagged_face in tagged_faces {
183        let face_id = tagged_face.get_face_id(&body, exec_state, &args, false).await?;
184        face_ids.insert(face_id);
185    }
186
187    for face_index in face_indices {
188        let face_uuid_response = exec_state
189            .send_modeling_cmd(
190                ModelingCmdMeta::from_args(exec_state, &args),
191                ModelingCmd::from(
192                    mcmd::Solid3dGetFaceUuid::builder()
193                        .object_id(body.id)
194                        .face_index(face_index)
195                        .build(),
196                ),
197            )
198            .await?;
199
200        let OkWebSocketResponseData::Modeling {
201            modeling_response: OkModelingCmdResponse::Solid3dGetFaceUuid(inner_resp),
202        } = face_uuid_response
203        else {
204            return Err(KclError::new_semantic(KclErrorDetails::new(
205                format!(
206                    "Engine returned invalid response, it should have returned Solid3dGetFaceUuid but it returned {face_uuid_response:?}"
207                ),
208                vec![args.source_range],
209            )));
210        };
211        face_ids.insert(inner_resp.face_id);
212    }
213
214    // Now that we've got all the faces, delete them all.
215    let delete_face_response = exec_state
216        .send_modeling_cmd(
217            ModelingCmdMeta::from_args(exec_state, &args),
218            ModelingCmd::from(
219                mcmd::EntityDeleteChildren::builder()
220                    .entity_id(body.id)
221                    .child_entity_ids(face_ids)
222                    .build(),
223            ),
224        )
225        .await?;
226
227    let OkWebSocketResponseData::Modeling {
228        modeling_response: OkModelingCmdResponse::EntityDeleteChildren(mout::EntityDeleteChildren { .. }),
229    } = delete_face_response
230    else {
231        return Err(KclError::new_semantic(KclErrorDetails::new(
232            format!(
233                "Engine returned invalid response, it should have returned EntityDeleteChildren but it returned {delete_face_response:?}"
234            ),
235            vec![args.source_range],
236        )));
237    };
238
239    // Return the same body, it just has fewer faces.
240    Ok(body)
241}
242
243/// Create a new surface that blends between two edges of separate surface bodies
244pub async fn blend(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
245    let edges: Vec<KclValue> = args.get_unlabeled_kw_arg(
246        "edges",
247        &RuntimeType::Array(
248            Box::new(RuntimeType::Union(vec![
249                RuntimeType::Primitive(PrimitiveType::BoundedEdge),
250                RuntimeType::tagged_edge(),
251                RuntimeType::Primitive(PrimitiveType::Any),
252            ])),
253            ArrayLen::Known(2),
254        ),
255        exec_state,
256    )?;
257
258    let mut bounded_edges = Vec::with_capacity(edges.len());
259    for edge in edges {
260        bounded_edges.push(resolve_blend_edge(edge, exec_state, &args).await?);
261    }
262
263    inner_blend(bounded_edges, exec_state, args.clone())
264        .await
265        .map(Box::new)
266        .map(|value| KclValue::Solid { value })
267}
268
269/// When edge specifiers are used, the first face in `sideFaces` is used in the
270/// [`BoundedEdge`] struct.
271async fn resolve_blend_edge(edge: KclValue, exec_state: &mut ExecState, args: &Args) -> Result<BoundedEdge, KclError> {
272    match edge {
273        KclValue::BoundedEdge { value, .. } => Ok(value),
274        KclValue::TagIdentifier(tag) => {
275            let tagged_edge = args.get_tag_engine_info(exec_state, &tag)?;
276            Ok(BoundedEdge {
277                face_id: tagged_edge.geometry.id(),
278                edge_id: Some(tagged_edge.id),
279                edge_specifier: None,
280                lower_bound: 0.0,
281                upper_bound: 1.0,
282            })
283        }
284        KclValue::Object { value: obj, .. } => {
285            let spec = edge::parse_edge_specifier_object(&obj, args)?;
286            let face_id = edge::face_id_from_first_side_face(&spec, exec_state, args)?;
287            Ok(BoundedEdge {
288                face_id,
289                edge_id: None,
290                edge_specifier: Some(spec),
291                lower_bound: 0.0,
292                upper_bound: 1.0,
293            })
294        }
295        _ => Err(KclError::new_internal(KclErrorDetails::new(
296            "Unexpected edge value while preparing blend edges. Expected BoundedEdge, tagged edge, or edge specifier object (e.g. { sideFaces = [...], endFaces = [...], index = 0 }).".to_owned(),
297            vec![args.source_range],
298        ))),
299    }
300}
301
302async fn inner_blend(edges: Vec<BoundedEdge>, exec_state: &mut ExecState, args: Args) -> Result<Solid, KclError> {
303    let id = exec_state.next_uuid();
304
305    let mut surface_refs = Vec::with_capacity(edges.len());
306    for edge in &edges {
307        let fraction = if let Some(eid) = edge.edge_id {
308            FractionOfEdge::builder()
309                .edge_id(eid)
310                .lower_bound(edge.lower_bound)
311                .upper_bound(edge.upper_bound)
312                .build()
313        } else if let Some(ref spec) = edge.edge_specifier {
314            let resolved = edge::resolve_unresolved_edge_specifier(edge.face_id, spec, exec_state, &args).await?;
315            FractionOfEdge::builder()
316                .edge_specifier(resolved)
317                .lower_bound(edge.lower_bound)
318                .upper_bound(edge.upper_bound)
319                .build()
320        } else {
321            return Err(KclError::new_internal(KclErrorDetails::new(
322                "BoundedEdge must have edge_id or edge_specifier".to_owned(),
323                vec![args.source_range],
324            )));
325        };
326        surface_refs.push(
327            SurfaceEdgeReference::builder()
328                .object_id(edge.face_id)
329                .edges(vec![fraction])
330                .build(),
331        );
332    }
333
334    exec_state
335        .batch_modeling_cmd(
336            ModelingCmdMeta::from_args_id(exec_state, &args, id),
337            ModelingCmd::from(mcmd::SurfaceBlend::builder().surfaces(surface_refs).build()),
338        )
339        .await?;
340
341    let solid = Solid {
342        id,
343        value_id: id,
344        topology_id: id,
345        pattern_source_artifact_id: None,
346        artifact_id: id.into(),
347        value: vec![],
348        faces: Default::default(),
349        creator: SolidCreator::Procedural,
350        start_cap_id: None,
351        end_cap_id: None,
352        edge_cuts: vec![],
353        pending_edge_cut_ids: vec![],
354        units: exec_state.length_unit(),
355        sectional: false,
356        meta: vec![crate::execution::Metadata {
357            source_range: args.source_range,
358        }],
359    };
360    //TODO: How do we pass back the two new edge ids that were created?
361    Ok(solid)
362}
363
364/// Stitch multiple surfaces together into one polysurface
365pub async fn join(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
366    let selection: Vec<Solid> = args.get_unlabeled_kw_arg("selection", &RuntimeType::solids(), exec_state)?;
367    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
368
369    inner_join(selection, tolerance, exec_state, args)
370        .await
371        .map(Box::new)
372        .map(|value| KclValue::Solid { value })
373}
374
375async fn inner_join(
376    selection: Vec<Solid>,
377    tolerance: Option<TyF64>,
378    exec_state: &mut ExecState,
379    args: Args,
380) -> Result<Solid, KclError> {
381    if selection.len() == 1 {
382        let cmd = mcmd::Solid3dJoin::builder().object_id(selection[0].id).build();
383
384        exec_state
385            .batch_modeling_cmd(ModelingCmdMeta::from_args(exec_state, &args), ModelingCmd::from(cmd))
386            .await?;
387
388        Ok(selection[0].clone())
389    } else {
390        let body_out_id = exec_state.next_uuid();
391
392        exec_state
393            .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &selection)
394            .await?;
395
396        let body_ids = selection.iter().map(|body| body.id).collect();
397        let tolerance = tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM);
398        let cmd = mcmd::Solid3dMultiJoin::builder()
399            .object_ids(body_ids)
400            .tolerance(LengthUnit(tolerance))
401            .build();
402
403        exec_state
404            .batch_modeling_cmd(
405                ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
406                ModelingCmd::from(cmd),
407            )
408            .await?;
409
410        let solid = Solid {
411            id: body_out_id,
412            value_id: body_out_id,
413            topology_id: body_out_id,
414            pattern_source_artifact_id: None,
415            artifact_id: body_out_id.into(),
416            value: vec![],
417            faces: Default::default(),
418            creator: SolidCreator::Procedural,
419            start_cap_id: None,
420            end_cap_id: None,
421            edge_cuts: vec![],
422            pending_edge_cut_ids: vec![],
423            units: exec_state.length_unit(),
424            sectional: false,
425            meta: vec![args.source_range.into()],
426        };
427        record_consumed_solids(
428            exec_state,
429            &selection,
430            ConsumedSolidOperation::JoinSurfaces,
431            std::slice::from_ref(&solid),
432        );
433        Ok(solid)
434    }
435}