1use 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
18pub 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 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 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 exec_state
65 .flush_batch_for_solids(args.into(), std::slice::from_ref(&solid))
66 .await?;
67
68 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 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 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 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
144pub 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 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
170async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> {
174 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 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 assert!(plane_info.is_left_handed());
248 let fixed = plane_info.make_right_handed();
250 assert!(fixed.is_right_handed());
251 }
252}