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