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