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, each_cmd as mcmd};
7use kittycad_modeling_cmds::{
8    self as kcmc,
9    ok_response::OkModelingCmdResponse,
10    output as mout,
11    shared::{BodyType, FractionOfEdge, SurfaceEdgeReference},
12    websocket::OkWebSocketResponseData,
13};
14
15use crate::{
16    errors::{KclError, KclErrorDetails},
17    execution::{
18        BoundedEdge, ExecState, KclValue, ModelingCmdMeta, Solid, SolidCreator,
19        types::{ArrayLen, PrimitiveType, RuntimeType},
20    },
21    std::{Args, args::TyF64, sketch::FaceTag},
22};
23
24/// Flips the orientation of a surface, swapping which side is the front and which is the reverse.
25pub async fn flip_surface(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
26    let surface = args.get_unlabeled_kw_arg("surface", &RuntimeType::solids(), exec_state)?;
27    let out = inner_flip_surface(surface, exec_state, args).await?;
28    Ok(out.into())
29}
30
31async fn inner_flip_surface(
32    surfaces: Vec<Solid>,
33    exec_state: &mut ExecState,
34    args: Args,
35) -> Result<Vec<Solid>, KclError> {
36    for surface in &surfaces {
37        exec_state
38            .batch_modeling_cmd(
39                ModelingCmdMeta::from_args(exec_state, &args),
40                ModelingCmd::from(mcmd::Solid3dFlip::builder().object_id(surface.id).build()),
41            )
42            .await?;
43    }
44
45    Ok(surfaces)
46}
47
48/// Check if this object is a solid or not.
49pub async fn is_solid(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
50    let argument = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
51    let meta = vec![crate::execution::Metadata {
52        source_range: args.source_range,
53    }];
54
55    let res = inner_is_equal_body_type(argument, exec_state, args, BodyType::Solid).await?;
56    Ok(KclValue::Bool { value: res, meta })
57}
58
59/// Check if this object is a surface or not.
60pub async fn is_surface(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::Surface).await?;
67    Ok(KclValue::Bool { value: res, meta })
68}
69
70async fn inner_is_equal_body_type(
71    surface: Solid,
72    exec_state: &mut ExecState,
73    args: Args,
74    expected: BodyType,
75) -> Result<bool, KclError> {
76    let meta = ModelingCmdMeta::from_args(exec_state, &args);
77    let cmd = ModelingCmd::from(mcmd::Solid3dGetBodyType::builder().object_id(surface.id).build());
78
79    let response = exec_state.send_modeling_cmd(meta, cmd).await?;
80
81    let OkWebSocketResponseData::Modeling {
82        modeling_response: OkModelingCmdResponse::Solid3dGetBodyType(body),
83    } = response
84    else {
85        return Err(KclError::new_semantic(KclErrorDetails::new(
86            format!(
87                "Engine returned invalid response, it should have returned Solid3dGetBodyType but it returned {response:#?}"
88            ),
89            vec![args.source_range],
90        )));
91    };
92
93    Ok(expected == body.body_type)
94}
95
96pub async fn delete_face(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
97    let body = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
98    let faces: Option<Vec<FaceTag>> = args.get_kw_arg_opt(
99        "faces",
100        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
101        exec_state,
102    )?;
103    let face_indices: Option<Vec<TyF64>> = args.get_kw_arg_opt(
104        "faceIndices",
105        &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Minimum(1)),
106        exec_state,
107    )?;
108    let face_indices = if let Some(face_indices) = face_indices {
109        let faces = face_indices
110            .into_iter()
111            .map(|num| {
112                crate::try_f64_to_u32(num.n).ok_or_else(|| {
113                    KclError::new_semantic(KclErrorDetails::new(
114                        format!("Face indices must be whole numbers, got {}", num.n),
115                        vec![args.source_range],
116                    ))
117                })
118            })
119            .collect::<Result<Vec<_>, _>>()?;
120        Some(faces)
121    } else {
122        None
123    };
124    inner_delete_face(body, faces, face_indices, exec_state, args)
125        .await
126        .map(Box::new)
127        .map(|value| KclValue::Solid { value })
128}
129
130async fn inner_delete_face(
131    body: Solid,
132    tagged_faces: Option<Vec<FaceTag>>,
133    face_indices: Option<Vec<u32>>,
134    exec_state: &mut ExecState,
135    args: Args,
136) -> Result<Solid, KclError> {
137    // Validate args:
138    // User has to give us SOMETHING to delete.
139    if tagged_faces.is_none() && face_indices.is_none() {
140        return Err(KclError::new_semantic(KclErrorDetails::new(
141            "You must use either the `faces` or the `faceIndices` parameter".to_string(),
142            vec![args.source_range],
143        )));
144    }
145
146    // Early return for mock response, just return the same solid.
147    // If we tracked faces, we would remove some faces... but we don't really.
148    let no_engine_commands = args.ctx.no_engine_commands().await;
149    if no_engine_commands {
150        return Ok(body);
151    }
152
153    // Combine the list of faces, both tagged and indexed.
154    let tagged_faces = tagged_faces.unwrap_or_default();
155    let face_indices = face_indices.unwrap_or_default();
156    // Get the face's ID
157    let mut face_ids = HashSet::with_capacity(face_indices.len() + tagged_faces.len());
158
159    for tagged_face in tagged_faces {
160        let face_id = tagged_face.get_face_id(&body, exec_state, &args, false).await?;
161        face_ids.insert(face_id);
162    }
163
164    for face_index in face_indices {
165        let face_uuid_response = exec_state
166            .send_modeling_cmd(
167                ModelingCmdMeta::from_args(exec_state, &args),
168                ModelingCmd::from(
169                    mcmd::Solid3dGetFaceUuid::builder()
170                        .object_id(body.id)
171                        .face_index(face_index)
172                        .build(),
173                ),
174            )
175            .await?;
176
177        let OkWebSocketResponseData::Modeling {
178            modeling_response: OkModelingCmdResponse::Solid3dGetFaceUuid(inner_resp),
179        } = face_uuid_response
180        else {
181            return Err(KclError::new_semantic(KclErrorDetails::new(
182                format!(
183                    "Engine returned invalid response, it should have returned Solid3dGetFaceUuid but it returned {face_uuid_response:?}"
184                ),
185                vec![args.source_range],
186            )));
187        };
188        face_ids.insert(inner_resp.face_id);
189    }
190
191    // Now that we've got all the faces, delete them all.
192    let delete_face_response = exec_state
193        .send_modeling_cmd(
194            ModelingCmdMeta::from_args(exec_state, &args),
195            ModelingCmd::from(
196                mcmd::EntityDeleteChildren::builder()
197                    .entity_id(body.id)
198                    .child_entity_ids(face_ids)
199                    .build(),
200            ),
201        )
202        .await?;
203
204    let OkWebSocketResponseData::Modeling {
205        modeling_response: OkModelingCmdResponse::EntityDeleteChildren(mout::EntityDeleteChildren { .. }),
206    } = delete_face_response
207    else {
208        return Err(KclError::new_semantic(KclErrorDetails::new(
209            format!(
210                "Engine returned invalid response, it should have returned EntityDeleteChildren but it returned {delete_face_response:?}"
211            ),
212            vec![args.source_range],
213        )));
214    };
215
216    // Return the same body, it just has fewer faces.
217    Ok(body)
218}
219
220/// Create a new surface that blends between two edges of separate surface bodies
221pub async fn blend(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
222    let bounded_edges = args.get_unlabeled_kw_arg(
223        "edges",
224        &RuntimeType::Array(
225            Box::new(RuntimeType::Primitive(PrimitiveType::BoundedEdge)),
226            ArrayLen::Known(2),
227        ),
228        exec_state,
229    )?;
230
231    inner_blend(bounded_edges, exec_state, args.clone())
232        .await
233        .map(Box::new)
234        .map(|value| KclValue::Solid { value })
235}
236
237async fn inner_blend(edges: Vec<BoundedEdge>, exec_state: &mut ExecState, args: Args) -> Result<Solid, KclError> {
238    let id = exec_state.next_uuid();
239
240    let surface_refs: Vec<SurfaceEdgeReference> = edges
241        .iter()
242        .map(|edge| {
243            SurfaceEdgeReference::builder()
244                .object_id(edge.face_id)
245                .edges(vec![
246                    FractionOfEdge::builder()
247                        .edge_id(edge.edge_id)
248                        .lower_bound(edge.lower_bound)
249                        .upper_bound(edge.upper_bound)
250                        .build(),
251                ])
252                .build()
253        })
254        .collect();
255
256    exec_state
257        .batch_modeling_cmd(
258            ModelingCmdMeta::from_args_id(exec_state, &args, id),
259            ModelingCmd::from(mcmd::SurfaceBlend::builder().surfaces(surface_refs).build()),
260        )
261        .await?;
262
263    let solid = Solid {
264        id,
265        artifact_id: id.into(),
266        value: vec![],
267        creator: SolidCreator::Procedural,
268        start_cap_id: None,
269        end_cap_id: None,
270        edge_cuts: vec![],
271        units: exec_state.length_unit(),
272        sectional: false,
273        meta: vec![crate::execution::Metadata {
274            source_range: args.source_range,
275        }],
276    };
277    //TODO: How do we pass back the two new edge ids that were created?
278    Ok(solid)
279}