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