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