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