kcl_lib/std/
planes.rs

1//! Standard library plane helpers.
2
3use kcmc::{ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, shared::Color};
4use kittycad_modeling_cmds::{self as kcmc, ok_response::OkModelingCmdResponse, websocket::OkWebSocketResponseData};
5
6use super::{
7    args::TyF64,
8    sketch::{FaceTag, PlaneData},
9};
10use crate::{
11    UnitLen,
12    errors::{KclError, KclErrorDetails},
13    execution::{ExecState, KclValue, Metadata, ModelingCmdMeta, Plane, PlaneType, types::RuntimeType},
14    std::Args,
15};
16
17/// Find the plane of a given face.
18pub async fn plane_of(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
19    let solid = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
20    let face = args.get_kw_arg("face", &RuntimeType::tagged_face(), exec_state)?;
21
22    inner_plane_of(solid, face, exec_state, &args)
23        .await
24        .map(Box::new)
25        .map(|value| KclValue::Plane { value })
26}
27
28async fn inner_plane_of(
29    solid: crate::execution::Solid,
30    face: FaceTag,
31    exec_state: &mut ExecState,
32    args: &Args,
33) -> Result<Plane, KclError> {
34    // Support mock execution
35    // Return an arbitrary (incorrect) plane and a non-fatal error.
36    if args.ctx.no_engine_commands().await {
37        let plane_id = exec_state.id_generator().next_uuid();
38        exec_state.err(crate::CompilationError {
39            source_range: args.source_range,
40            message: "The engine isn't available, so returning an arbitrary incorrect plane".to_owned(),
41            suggestion: None,
42            severity: crate::errors::Severity::Error,
43            tag: crate::errors::Tag::None,
44        });
45        return Ok(Plane {
46            artifact_id: plane_id.into(),
47            id: plane_id,
48            // Engine doesn't know about the ID we created, so set this to Uninit.
49            value: PlaneType::Uninit,
50            info: crate::execution::PlaneInfo {
51                origin: Default::default(),
52                x_axis: Default::default(),
53                y_axis: Default::default(),
54            },
55            meta: vec![Metadata {
56                source_range: args.source_range,
57            }],
58        });
59    }
60
61    // Query the engine to learn what plane, if any, this face is on.
62    let face_id = face.get_face_id(&solid, exec_state, args, true).await?;
63    let meta = args.into();
64    let cmd = ModelingCmd::FaceIsPlanar(mcmd::FaceIsPlanar { object_id: face_id });
65    let plane_resp = exec_state.send_modeling_cmd(meta, cmd).await?;
66    let OkWebSocketResponseData::Modeling {
67        modeling_response: OkModelingCmdResponse::FaceIsPlanar(planar),
68    } = plane_resp
69    else {
70        return Err(KclError::new_semantic(KclErrorDetails::new(
71            format!(
72                "Engine returned invalid response, it should have returned FaceIsPlanar but it returned {plane_resp:#?}"
73            ),
74            vec![args.source_range],
75        )));
76    };
77    // Destructure engine's response to check if the face was on a plane.
78    let not_planar: Result<_, KclError> = Err(KclError::new_semantic(KclErrorDetails::new(
79        "The face you provided doesn't lie on any plane. It might be curved.".to_owned(),
80        vec![args.source_range],
81    )));
82    let Some(x_axis) = planar.x_axis else { return not_planar };
83    let Some(y_axis) = planar.y_axis else { return not_planar };
84    let Some(origin) = planar.origin else { return not_planar };
85
86    // Engine always returns measurements in mm.
87    let engine_units = UnitLen::Mm;
88    let x_axis = crate::execution::Point3d {
89        x: x_axis.x,
90        y: x_axis.y,
91        z: x_axis.z,
92        units: engine_units,
93    };
94    let y_axis = crate::execution::Point3d {
95        x: y_axis.x,
96        y: y_axis.y,
97        z: y_axis.z,
98        units: engine_units,
99    };
100    let origin = crate::execution::Point3d {
101        x: origin.x.0,
102        y: origin.y.0,
103        z: origin.z.0,
104        units: engine_units,
105    };
106
107    // Engine doesn't send back an ID, so let's just make a new plane ID.
108    let plane_id = exec_state.id_generator().next_uuid();
109    Ok(Plane {
110        artifact_id: plane_id.into(),
111        id: plane_id,
112        // Engine doesn't know about the ID we created, so set this to Uninit.
113        value: PlaneType::Uninit,
114        info: crate::execution::PlaneInfo { origin, x_axis, y_axis },
115        meta: vec![Metadata {
116            source_range: args.source_range,
117        }],
118    })
119}
120
121/// Offset a plane by a distance along its normal.
122pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
123    let std_plane = args.get_unlabeled_kw_arg("plane", &RuntimeType::plane(), exec_state)?;
124    let offset: TyF64 = args.get_kw_arg("offset", &RuntimeType::length(), exec_state)?;
125    let plane = inner_offset_plane(std_plane, offset, exec_state, &args).await?;
126    Ok(KclValue::Plane { value: Box::new(plane) })
127}
128
129async fn inner_offset_plane(
130    plane: PlaneData,
131    offset: TyF64,
132    exec_state: &mut ExecState,
133    args: &Args,
134) -> Result<Plane, KclError> {
135    let mut plane = Plane::from_plane_data(plane, exec_state)?;
136    // Though offset planes might be derived from standard planes, they are not
137    // standard planes themselves.
138    plane.value = PlaneType::Custom;
139
140    let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
141    plane.info.origin += normal * offset.to_length_units(plane.info.origin.units);
142    make_offset_plane_in_engine(&plane, exec_state, args).await?;
143
144    Ok(plane)
145}
146
147// Engine-side effectful creation of an actual plane object.
148// offset planes are shown by default, and hidden by default if they
149// are used as a sketch plane. That hiding command is sent within inner_start_profile_at
150async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> {
151    // Create new default planes.
152    let default_size = 100.0;
153    let color = Color {
154        r: 0.6,
155        g: 0.6,
156        b: 0.6,
157        a: 0.3,
158    };
159
160    let meta = ModelingCmdMeta::from_args_id(args, plane.id);
161    exec_state
162        .batch_modeling_cmd(
163            meta,
164            ModelingCmd::from(mcmd::MakePlane {
165                clobber: false,
166                origin: plane.info.origin.into(),
167                size: LengthUnit(default_size),
168                x_axis: plane.info.x_axis.into(),
169                y_axis: plane.info.y_axis.into(),
170                hide: Some(false),
171            }),
172        )
173        .await?;
174
175    // Set the color.
176    exec_state
177        .batch_modeling_cmd(
178            args.into(),
179            ModelingCmd::from(mcmd::PlaneSetColor {
180                color,
181                plane_id: plane.id,
182            }),
183        )
184        .await?;
185
186    Ok(())
187}