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, PlaneInfo, PlaneKind, 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 let plane_id = exec_state.id_generator().next_uuid();
36
37 if args.ctx.no_engine_commands().await {
40 exec_state.err(crate::CompilationError {
41 source_range: args.source_range,
42 message: "The engine isn't available, so returning an arbitrary incorrect plane".to_owned(),
43 suggestion: None,
44 severity: crate::errors::Severity::Error,
45 tag: crate::errors::Tag::None,
46 });
47 return Ok(Plane {
48 artifact_id: plane_id.into(),
49 id: plane_id,
50 object_id: None,
53 kind: PlaneKind::Custom,
54 info: crate::execution::PlaneInfo {
55 origin: crate::execution::Point3d {
56 x: 0.0,
57 y: 0.0,
58 z: 0.0,
59 units: Some(UnitLength::Millimeters),
60 },
61 x_axis: crate::execution::Point3d {
62 x: 1.0,
63 y: 0.0,
64 z: 0.0,
65 units: None,
66 },
67 y_axis: crate::execution::Point3d {
68 x: 0.0,
69 y: 1.0,
70 z: 0.0,
71 units: None,
72 },
73 z_axis: crate::execution::Point3d {
74 x: 0.0,
75 y: 0.0,
76 z: 1.0,
77 units: None,
78 },
79 },
80 meta: vec![Metadata {
81 source_range: args.source_range,
82 }],
83 });
84 }
85
86 exec_state
88 .flush_batch_for_solids(
89 ModelingCmdMeta::from_args(exec_state, args),
90 std::slice::from_ref(&solid),
91 )
92 .await?;
93
94 let face_id = face.get_face_id(&solid, exec_state, args, true).await?;
96 let meta = ModelingCmdMeta::from_args_id(exec_state, args, plane_id);
97 let cmd = ModelingCmd::FaceIsPlanar(mcmd::FaceIsPlanar::builder().object_id(face_id).build());
98 let plane_resp = exec_state.send_modeling_cmd(meta, cmd).await?;
99 let OkWebSocketResponseData::Modeling {
100 modeling_response: OkModelingCmdResponse::FaceIsPlanar(planar),
101 } = plane_resp
102 else {
103 return Err(KclError::new_semantic(KclErrorDetails::new(
104 format!(
105 "Engine returned invalid response, it should have returned FaceIsPlanar but it returned {plane_resp:#?}"
106 ),
107 vec![args.source_range],
108 )));
109 };
110
111 let not_planar: Result<_, KclError> = Err(KclError::new_semantic(KclErrorDetails::new(
113 "The face you provided doesn't lie on any plane. It might be curved.".to_owned(),
114 vec![args.source_range],
115 )));
116 let Some(x_axis) = planar.x_axis else { return not_planar };
117 let Some(y_axis) = planar.y_axis else { return not_planar };
118 let Some(z_axis) = planar.z_axis else { return not_planar };
119 let Some(origin) = planar.origin else { return not_planar };
120
121 let engine_units = Some(UnitLength::Millimeters);
123 let x_axis = crate::execution::Point3d {
124 x: x_axis.x,
125 y: x_axis.y,
126 z: x_axis.z,
127 units: engine_units,
128 };
129 let y_axis = crate::execution::Point3d {
130 x: y_axis.x,
131 y: y_axis.y,
132 z: y_axis.z,
133 units: engine_units,
134 };
135 let z_axis = crate::execution::Point3d {
136 x: z_axis.x,
137 y: z_axis.y,
138 z: z_axis.z,
139 units: engine_units,
140 };
141 let origin = crate::execution::Point3d {
142 x: origin.x.0,
143 y: origin.y.0,
144 z: origin.z.0,
145 units: engine_units,
146 };
147
148 let plane_info = crate::execution::PlaneInfo {
151 origin,
152 x_axis,
153 y_axis,
154 z_axis,
155 };
156 let plane_info = plane_info.make_right_handed();
157
158 let plane_object_id = exec_state.next_object_id();
159 #[cfg(feature = "artifact-graph")]
160 {
161 use crate::execution::ArtifactId;
162
163 let plane_object = crate::front::Object {
164 id: plane_object_id,
165 kind: crate::front::ObjectKind::Plane(crate::front::Plane::Object(plane_object_id)),
166 label: Default::default(),
167 comments: Default::default(),
168 artifact_id: ArtifactId::new(plane_id),
169 source: args.source_range.into(),
170 };
171 exec_state.add_scene_object(plane_object, args.source_range);
172 }
173
174 Ok(Plane {
175 artifact_id: plane_id.into(),
176 id: plane_id,
177 object_id: Some(plane_object_id),
178 kind: PlaneKind::Custom,
179 info: plane_info,
180 meta: vec![Metadata {
181 source_range: args.source_range,
182 }],
183 })
184}
185
186pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
188 let std_plane = args.get_unlabeled_kw_arg("plane", &RuntimeType::plane(), exec_state)?;
189 let offset: TyF64 = args.get_kw_arg("offset", &RuntimeType::length(), exec_state)?;
190 let plane = inner_offset_plane(std_plane, offset, exec_state, &args).await?;
191 Ok(KclValue::Plane { value: Box::new(plane) })
192}
193
194async fn inner_offset_plane(
195 plane: PlaneData,
196 offset: TyF64,
197 exec_state: &mut ExecState,
198 args: &Args,
199) -> Result<Plane, KclError> {
200 let mut info = PlaneInfo::try_from(plane)?;
201
202 let normal = info.x_axis.axes_cross_product(&info.y_axis);
203 info.origin += normal * offset.to_length_units(info.origin.units.unwrap_or(UnitLength::Millimeters));
204
205 let id = exec_state.next_uuid();
206 let mut plane = Plane {
207 id,
208 artifact_id: id.into(),
209 object_id: None,
210 kind: PlaneKind::Custom,
211 info,
212 meta: vec![Metadata {
213 source_range: args.source_range,
214 }],
215 };
216 make_offset_plane_in_engine(&mut plane, exec_state, args).await?;
217
218 Ok(plane)
219}
220
221async fn make_offset_plane_in_engine(
225 plane: &mut Plane,
226 exec_state: &mut ExecState,
227 args: &Args,
228) -> Result<(), KclError> {
229 let plane_object_id = exec_state.next_object_id();
230 #[cfg(feature = "artifact-graph")]
231 {
232 let plane_object = crate::front::Object {
233 id: plane_object_id,
234 kind: crate::front::ObjectKind::Plane(crate::front::Plane::Object(plane_object_id)),
235 label: Default::default(),
236 comments: Default::default(),
237 artifact_id: plane.artifact_id,
238 source: args.source_range.into(),
239 };
240 exec_state.add_scene_object(plane_object, args.source_range);
241 }
242
243 let default_size = 100.0;
245 let color = Color {
246 r: 0.6,
247 g: 0.6,
248 b: 0.6,
249 a: 0.3,
250 };
251
252 let meta = ModelingCmdMeta::from_args_id(exec_state, args, plane.id);
253 exec_state
254 .batch_modeling_cmd(
255 meta,
256 ModelingCmd::from(
257 mcmd::MakePlane::builder()
258 .clobber(false)
259 .origin(plane.info.origin.into())
260 .size(LengthUnit(default_size))
261 .x_axis(plane.info.x_axis.into())
262 .y_axis(plane.info.y_axis.into())
263 .hide(false)
264 .build(),
265 ),
266 )
267 .await?;
268
269 exec_state
271 .batch_modeling_cmd(
272 ModelingCmdMeta::from_args(exec_state, args),
273 ModelingCmd::from(mcmd::PlaneSetColor::builder().color(color).plane_id(plane.id).build()),
274 )
275 .await?;
276
277 plane.kind = PlaneKind::Custom;
280 plane.object_id = Some(plane_object_id);
281
282 Ok(())
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::execution::{PlaneInfo, Point3d};
289
290 #[test]
291 fn fixes_left_handed_plane() {
292 let plane_info = PlaneInfo {
293 origin: Point3d {
294 x: 0.0,
295 y: 0.0,
296 z: 0.0,
297 units: Some(UnitLength::Millimeters),
298 },
299 x_axis: Point3d {
300 x: 1.0,
301 y: 0.0,
302 z: 0.0,
303 units: None,
304 },
305 y_axis: Point3d {
306 x: 0.0,
307 y: 1.0,
308 z: 0.0,
309 units: None,
310 },
311 z_axis: Point3d {
312 x: 0.0,
313 y: 0.0,
314 z: -1.0,
315 units: None,
316 },
317 };
318
319 assert!(plane_info.is_left_handed());
321 let fixed = plane_info.make_right_handed();
323 assert!(fixed.is_right_handed());
324 }
325}