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::{
5    self as kcmc, ok_response::OkModelingCmdResponse, units::UnitLength, websocket::OkWebSocketResponseData,
6};
7
8use super::{
9    args::TyF64,
10    sketch::{FaceTag, PlaneData},
11};
12use crate::{
13    errors::{KclError, KclErrorDetails},
14    execution::{ExecState, KclValue, Metadata, ModelingCmdMeta, Plane, PlaneType, types::RuntimeType},
15    std::Args,
16};
17
18/// Find the plane of a given face.
19pub async fn plane_of(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
20    let solid = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
21    let face = args.get_kw_arg("face", &RuntimeType::tagged_face(), exec_state)?;
22
23    inner_plane_of(solid, face, exec_state, &args)
24        .await
25        .map(Box::new)
26        .map(|value| KclValue::Plane { value })
27}
28
29pub(crate) async fn inner_plane_of(
30    solid: crate::execution::Solid,
31    face: FaceTag,
32    exec_state: &mut ExecState,
33    args: &Args,
34) -> Result<Plane, KclError> {
35    let plane_id = exec_state.id_generator().next_uuid();
36
37    #[cfg(not(feature = "artifact-graph"))]
38    let plane_object_id = None;
39    #[cfg(feature = "artifact-graph")]
40    let plane_object_id = {
41        use crate::execution::ArtifactId;
42
43        let plane_object_id = exec_state.next_object_id();
44        let plane_object = crate::front::Object {
45            id: plane_object_id,
46            kind: crate::front::ObjectKind::Plane(crate::front::Plane::Object(plane_object_id)),
47            label: Default::default(),
48            comments: Default::default(),
49            artifact_id: ArtifactId::new(plane_id),
50            source: args.source_range.into(),
51        };
52        exec_state.add_scene_object(plane_object, args.source_range);
53        Some(plane_object_id)
54    };
55
56    // Support mock execution
57    // Return an arbitrary (incorrect) plane and a non-fatal error.
58    if args.ctx.no_engine_commands().await {
59        exec_state.err(crate::CompilationError {
60            source_range: args.source_range,
61            message: "The engine isn't available, so returning an arbitrary incorrect plane".to_owned(),
62            suggestion: None,
63            severity: crate::errors::Severity::Error,
64            tag: crate::errors::Tag::None,
65        });
66        return Ok(Plane {
67            artifact_id: plane_id.into(),
68            id: plane_id,
69            object_id: plane_object_id,
70            // Engine doesn't know about the ID we created, so set this to Uninit.
71            value: PlaneType::Uninit,
72            info: crate::execution::PlaneInfo {
73                origin: crate::execution::Point3d {
74                    x: 0.0,
75                    y: 0.0,
76                    z: 0.0,
77                    units: Some(UnitLength::Millimeters),
78                },
79                x_axis: crate::execution::Point3d {
80                    x: 1.0,
81                    y: 0.0,
82                    z: 0.0,
83                    units: None,
84                },
85                y_axis: crate::execution::Point3d {
86                    x: 0.0,
87                    y: 1.0,
88                    z: 0.0,
89                    units: None,
90                },
91                z_axis: crate::execution::Point3d {
92                    x: 0.0,
93                    y: 0.0,
94                    z: 1.0,
95                    units: None,
96                },
97            },
98            meta: vec![Metadata {
99                source_range: args.source_range,
100            }],
101        });
102    }
103
104    // Flush the batch for our fillets/chamfers if there are any.
105    exec_state
106        .flush_batch_for_solids(
107            ModelingCmdMeta::from_args(exec_state, args),
108            std::slice::from_ref(&solid),
109        )
110        .await?;
111
112    // Query the engine to learn what plane, if any, this face is on.
113    let face_id = face.get_face_id(&solid, exec_state, args, true).await?;
114    let meta = ModelingCmdMeta::from_args_id(exec_state, args, plane_id);
115    let cmd = ModelingCmd::FaceIsPlanar(mcmd::FaceIsPlanar { object_id: face_id });
116    let plane_resp = exec_state.send_modeling_cmd(meta, cmd).await?;
117    let OkWebSocketResponseData::Modeling {
118        modeling_response: OkModelingCmdResponse::FaceIsPlanar(planar),
119    } = plane_resp
120    else {
121        return Err(KclError::new_semantic(KclErrorDetails::new(
122            format!(
123                "Engine returned invalid response, it should have returned FaceIsPlanar but it returned {plane_resp:#?}"
124            ),
125            vec![args.source_range],
126        )));
127    };
128
129    // Destructure engine's response to check if the face was on a plane.
130    let not_planar: Result<_, KclError> = Err(KclError::new_semantic(KclErrorDetails::new(
131        "The face you provided doesn't lie on any plane. It might be curved.".to_owned(),
132        vec![args.source_range],
133    )));
134    let Some(x_axis) = planar.x_axis else { return not_planar };
135    let Some(y_axis) = planar.y_axis else { return not_planar };
136    let Some(z_axis) = planar.z_axis else { return not_planar };
137    let Some(origin) = planar.origin else { return not_planar };
138
139    // Engine always returns measurements in mm.
140    let engine_units = Some(UnitLength::Millimeters);
141    let x_axis = crate::execution::Point3d {
142        x: x_axis.x,
143        y: x_axis.y,
144        z: x_axis.z,
145        units: engine_units,
146    };
147    let y_axis = crate::execution::Point3d {
148        x: y_axis.x,
149        y: y_axis.y,
150        z: y_axis.z,
151        units: engine_units,
152    };
153    let z_axis = crate::execution::Point3d {
154        x: z_axis.x,
155        y: z_axis.y,
156        z: z_axis.z,
157        units: engine_units,
158    };
159    let origin = crate::execution::Point3d {
160        x: origin.x.0,
161        y: origin.y.0,
162        z: origin.z.0,
163        units: engine_units,
164    };
165
166    // Planes should always be right-handed, but due to an engine bug sometimes they're not.
167    // Test for right-handedness: cross(X,Y) is Z
168    let plane_info = crate::execution::PlaneInfo {
169        origin,
170        x_axis,
171        y_axis,
172        z_axis,
173    };
174    let plane_info = plane_info.make_right_handed();
175
176    Ok(Plane {
177        artifact_id: plane_id.into(),
178        id: plane_id,
179        object_id: plane_object_id,
180        value: PlaneType::Custom,
181        info: plane_info,
182        meta: vec![Metadata {
183            source_range: args.source_range,
184        }],
185    })
186}
187
188/// Offset a plane by a distance along its normal.
189pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
190    let std_plane = args.get_unlabeled_kw_arg("plane", &RuntimeType::plane(), exec_state)?;
191    let offset: TyF64 = args.get_kw_arg("offset", &RuntimeType::length(), exec_state)?;
192    let plane = inner_offset_plane(std_plane, offset, exec_state, &args).await?;
193    Ok(KclValue::Plane { value: Box::new(plane) })
194}
195
196async fn inner_offset_plane(
197    plane: PlaneData,
198    offset: TyF64,
199    exec_state: &mut ExecState,
200    args: &Args,
201) -> Result<Plane, KclError> {
202    let mut plane = Plane::from_plane_data(plane, exec_state)?;
203    // Though offset planes might be derived from standard planes, they are not
204    // standard planes themselves.
205    plane.value = PlaneType::Custom;
206
207    let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
208    plane.info.origin += normal * offset.to_length_units(plane.info.origin.units.unwrap_or(UnitLength::Millimeters));
209    make_offset_plane_in_engine(&plane, exec_state, args).await?;
210
211    Ok(plane)
212}
213
214// Engine-side effectful creation of an actual plane object.
215// offset planes are shown by default, and hidden by default if they
216// are used as a sketch plane. That hiding command is sent within inner_start_profile_at
217async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> {
218    // Create new default planes.
219    let default_size = 100.0;
220    let color = Color {
221        r: 0.6,
222        g: 0.6,
223        b: 0.6,
224        a: 0.3,
225    };
226
227    let meta = ModelingCmdMeta::from_args_id(exec_state, args, plane.id);
228    exec_state
229        .batch_modeling_cmd(
230            meta,
231            ModelingCmd::from(mcmd::MakePlane {
232                clobber: false,
233                origin: plane.info.origin.into(),
234                size: LengthUnit(default_size),
235                x_axis: plane.info.x_axis.into(),
236                y_axis: plane.info.y_axis.into(),
237                hide: Some(false),
238            }),
239        )
240        .await?;
241
242    // Set the color.
243    exec_state
244        .batch_modeling_cmd(
245            ModelingCmdMeta::from_args(exec_state, args),
246            ModelingCmd::from(mcmd::PlaneSetColor {
247                color,
248                plane_id: plane.id,
249            }),
250        )
251        .await?;
252
253    Ok(())
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::execution::{PlaneInfo, Point3d};
260
261    #[test]
262    fn fixes_left_handed_plane() {
263        let plane_info = PlaneInfo {
264            origin: Point3d {
265                x: 0.0,
266                y: 0.0,
267                z: 0.0,
268                units: Some(UnitLength::Millimeters),
269            },
270            x_axis: Point3d {
271                x: 1.0,
272                y: 0.0,
273                z: 0.0,
274                units: None,
275            },
276            y_axis: Point3d {
277                x: 0.0,
278                y: 1.0,
279                z: 0.0,
280                units: None,
281            },
282            z_axis: Point3d {
283                x: 0.0,
284                y: 0.0,
285                z: -1.0,
286                units: None,
287            },
288        };
289
290        // This plane is NOT right-handed.
291        assert!(plane_info.is_left_handed());
292        // But we can make it right-handed:
293        let fixed = plane_info.make_right_handed();
294        assert!(fixed.is_right_handed());
295    }
296}