1use std::collections::HashSet;
4
5use anyhow::Result;
6use kcmc::ModelingCmd;
7use kcmc::each_cmd as mcmd;
8use kittycad_modeling_cmds::length_unit::LengthUnit;
9use kittycad_modeling_cmds::ok_response::OkModelingCmdResponse;
10use kittycad_modeling_cmds::output as mout;
11use kittycad_modeling_cmds::shared::BodyType;
12use kittycad_modeling_cmds::shared::FractionOfEdge;
13use kittycad_modeling_cmds::shared::SurfaceEdgeReference;
14use kittycad_modeling_cmds::websocket::OkWebSocketResponseData;
15use kittycad_modeling_cmds::{self as kcmc};
16
17use crate::errors::KclError;
18use crate::errors::KclErrorDetails;
19use crate::execution::BoundedEdge;
20use crate::execution::ExecState;
21use crate::execution::KclValue;
22use crate::execution::ModelingCmdMeta;
23use crate::execution::Solid;
24use crate::execution::SolidCreator;
25use crate::execution::types::ArrayLen;
26use crate::execution::types::PrimitiveType;
27use crate::execution::types::RuntimeType;
28use crate::std::Args;
29use crate::std::DEFAULT_TOLERANCE_MM;
30use crate::std::args::TyF64;
31use crate::std::sketch::FaceTag;
32
33pub async fn flip_surface(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
35 let surface = args.get_unlabeled_kw_arg("surface", &RuntimeType::solids(), exec_state)?;
36 let out = inner_flip_surface(surface, exec_state, args).await?;
37 Ok(out.into())
38}
39
40async fn inner_flip_surface(
41 surfaces: Vec<Solid>,
42 exec_state: &mut ExecState,
43 args: Args,
44) -> Result<Vec<Solid>, KclError> {
45 for surface in &surfaces {
46 exec_state
47 .batch_modeling_cmd(
48 ModelingCmdMeta::from_args(exec_state, &args),
49 ModelingCmd::from(mcmd::Solid3dFlip::builder().object_id(surface.id).build()),
50 )
51 .await?;
52 }
53
54 Ok(surfaces)
55}
56
57pub async fn is_solid(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
59 let argument = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
60 let meta = vec![crate::execution::Metadata {
61 source_range: args.source_range,
62 }];
63
64 let res = inner_is_equal_body_type(argument, exec_state, args, BodyType::Solid).await?;
65 Ok(KclValue::Bool { value: res, meta })
66}
67
68pub async fn is_surface(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
70 let argument = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
71 let meta = vec![crate::execution::Metadata {
72 source_range: args.source_range,
73 }];
74
75 let res = inner_is_equal_body_type(argument, exec_state, args, BodyType::Surface).await?;
76 Ok(KclValue::Bool { value: res, meta })
77}
78
79async fn inner_is_equal_body_type(
80 surface: Solid,
81 exec_state: &mut ExecState,
82 args: Args,
83 expected: BodyType,
84) -> Result<bool, KclError> {
85 let meta = ModelingCmdMeta::from_args(exec_state, &args);
86 let cmd = ModelingCmd::from(mcmd::Solid3dGetBodyType::builder().object_id(surface.id).build());
87
88 let response = exec_state.send_modeling_cmd(meta, cmd).await?;
89
90 let OkWebSocketResponseData::Modeling {
91 modeling_response: OkModelingCmdResponse::Solid3dGetBodyType(body),
92 } = response
93 else {
94 return Err(KclError::new_semantic(KclErrorDetails::new(
95 format!(
96 "Engine returned invalid response, it should have returned Solid3dGetBodyType but it returned {response:#?}"
97 ),
98 vec![args.source_range],
99 )));
100 };
101
102 Ok(expected == body.body_type)
103}
104
105pub async fn delete_face(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
106 let body = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
107 let faces: Option<Vec<FaceTag>> = args.get_kw_arg_opt(
108 "faces",
109 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
110 exec_state,
111 )?;
112 let face_indices: Option<Vec<TyF64>> = args.get_kw_arg_opt(
113 "faceIndices",
114 &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Minimum(1)),
115 exec_state,
116 )?;
117 let face_indices = if let Some(face_indices) = face_indices {
118 let faces = face_indices
119 .into_iter()
120 .map(|num| {
121 crate::try_f64_to_u32(num.n).ok_or_else(|| {
122 KclError::new_semantic(KclErrorDetails::new(
123 format!("Face indices must be whole numbers, got {}", num.n),
124 vec![args.source_range],
125 ))
126 })
127 })
128 .collect::<Result<Vec<_>, _>>()?;
129 Some(faces)
130 } else {
131 None
132 };
133 inner_delete_face(body, faces, face_indices, exec_state, args)
134 .await
135 .map(Box::new)
136 .map(|value| KclValue::Solid { value })
137}
138
139async fn inner_delete_face(
140 body: Solid,
141 tagged_faces: Option<Vec<FaceTag>>,
142 face_indices: Option<Vec<u32>>,
143 exec_state: &mut ExecState,
144 args: Args,
145) -> Result<Solid, KclError> {
146 if tagged_faces.is_none() && face_indices.is_none() {
149 return Err(KclError::new_semantic(KclErrorDetails::new(
150 "You must use either the `faces` or the `faceIndices` parameter".to_string(),
151 vec![args.source_range],
152 )));
153 }
154
155 let no_engine_commands = args.ctx.no_engine_commands().await;
158 if no_engine_commands {
159 return Ok(body);
160 }
161
162 let tagged_faces = tagged_faces.unwrap_or_default();
164 let face_indices = face_indices.unwrap_or_default();
165 let mut face_ids = HashSet::with_capacity(face_indices.len() + tagged_faces.len());
167
168 for tagged_face in tagged_faces {
169 let face_id = tagged_face.get_face_id(&body, exec_state, &args, false).await?;
170 face_ids.insert(face_id);
171 }
172
173 for face_index in face_indices {
174 let face_uuid_response = exec_state
175 .send_modeling_cmd(
176 ModelingCmdMeta::from_args(exec_state, &args),
177 ModelingCmd::from(
178 mcmd::Solid3dGetFaceUuid::builder()
179 .object_id(body.id)
180 .face_index(face_index)
181 .build(),
182 ),
183 )
184 .await?;
185
186 let OkWebSocketResponseData::Modeling {
187 modeling_response: OkModelingCmdResponse::Solid3dGetFaceUuid(inner_resp),
188 } = face_uuid_response
189 else {
190 return Err(KclError::new_semantic(KclErrorDetails::new(
191 format!(
192 "Engine returned invalid response, it should have returned Solid3dGetFaceUuid but it returned {face_uuid_response:?}"
193 ),
194 vec![args.source_range],
195 )));
196 };
197 face_ids.insert(inner_resp.face_id);
198 }
199
200 let delete_face_response = exec_state
202 .send_modeling_cmd(
203 ModelingCmdMeta::from_args(exec_state, &args),
204 ModelingCmd::from(
205 mcmd::EntityDeleteChildren::builder()
206 .entity_id(body.id)
207 .child_entity_ids(face_ids)
208 .build(),
209 ),
210 )
211 .await?;
212
213 let OkWebSocketResponseData::Modeling {
214 modeling_response: OkModelingCmdResponse::EntityDeleteChildren(mout::EntityDeleteChildren { .. }),
215 } = delete_face_response
216 else {
217 return Err(KclError::new_semantic(KclErrorDetails::new(
218 format!(
219 "Engine returned invalid response, it should have returned EntityDeleteChildren but it returned {delete_face_response:?}"
220 ),
221 vec![args.source_range],
222 )));
223 };
224
225 Ok(body)
227}
228
229pub async fn blend(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
231 let edges: Vec<KclValue> = args.get_unlabeled_kw_arg(
232 "edges",
233 &RuntimeType::Array(
234 Box::new(RuntimeType::Union(vec![
235 RuntimeType::Primitive(PrimitiveType::BoundedEdge),
236 RuntimeType::tagged_edge(),
237 ])),
238 ArrayLen::Known(2),
239 ),
240 exec_state,
241 )?;
242
243 let mut bounded_edges = Vec::with_capacity(edges.len());
244 for edge in edges {
245 bounded_edges.push(resolve_blend_edge(edge, exec_state, &args).await?);
246 }
247
248 inner_blend(bounded_edges, exec_state, args.clone())
249 .await
250 .map(Box::new)
251 .map(|value| KclValue::Solid { value })
252}
253
254async fn resolve_blend_edge(edge: KclValue, exec_state: &mut ExecState, args: &Args) -> Result<BoundedEdge, KclError> {
255 match edge {
256 KclValue::BoundedEdge { value, .. } => Ok(value),
257 KclValue::TagIdentifier(tag) => {
258 let tagged_edge = args.get_tag_engine_info(exec_state, &tag)?;
259 Ok(BoundedEdge {
260 face_id: tagged_edge.geometry.id(),
261 edge_id: tagged_edge.id,
262 lower_bound: 0.0,
263 upper_bound: 1.0,
264 })
265 }
266 _ => Err(KclError::new_internal(KclErrorDetails::new(
267 "Unexpected edge value while preparing blend edges.".to_owned(),
268 vec![args.source_range],
269 ))),
270 }
271}
272
273async fn inner_blend(edges: Vec<BoundedEdge>, exec_state: &mut ExecState, args: Args) -> Result<Solid, KclError> {
274 let id = exec_state.next_uuid();
275
276 let surface_refs: Vec<SurfaceEdgeReference> = edges
277 .iter()
278 .map(|edge| {
279 SurfaceEdgeReference::builder()
280 .object_id(edge.face_id)
281 .edges(vec![
282 FractionOfEdge::builder()
283 .edge_id(edge.edge_id)
284 .lower_bound(edge.lower_bound)
285 .upper_bound(edge.upper_bound)
286 .build(),
287 ])
288 .build()
289 })
290 .collect();
291
292 exec_state
293 .batch_modeling_cmd(
294 ModelingCmdMeta::from_args_id(exec_state, &args, id),
295 ModelingCmd::from(mcmd::SurfaceBlend::builder().surfaces(surface_refs).build()),
296 )
297 .await?;
298
299 let solid = Solid {
300 id,
301 artifact_id: id.into(),
302 value: vec![],
303 creator: SolidCreator::Procedural,
304 start_cap_id: None,
305 end_cap_id: None,
306 edge_cuts: vec![],
307 units: exec_state.length_unit(),
308 sectional: false,
309 meta: vec![crate::execution::Metadata {
310 source_range: args.source_range,
311 }],
312 };
313 Ok(solid)
315}
316
317pub async fn join(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
319 let selection: Vec<Solid> = args.get_unlabeled_kw_arg("selection", &RuntimeType::solids(), exec_state)?;
320 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
321
322 inner_join(selection, tolerance, exec_state, args)
323 .await
324 .map(Box::new)
325 .map(|value| KclValue::Solid { value })
326}
327
328async fn inner_join(
329 selection: Vec<Solid>,
330 tolerance: Option<TyF64>,
331 exec_state: &mut ExecState,
332 args: Args,
333) -> Result<Solid, KclError> {
334 if selection.len() == 1 {
335 let cmd = mcmd::Solid3dJoin::builder().object_id(selection[0].id).build();
336
337 exec_state
338 .batch_modeling_cmd(ModelingCmdMeta::from_args(exec_state, &args), ModelingCmd::from(cmd))
339 .await?;
340
341 Ok(selection[0].clone())
342 } else {
343 let body_out_id = exec_state.next_uuid();
344
345 exec_state
346 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &selection)
347 .await?;
348
349 let body_ids = selection.iter().map(|body| body.id).collect();
350 let tolerance = tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM);
351 let cmd = mcmd::Solid3dMultiJoin::builder()
352 .object_ids(body_ids)
353 .tolerance(LengthUnit(tolerance))
354 .build();
355
356 exec_state
357 .batch_modeling_cmd(
358 ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
359 ModelingCmd::from(cmd),
360 )
361 .await?;
362
363 let solid = Solid {
364 id: body_out_id,
365 artifact_id: body_out_id.into(),
366 value: vec![],
367 creator: SolidCreator::Procedural,
368 start_cap_id: None,
369 end_cap_id: None,
370 edge_cuts: vec![],
371 units: exec_state.length_unit(),
372 sectional: false,
373 meta: vec![args.source_range.into()],
374 };
375 Ok(solid)
376 }
377}