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