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