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: 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.get_face_id(&solid, exec_state, args, true).await?;
93 let plane_id = exec_state.id_generator().next_uuid();
94 let meta = ModelingCmdMeta::from_args_id(exec_state, args, plane_id);
95 let cmd = ModelingCmd::FaceIsPlanar(mcmd::FaceIsPlanar { object_id: face_id });
96 let plane_resp = exec_state.send_modeling_cmd(meta, cmd).await?;
97 let OkWebSocketResponseData::Modeling {
98 modeling_response: OkModelingCmdResponse::FaceIsPlanar(planar),
99 } = plane_resp
100 else {
101 return Err(KclError::new_semantic(KclErrorDetails::new(
102 format!(
103 "Engine returned invalid response, it should have returned FaceIsPlanar but it returned {plane_resp:#?}"
104 ),
105 vec![args.source_range],
106 )));
107 };
108
109 let not_planar: Result<_, KclError> = Err(KclError::new_semantic(KclErrorDetails::new(
111 "The face you provided doesn't lie on any plane. It might be curved.".to_owned(),
112 vec![args.source_range],
113 )));
114 let Some(x_axis) = planar.x_axis else { return not_planar };
115 let Some(y_axis) = planar.y_axis else { return not_planar };
116 let Some(z_axis) = planar.z_axis else { return not_planar };
117 let Some(origin) = planar.origin else { return not_planar };
118
119 let engine_units = Some(UnitLength::Millimeters);
121 let x_axis = crate::execution::Point3d {
122 x: x_axis.x,
123 y: x_axis.y,
124 z: x_axis.z,
125 units: engine_units,
126 };
127 let y_axis = crate::execution::Point3d {
128 x: y_axis.x,
129 y: y_axis.y,
130 z: y_axis.z,
131 units: engine_units,
132 };
133 let z_axis = crate::execution::Point3d {
134 x: z_axis.x,
135 y: z_axis.y,
136 z: z_axis.z,
137 units: engine_units,
138 };
139 let origin = crate::execution::Point3d {
140 x: origin.x.0,
141 y: origin.y.0,
142 z: origin.z.0,
143 units: engine_units,
144 };
145
146 let plane_info = crate::execution::PlaneInfo {
149 origin,
150 x_axis,
151 y_axis,
152 z_axis,
153 };
154 let plane_info = plane_info.make_right_handed();
155
156 Ok(Plane {
157 artifact_id: plane_id.into(),
158 id: plane_id,
159 value: PlaneType::Custom,
160 info: plane_info,
161 meta: vec![Metadata {
162 source_range: args.source_range,
163 }],
164 })
165}
166
167pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
169 let std_plane = args.get_unlabeled_kw_arg("plane", &RuntimeType::plane(), exec_state)?;
170 let offset: TyF64 = args.get_kw_arg("offset", &RuntimeType::length(), exec_state)?;
171 let plane = inner_offset_plane(std_plane, offset, exec_state, &args).await?;
172 Ok(KclValue::Plane { value: Box::new(plane) })
173}
174
175async fn inner_offset_plane(
176 plane: PlaneData,
177 offset: TyF64,
178 exec_state: &mut ExecState,
179 args: &Args,
180) -> Result<Plane, KclError> {
181 let mut plane = Plane::from_plane_data(plane, exec_state)?;
182 plane.value = PlaneType::Custom;
185
186 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
187 plane.info.origin += normal * offset.to_length_units(plane.info.origin.units.unwrap_or(UnitLength::Millimeters));
188 make_offset_plane_in_engine(&plane, exec_state, args).await?;
189
190 Ok(plane)
191}
192
193async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> {
197 let default_size = 100.0;
199 let color = Color {
200 r: 0.6,
201 g: 0.6,
202 b: 0.6,
203 a: 0.3,
204 };
205
206 let meta = ModelingCmdMeta::from_args_id(exec_state, args, plane.id);
207 exec_state
208 .batch_modeling_cmd(
209 meta,
210 ModelingCmd::from(mcmd::MakePlane {
211 clobber: false,
212 origin: plane.info.origin.into(),
213 size: LengthUnit(default_size),
214 x_axis: plane.info.x_axis.into(),
215 y_axis: plane.info.y_axis.into(),
216 hide: Some(false),
217 }),
218 )
219 .await?;
220
221 exec_state
223 .batch_modeling_cmd(
224 ModelingCmdMeta::from_args(exec_state, args),
225 ModelingCmd::from(mcmd::PlaneSetColor {
226 color,
227 plane_id: plane.id,
228 }),
229 )
230 .await?;
231
232 Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::execution::{PlaneInfo, Point3d};
239
240 #[test]
241 fn fixes_left_handed_plane() {
242 let plane_info = PlaneInfo {
243 origin: Point3d {
244 x: 0.0,
245 y: 0.0,
246 z: 0.0,
247 units: Some(UnitLength::Millimeters),
248 },
249 x_axis: Point3d {
250 x: 1.0,
251 y: 0.0,
252 z: 0.0,
253 units: None,
254 },
255 y_axis: Point3d {
256 x: 0.0,
257 y: 1.0,
258 z: 0.0,
259 units: None,
260 },
261 z_axis: Point3d {
262 x: 0.0,
263 y: 0.0,
264 z: -1.0,
265 units: None,
266 },
267 };
268
269 assert!(plane_info.is_left_handed());
271 let fixed = plane_info.make_right_handed();
273 assert!(fixed.is_right_handed());
274 }
275}