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
28pub(crate) async 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                z_axis: Default::default(),
55            },
56            meta: vec![Metadata {
57                source_range: args.source_range,
58            }],
59        });
60    }
61
62    // Query the engine to learn what plane, if any, this face is on.
63    let face_id = face.get_face_id(&solid, exec_state, args, true).await?;
64    let plane_id = exec_state.id_generator().next_uuid();
65    let meta = ModelingCmdMeta::with_id(&args.ctx, args.source_range, plane_id);
66    let cmd = ModelingCmd::FaceIsPlanar(mcmd::FaceIsPlanar { object_id: face_id });
67    let plane_resp = exec_state.send_modeling_cmd(meta, cmd).await?;
68    let OkWebSocketResponseData::Modeling {
69        modeling_response: OkModelingCmdResponse::FaceIsPlanar(planar),
70    } = plane_resp
71    else {
72        return Err(KclError::new_semantic(KclErrorDetails::new(
73            format!(
74                "Engine returned invalid response, it should have returned FaceIsPlanar but it returned {plane_resp:#?}"
75            ),
76            vec![args.source_range],
77        )));
78    };
79
80    // Destructure engine's response to check if the face was on a plane.
81    let not_planar: Result<_, KclError> = Err(KclError::new_semantic(KclErrorDetails::new(
82        "The face you provided doesn't lie on any plane. It might be curved.".to_owned(),
83        vec![args.source_range],
84    )));
85    let Some(x_axis) = planar.x_axis else { return not_planar };
86    let Some(y_axis) = planar.y_axis else { return not_planar };
87    let Some(z_axis) = planar.z_axis else { return not_planar };
88    let Some(origin) = planar.origin else { return not_planar };
89
90    // Engine always returns measurements in mm.
91    let engine_units = UnitLen::Mm;
92    let x_axis = crate::execution::Point3d {
93        x: x_axis.x,
94        y: x_axis.y,
95        z: x_axis.z,
96        units: engine_units,
97    };
98    let y_axis = crate::execution::Point3d {
99        x: y_axis.x,
100        y: y_axis.y,
101        z: y_axis.z,
102        units: engine_units,
103    };
104    let z_axis = crate::execution::Point3d {
105        x: z_axis.x,
106        y: z_axis.y,
107        z: z_axis.z,
108        units: engine_units,
109    };
110    let origin = crate::execution::Point3d {
111        x: origin.x.0,
112        y: origin.y.0,
113        z: origin.z.0,
114        units: engine_units,
115    };
116
117    // Planes should always be right-handed, but due to an engine bug sometimes they're not.
118    // Test for right-handedness: cross(X,Y) is Z
119    let plane_info = crate::execution::PlaneInfo {
120        origin,
121        x_axis,
122        y_axis,
123        z_axis,
124    };
125    let plane_info = plane_info.make_right_handed();
126
127    Ok(Plane {
128        artifact_id: plane_id.into(),
129        id: plane_id,
130        value: PlaneType::Custom,
131        info: plane_info,
132        meta: vec![Metadata {
133            source_range: args.source_range,
134        }],
135    })
136}
137
138/// Offset a plane by a distance along its normal.
139pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
140    let std_plane = args.get_unlabeled_kw_arg("plane", &RuntimeType::plane(), exec_state)?;
141    let offset: TyF64 = args.get_kw_arg("offset", &RuntimeType::length(), exec_state)?;
142    let plane = inner_offset_plane(std_plane, offset, exec_state, &args).await?;
143    Ok(KclValue::Plane { value: Box::new(plane) })
144}
145
146async fn inner_offset_plane(
147    plane: PlaneData,
148    offset: TyF64,
149    exec_state: &mut ExecState,
150    args: &Args,
151) -> Result<Plane, KclError> {
152    let mut plane = Plane::from_plane_data(plane, exec_state)?;
153    // Though offset planes might be derived from standard planes, they are not
154    // standard planes themselves.
155    plane.value = PlaneType::Custom;
156
157    let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
158    plane.info.origin += normal * offset.to_length_units(plane.info.origin.units);
159    make_offset_plane_in_engine(&plane, exec_state, args).await?;
160
161    Ok(plane)
162}
163
164// Engine-side effectful creation of an actual plane object.
165// offset planes are shown by default, and hidden by default if they
166// are used as a sketch plane. That hiding command is sent within inner_start_profile_at
167async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> {
168    // Create new default planes.
169    let default_size = 100.0;
170    let color = Color {
171        r: 0.6,
172        g: 0.6,
173        b: 0.6,
174        a: 0.3,
175    };
176
177    let meta = ModelingCmdMeta::from_args_id(args, plane.id);
178    exec_state
179        .batch_modeling_cmd(
180            meta,
181            ModelingCmd::from(mcmd::MakePlane {
182                clobber: false,
183                origin: plane.info.origin.into(),
184                size: LengthUnit(default_size),
185                x_axis: plane.info.x_axis.into(),
186                y_axis: plane.info.y_axis.into(),
187                hide: Some(false),
188            }),
189        )
190        .await?;
191
192    // Set the color.
193    exec_state
194        .batch_modeling_cmd(
195            args.into(),
196            ModelingCmd::from(mcmd::PlaneSetColor {
197                color,
198                plane_id: plane.id,
199            }),
200        )
201        .await?;
202
203    Ok(())
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::execution::{PlaneInfo, Point3d};
210
211    #[test]
212    fn fixes_left_handed_plane() {
213        let plane_info = PlaneInfo {
214            origin: Point3d {
215                x: 0.0,
216                y: 0.0,
217                z: 0.0,
218                units: UnitLen::Mm,
219            },
220            x_axis: Point3d {
221                x: 1.0,
222                y: 0.0,
223                z: 0.0,
224                units: UnitLen::Mm,
225            },
226            y_axis: Point3d {
227                x: 0.0,
228                y: 1.0,
229                z: 0.0,
230                units: UnitLen::Mm,
231            },
232            z_axis: Point3d {
233                x: 0.0,
234                y: 0.0,
235                z: -1.0,
236                units: UnitLen::Mm,
237            },
238        };
239
240        // This plane is NOT right-handed.
241        assert!(plane_info.is_left_handed());
242        // But we can make it right-handed:
243        let fixed = plane_info.make_right_handed();
244        assert!(fixed.is_right_handed());
245    }
246}