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