Skip to main content

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