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    if args.ctx.no_engine_commands().await {
89        // In mock execution, we can't query the surface type and know it for real.
90        // So just give a best-effort attempt to figure out the surface/solid type.
91        return if let Some(body_type) = surface.best_guess_body_type {
92            Ok(expected == body_type)
93        } else {
94            // No body type known, so we don't know whether this is true or false.
95            // Best effort guess is false.
96            // Hopefully shouldn't matter to mock execution.
97            Ok(false)
98        };
99    }
100
101    Ok(expected == query_body_type(&surface, exec_state, &args).await?)
102}
103
104pub(crate) async fn query_body_type(
105    surface: &Solid,
106    exec_state: &mut ExecState,
107    args: &Args,
108) -> Result<BodyType, KclError> {
109    let meta = ModelingCmdMeta::from_args(exec_state, args);
110    let cmd = ModelingCmd::from(mcmd::Solid3dGetBodyType::builder().object_id(surface.id).build());
111
112    let response = exec_state.send_modeling_cmd(meta, cmd).await?;
113
114    let OkWebSocketResponseData::Modeling {
115        modeling_response: OkModelingCmdResponse::Solid3dGetBodyType(body),
116    } = response
117    else {
118        return Err(KclError::new_semantic(KclErrorDetails::new(
119            format!(
120                "Engine returned invalid response, it should have returned Solid3dGetBodyType but it returned {response:#?}"
121            ),
122            vec![args.source_range],
123        )));
124    };
125
126    Ok(body.body_type)
127}
128
129pub async fn delete_face(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
130    let body = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
131    let faces: Option<Vec<FaceTag>> = args.get_kw_arg_opt(
132        "faces",
133        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
134        exec_state,
135    )?;
136    let face_indices: Option<Vec<TyF64>> = args.get_kw_arg_opt(
137        "faceIndices",
138        &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Minimum(1)),
139        exec_state,
140    )?;
141    let face_indices = if let Some(face_indices) = face_indices {
142        let faces = face_indices
143            .into_iter()
144            .map(|num| {
145                crate::try_f64_to_u32(num.n).ok_or_else(|| {
146                    KclError::new_semantic(KclErrorDetails::new(
147                        format!("Face indices must be whole numbers, got {}", num.n),
148                        vec![args.source_range],
149                    ))
150                })
151            })
152            .collect::<Result<Vec<_>, _>>()?;
153        Some(faces)
154    } else {
155        None
156    };
157    inner_delete_face(body, faces, face_indices, exec_state, args)
158        .await
159        .map(Box::new)
160        .map(|value| KclValue::Solid { value })
161}
162
163async fn inner_delete_face(
164    mut body: Solid,
165    tagged_faces: Option<Vec<FaceTag>>,
166    face_indices: Option<Vec<u32>>,
167    exec_state: &mut ExecState,
168    args: Args,
169) -> Result<Solid, KclError> {
170    // Validate args:
171    // User has to give us SOMETHING to delete.
172    if tagged_faces.is_none() && face_indices.is_none() {
173        return Err(KclError::new_semantic(KclErrorDetails::new(
174            "You must use either the `faces` or the `faceIndices` parameter".to_string(),
175            vec![args.source_range],
176        )));
177    }
178
179    // Early return for mock response, just return the same solid.
180    // If we tracked faces, we would remove some faces... but we don't really.
181    let no_engine_commands = args.ctx.no_engine_commands().await;
182    if no_engine_commands {
183        return Ok(body);
184    }
185
186    // Chamfers and fillets are batched until the end of the file so they do not
187    // invalidate source edge IDs too early. If deleteFace targets one of those
188    // generated faces, the edge cut must be flushed before the delete command
189    // references it.
190    exec_state
191        .flush_batch_for_solids(
192            ModelingCmdMeta::from_args(exec_state, &args),
193            std::slice::from_ref(&body),
194        )
195        .await?;
196
197    // Combine the list of faces, both tagged and indexed.
198    let tagged_faces = tagged_faces.unwrap_or_default();
199    let face_indices = face_indices.unwrap_or_default();
200    // Get the face's ID
201    let mut face_ids = HashSet::with_capacity(face_indices.len() + tagged_faces.len());
202
203    for tagged_face in tagged_faces {
204        let face_id = tagged_face.get_face_id(&body, exec_state, &args, false).await?;
205        face_ids.insert(face_id);
206    }
207
208    for face_index in face_indices {
209        let face_uuid_response = exec_state
210            .send_modeling_cmd(
211                ModelingCmdMeta::from_args(exec_state, &args),
212                ModelingCmd::from(
213                    mcmd::Solid3dGetFaceUuid::builder()
214                        .object_id(body.id)
215                        .face_index(face_index)
216                        .build(),
217                ),
218            )
219            .await?;
220
221        let OkWebSocketResponseData::Modeling {
222            modeling_response: OkModelingCmdResponse::Solid3dGetFaceUuid(inner_resp),
223        } = face_uuid_response
224        else {
225            return Err(KclError::new_semantic(KclErrorDetails::new(
226                format!(
227                    "Engine returned invalid response, it should have returned Solid3dGetFaceUuid but it returned {face_uuid_response:?}"
228                ),
229                vec![args.source_range],
230            )));
231        };
232        face_ids.insert(inner_resp.face_id);
233    }
234
235    // Now that we've got all the faces, delete them all.
236    let delete_face_response = exec_state
237        .send_modeling_cmd(
238            ModelingCmdMeta::from_args(exec_state, &args),
239            ModelingCmd::from(
240                mcmd::EntityDeleteChildren::builder()
241                    .entity_id(body.id)
242                    .child_entity_ids(face_ids)
243                    .build(),
244            ),
245        )
246        .await?;
247
248    let OkWebSocketResponseData::Modeling {
249        modeling_response: OkModelingCmdResponse::EntityDeleteChildren(mout::EntityDeleteChildren { .. }),
250    } = delete_face_response
251    else {
252        return Err(KclError::new_semantic(KclErrorDetails::new(
253            format!(
254                "Engine returned invalid response, it should have returned EntityDeleteChildren but it returned {delete_face_response:?}"
255            ),
256            vec![args.source_range],
257        )));
258    };
259
260    // Return the same body, it just has fewer faces.
261    // And it's _probably_ a polysurface now, because if it was a solid before,
262    // it's _probably_ a surface after some required face was deleted and the volume
263    // is no longer closed. If it was a surface before, it's still a surface.
264    body.best_guess_body_type = Some(BodyType::Surface);
265    Ok(body)
266}
267
268/// Create a new surface that blends between two edges of separate surface bodies
269pub async fn blend(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
270    let edges: Vec<KclValue> = args.get_unlabeled_kw_arg(
271        "edges",
272        &RuntimeType::Array(
273            Box::new(RuntimeType::Union(vec![
274                RuntimeType::Primitive(PrimitiveType::BoundedEdge),
275                RuntimeType::tagged_edge(),
276                RuntimeType::Primitive(PrimitiveType::Any),
277            ])),
278            ArrayLen::Known(2),
279        ),
280        exec_state,
281    )?;
282
283    let mut bounded_edges = Vec::with_capacity(edges.len());
284    for edge in edges {
285        bounded_edges.push(resolve_blend_edge(edge, exec_state, &args).await?);
286    }
287
288    inner_blend(bounded_edges, exec_state, args.clone())
289        .await
290        .map(Box::new)
291        .map(|value| KclValue::Solid { value })
292}
293
294/// When edge specifiers are used, the first face in `sideFaces` is used in the
295/// [`BoundedEdge`] struct.
296async fn resolve_blend_edge(edge: KclValue, exec_state: &mut ExecState, args: &Args) -> Result<BoundedEdge, KclError> {
297    match edge {
298        KclValue::BoundedEdge { value, .. } => Ok(value),
299        KclValue::TagIdentifier(tag) => {
300            let tagged_edge = args.get_tag_engine_info(exec_state, &tag)?;
301            Ok(BoundedEdge {
302                face_id: tagged_edge.geometry.id(),
303                edge_id: Some(tagged_edge.id),
304                edge_specifier: None,
305                lower_bound: 0.0,
306                upper_bound: 1.0,
307            })
308        }
309        KclValue::Object { value: obj, .. } => {
310            let spec = edge::parse_edge_specifier_object(&obj, args)?;
311            let face_id = edge::face_id_from_first_side_face(&spec, exec_state, args)?;
312            Ok(BoundedEdge {
313                face_id,
314                edge_id: None,
315                edge_specifier: Some(spec),
316                lower_bound: 0.0,
317                upper_bound: 1.0,
318            })
319        }
320        _ => Err(KclError::new_internal(KclErrorDetails::new(
321            "Unexpected edge value while preparing blend edges. Expected BoundedEdge, tagged edge, or edge specifier object (e.g. { sideFaces = [...], endFaces = [...], index = 0 }).".to_owned(),
322            vec![args.source_range],
323        ))),
324    }
325}
326
327async fn inner_blend(edges: Vec<BoundedEdge>, exec_state: &mut ExecState, args: Args) -> Result<Solid, KclError> {
328    let id = exec_state.next_uuid();
329
330    let mut surface_refs = Vec::with_capacity(edges.len());
331    for edge in &edges {
332        let fraction = if let Some(eid) = edge.edge_id {
333            FractionOfEdge::builder()
334                .edge_id(eid)
335                .lower_bound(edge.lower_bound)
336                .upper_bound(edge.upper_bound)
337                .build()
338        } else if let Some(ref spec) = edge.edge_specifier {
339            let resolved = edge::resolve_unresolved_edge_specifier(edge.face_id, spec, exec_state, &args).await?;
340            FractionOfEdge::builder()
341                .edge_specifier(resolved)
342                .lower_bound(edge.lower_bound)
343                .upper_bound(edge.upper_bound)
344                .build()
345        } else {
346            return Err(KclError::new_internal(KclErrorDetails::new(
347                "BoundedEdge must have edge_id or edge_specifier".to_owned(),
348                vec![args.source_range],
349            )));
350        };
351        surface_refs.push(
352            SurfaceEdgeReference::builder()
353                .object_id(edge.face_id)
354                .edges(vec![fraction])
355                .build(),
356        );
357    }
358
359    exec_state
360        .batch_modeling_cmd(
361            ModelingCmdMeta::from_args_id(exec_state, &args, id),
362            ModelingCmd::from(mcmd::SurfaceBlend::builder().surfaces(surface_refs).build()),
363        )
364        .await?;
365
366    let solid = Solid {
367        id,
368        value_id: id,
369        topology_id: id,
370        pattern_source_artifact_id: None,
371        best_guess_body_type: Some(BodyType::Surface),
372        artifact_id: id.into(),
373        value: vec![],
374        faces: Default::default(),
375        creator: SolidCreator::Procedural,
376        start_cap_id: None,
377        end_cap_id: None,
378        edge_cuts: vec![],
379        pending_edge_cut_ids: vec![],
380        units: exec_state.length_unit(),
381        sectional: false,
382        meta: vec![crate::execution::Metadata {
383            source_range: args.source_range,
384        }],
385    };
386    //TODO: How do we pass back the two new edge ids that were created?
387    Ok(solid)
388}
389
390/// Stitch multiple surfaces together into one polysurface
391pub async fn join(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
392    let selection: Vec<Solid> = args.get_unlabeled_kw_arg("selection", &RuntimeType::solids(), exec_state)?;
393    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
394
395    inner_join(selection, tolerance, exec_state, args)
396        .await
397        .map(Box::new)
398        .map(|value| KclValue::Solid { value })
399}
400
401async fn inner_join(
402    selection: Vec<Solid>,
403    tolerance: Option<TyF64>,
404    exec_state: &mut ExecState,
405    args: Args,
406) -> Result<Solid, KclError> {
407    if selection.len() == 1 {
408        let cmd = mcmd::Solid3dJoin::builder().object_id(selection[0].id).build();
409
410        exec_state
411            .batch_modeling_cmd(ModelingCmdMeta::from_args(exec_state, &args), ModelingCmd::from(cmd))
412            .await?;
413
414        Ok(selection[0].clone())
415    } else {
416        let body_out_id = exec_state.next_uuid();
417
418        exec_state
419            .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &selection)
420            .await?;
421
422        let body_ids = selection.iter().map(|body| body.id).collect();
423        let tolerance = tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM);
424        let cmd = mcmd::Solid3dMultiJoin::builder()
425            .object_ids(body_ids)
426            .tolerance(LengthUnit(tolerance))
427            .build();
428
429        exec_state
430            .batch_modeling_cmd(
431                ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
432                ModelingCmd::from(cmd),
433            )
434            .await?;
435
436        let solid = Solid {
437            id: body_out_id,
438            value_id: body_out_id,
439            topology_id: body_out_id,
440            pattern_source_artifact_id: None,
441            best_guess_body_type: None,
442            artifact_id: body_out_id.into(),
443            value: vec![],
444            faces: Default::default(),
445            creator: SolidCreator::Procedural,
446            start_cap_id: None,
447            end_cap_id: None,
448            edge_cuts: vec![],
449            pending_edge_cut_ids: vec![],
450            units: exec_state.length_unit(),
451            sectional: false,
452            meta: vec![args.source_range.into()],
453        };
454        record_consumed_solids(
455            exec_state,
456            &selection,
457            ConsumedSolidOperation::JoinSurfaces,
458            std::slice::from_ref(&solid),
459        );
460        Ok(solid)
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use crate::execution::MockConfig;
467
468    #[tokio::test(flavor = "multi_thread")]
469    async fn mock_body_type_queries_use_locally_known_type() {
470        let code = r#"
471@settings(defaultLengthUnit = mm, kclVersion = 2.0)
472
473solidSketch = sketch(on = XY) {
474  profile = circle(
475    start = [var 5mm, var 0mm],
476    center = [var 0mm, var 0mm],
477  )
478}
479solid = extrude(
480  region(segments = [solidSketch.profile]),
481  length = 5mm,
482  bodyType = "solid",
483)
484
485surfaceSketch = sketch(on = XY) {
486  profile = circle(
487    start = [var 20mm, var 0mm],
488    center = [var 15mm, var 0mm],
489  )
490}
491surface = extrude(
492  region(segments = [surfaceSketch.profile]),
493  length = 5mm,
494  bodyType = "surface",
495)
496
497assertIs(isSolid(solid))
498assertIs(!isSurface(solid))
499assertIs(isSurface(surface))
500assertIs(!isSolid(surface))
501"#;
502
503        let program = crate::Program::parse_no_errs(code).unwrap();
504        let ctx = crate::ExecutorContext::new_mock(None).await;
505        let result = ctx.run_mock(&program, &MockConfig::default()).await;
506        ctx.close().await;
507        result.unwrap();
508    }
509}