Skip to main content

kcl_lib/std/
planes.rs

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