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