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