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::ConsumedSolidOperation;
21use crate::execution::ExecState;
22use crate::execution::KclValue;
23use crate::execution::ModelingCmdMeta;
24use crate::execution::Solid;
25use crate::execution::SolidCreator;
26use crate::execution::types::ArrayLen;
27use crate::execution::types::PrimitiveType;
28use crate::execution::types::RuntimeType;
29use crate::std::Args;
30use crate::std::DEFAULT_TOLERANCE_MM;
31use crate::std::args::TyF64;
32use crate::std::edge;
33use crate::std::sketch::FaceTag;
34use crate::std::solid_consumption::record_consumed_solids;
35
36pub async fn flip_surface(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
38 let surface = args.get_unlabeled_kw_arg("surface", &RuntimeType::solids(), exec_state)?;
39 let out = inner_flip_surface(surface, exec_state, args).await?;
40 Ok(out.into())
41}
42
43async fn inner_flip_surface(
44 surfaces: Vec<Solid>,
45 exec_state: &mut ExecState,
46 args: Args,
47) -> Result<Vec<Solid>, KclError> {
48 for surface in &surfaces {
49 exec_state
50 .batch_modeling_cmd(
51 ModelingCmdMeta::from_args(exec_state, &args),
52 ModelingCmd::from(mcmd::Solid3dFlip::builder().object_id(surface.id).build()),
53 )
54 .await?;
55 }
56
57 Ok(surfaces)
58}
59
60pub async fn is_solid(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
62 let argument = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
63 let meta = vec![crate::execution::Metadata {
64 source_range: args.source_range,
65 }];
66
67 let res = inner_is_equal_body_type(argument, exec_state, args, BodyType::Solid).await?;
68 Ok(KclValue::Bool { value: res, meta })
69}
70
71pub async fn is_surface(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
73 let argument = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
74 let meta = vec![crate::execution::Metadata {
75 source_range: args.source_range,
76 }];
77
78 let res = inner_is_equal_body_type(argument, exec_state, args, BodyType::Surface).await?;
79 Ok(KclValue::Bool { value: res, meta })
80}
81
82async fn inner_is_equal_body_type(
83 surface: Solid,
84 exec_state: &mut ExecState,
85 args: Args,
86 expected: BodyType,
87) -> Result<bool, KclError> {
88 let meta = ModelingCmdMeta::from_args(exec_state, &args);
89 let cmd = ModelingCmd::from(mcmd::Solid3dGetBodyType::builder().object_id(surface.id).build());
90
91 let response = exec_state.send_modeling_cmd(meta, cmd).await?;
92
93 let OkWebSocketResponseData::Modeling {
94 modeling_response: OkModelingCmdResponse::Solid3dGetBodyType(body),
95 } = response
96 else {
97 return Err(KclError::new_semantic(KclErrorDetails::new(
98 format!(
99 "Engine returned invalid response, it should have returned Solid3dGetBodyType but it returned {response:#?}"
100 ),
101 vec![args.source_range],
102 )));
103 };
104
105 Ok(expected == body.body_type)
106}
107
108pub async fn delete_face(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
109 let body = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
110 let faces: Option<Vec<FaceTag>> = args.get_kw_arg_opt(
111 "faces",
112 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
113 exec_state,
114 )?;
115 let face_indices: Option<Vec<TyF64>> = args.get_kw_arg_opt(
116 "faceIndices",
117 &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Minimum(1)),
118 exec_state,
119 )?;
120 let face_indices = if let Some(face_indices) = face_indices {
121 let faces = face_indices
122 .into_iter()
123 .map(|num| {
124 crate::try_f64_to_u32(num.n).ok_or_else(|| {
125 KclError::new_semantic(KclErrorDetails::new(
126 format!("Face indices must be whole numbers, got {}", num.n),
127 vec![args.source_range],
128 ))
129 })
130 })
131 .collect::<Result<Vec<_>, _>>()?;
132 Some(faces)
133 } else {
134 None
135 };
136 inner_delete_face(body, faces, face_indices, exec_state, args)
137 .await
138 .map(Box::new)
139 .map(|value| KclValue::Solid { value })
140}
141
142async fn inner_delete_face(
143 body: Solid,
144 tagged_faces: Option<Vec<FaceTag>>,
145 face_indices: Option<Vec<u32>>,
146 exec_state: &mut ExecState,
147 args: Args,
148) -> Result<Solid, KclError> {
149 if tagged_faces.is_none() && face_indices.is_none() {
152 return Err(KclError::new_semantic(KclErrorDetails::new(
153 "You must use either the `faces` or the `faceIndices` parameter".to_string(),
154 vec![args.source_range],
155 )));
156 }
157
158 let no_engine_commands = args.ctx.no_engine_commands().await;
161 if no_engine_commands {
162 return Ok(body);
163 }
164
165 exec_state
170 .flush_batch_for_solids(
171 ModelingCmdMeta::from_args(exec_state, &args),
172 std::slice::from_ref(&body),
173 )
174 .await?;
175
176 let tagged_faces = tagged_faces.unwrap_or_default();
178 let face_indices = face_indices.unwrap_or_default();
179 let mut face_ids = HashSet::with_capacity(face_indices.len() + tagged_faces.len());
181
182 for tagged_face in tagged_faces {
183 let face_id = tagged_face.get_face_id(&body, exec_state, &args, false).await?;
184 face_ids.insert(face_id);
185 }
186
187 for face_index in face_indices {
188 let face_uuid_response = exec_state
189 .send_modeling_cmd(
190 ModelingCmdMeta::from_args(exec_state, &args),
191 ModelingCmd::from(
192 mcmd::Solid3dGetFaceUuid::builder()
193 .object_id(body.id)
194 .face_index(face_index)
195 .build(),
196 ),
197 )
198 .await?;
199
200 let OkWebSocketResponseData::Modeling {
201 modeling_response: OkModelingCmdResponse::Solid3dGetFaceUuid(inner_resp),
202 } = face_uuid_response
203 else {
204 return Err(KclError::new_semantic(KclErrorDetails::new(
205 format!(
206 "Engine returned invalid response, it should have returned Solid3dGetFaceUuid but it returned {face_uuid_response:?}"
207 ),
208 vec![args.source_range],
209 )));
210 };
211 face_ids.insert(inner_resp.face_id);
212 }
213
214 let delete_face_response = exec_state
216 .send_modeling_cmd(
217 ModelingCmdMeta::from_args(exec_state, &args),
218 ModelingCmd::from(
219 mcmd::EntityDeleteChildren::builder()
220 .entity_id(body.id)
221 .child_entity_ids(face_ids)
222 .build(),
223 ),
224 )
225 .await?;
226
227 let OkWebSocketResponseData::Modeling {
228 modeling_response: OkModelingCmdResponse::EntityDeleteChildren(mout::EntityDeleteChildren { .. }),
229 } = delete_face_response
230 else {
231 return Err(KclError::new_semantic(KclErrorDetails::new(
232 format!(
233 "Engine returned invalid response, it should have returned EntityDeleteChildren but it returned {delete_face_response:?}"
234 ),
235 vec![args.source_range],
236 )));
237 };
238
239 Ok(body)
241}
242
243pub async fn blend(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
245 let edges: Vec<KclValue> = args.get_unlabeled_kw_arg(
246 "edges",
247 &RuntimeType::Array(
248 Box::new(RuntimeType::Union(vec![
249 RuntimeType::Primitive(PrimitiveType::BoundedEdge),
250 RuntimeType::tagged_edge(),
251 RuntimeType::Primitive(PrimitiveType::Any),
252 ])),
253 ArrayLen::Known(2),
254 ),
255 exec_state,
256 )?;
257
258 let mut bounded_edges = Vec::with_capacity(edges.len());
259 for edge in edges {
260 bounded_edges.push(resolve_blend_edge(edge, exec_state, &args).await?);
261 }
262
263 inner_blend(bounded_edges, exec_state, args.clone())
264 .await
265 .map(Box::new)
266 .map(|value| KclValue::Solid { value })
267}
268
269async fn resolve_blend_edge(edge: KclValue, exec_state: &mut ExecState, args: &Args) -> Result<BoundedEdge, KclError> {
272 match edge {
273 KclValue::BoundedEdge { value, .. } => Ok(value),
274 KclValue::TagIdentifier(tag) => {
275 let tagged_edge = args.get_tag_engine_info(exec_state, &tag)?;
276 Ok(BoundedEdge {
277 face_id: tagged_edge.geometry.id(),
278 edge_id: Some(tagged_edge.id),
279 edge_specifier: None,
280 lower_bound: 0.0,
281 upper_bound: 1.0,
282 })
283 }
284 KclValue::Object { value: obj, .. } => {
285 let spec = edge::parse_edge_specifier_object(&obj, args)?;
286 let face_id = edge::face_id_from_first_side_face(&spec, exec_state, args)?;
287 Ok(BoundedEdge {
288 face_id,
289 edge_id: None,
290 edge_specifier: Some(spec),
291 lower_bound: 0.0,
292 upper_bound: 1.0,
293 })
294 }
295 _ => Err(KclError::new_internal(KclErrorDetails::new(
296 "Unexpected edge value while preparing blend edges. Expected BoundedEdge, tagged edge, or edge specifier object (e.g. { sideFaces = [...], endFaces = [...], index = 0 }).".to_owned(),
297 vec![args.source_range],
298 ))),
299 }
300}
301
302async fn inner_blend(edges: Vec<BoundedEdge>, exec_state: &mut ExecState, args: Args) -> Result<Solid, KclError> {
303 let id = exec_state.next_uuid();
304
305 let mut surface_refs = Vec::with_capacity(edges.len());
306 for edge in &edges {
307 let fraction = if let Some(eid) = edge.edge_id {
308 FractionOfEdge::builder()
309 .edge_id(eid)
310 .lower_bound(edge.lower_bound)
311 .upper_bound(edge.upper_bound)
312 .build()
313 } else if let Some(ref spec) = edge.edge_specifier {
314 let resolved = edge::resolve_unresolved_edge_specifier(edge.face_id, spec, exec_state, &args).await?;
315 FractionOfEdge::builder()
316 .edge_specifier(resolved)
317 .lower_bound(edge.lower_bound)
318 .upper_bound(edge.upper_bound)
319 .build()
320 } else {
321 return Err(KclError::new_internal(KclErrorDetails::new(
322 "BoundedEdge must have edge_id or edge_specifier".to_owned(),
323 vec![args.source_range],
324 )));
325 };
326 surface_refs.push(
327 SurfaceEdgeReference::builder()
328 .object_id(edge.face_id)
329 .edges(vec![fraction])
330 .build(),
331 );
332 }
333
334 exec_state
335 .batch_modeling_cmd(
336 ModelingCmdMeta::from_args_id(exec_state, &args, id),
337 ModelingCmd::from(mcmd::SurfaceBlend::builder().surfaces(surface_refs).build()),
338 )
339 .await?;
340
341 let solid = Solid {
342 id,
343 value_id: id,
344 topology_id: id,
345 pattern_source_artifact_id: None,
346 artifact_id: id.into(),
347 value: vec![],
348 faces: Default::default(),
349 creator: SolidCreator::Procedural,
350 start_cap_id: None,
351 end_cap_id: None,
352 edge_cuts: vec![],
353 pending_edge_cut_ids: vec![],
354 units: exec_state.length_unit(),
355 sectional: false,
356 meta: vec![crate::execution::Metadata {
357 source_range: args.source_range,
358 }],
359 };
360 Ok(solid)
362}
363
364pub async fn join(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
366 let selection: Vec<Solid> = args.get_unlabeled_kw_arg("selection", &RuntimeType::solids(), exec_state)?;
367 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
368
369 inner_join(selection, tolerance, exec_state, args)
370 .await
371 .map(Box::new)
372 .map(|value| KclValue::Solid { value })
373}
374
375async fn inner_join(
376 selection: Vec<Solid>,
377 tolerance: Option<TyF64>,
378 exec_state: &mut ExecState,
379 args: Args,
380) -> Result<Solid, KclError> {
381 if selection.len() == 1 {
382 let cmd = mcmd::Solid3dJoin::builder().object_id(selection[0].id).build();
383
384 exec_state
385 .batch_modeling_cmd(ModelingCmdMeta::from_args(exec_state, &args), ModelingCmd::from(cmd))
386 .await?;
387
388 Ok(selection[0].clone())
389 } else {
390 let body_out_id = exec_state.next_uuid();
391
392 exec_state
393 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &selection)
394 .await?;
395
396 let body_ids = selection.iter().map(|body| body.id).collect();
397 let tolerance = tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM);
398 let cmd = mcmd::Solid3dMultiJoin::builder()
399 .object_ids(body_ids)
400 .tolerance(LengthUnit(tolerance))
401 .build();
402
403 exec_state
404 .batch_modeling_cmd(
405 ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
406 ModelingCmd::from(cmd),
407 )
408 .await?;
409
410 let solid = Solid {
411 id: body_out_id,
412 value_id: body_out_id,
413 topology_id: body_out_id,
414 pattern_source_artifact_id: None,
415 artifact_id: body_out_id.into(),
416 value: vec![],
417 faces: Default::default(),
418 creator: SolidCreator::Procedural,
419 start_cap_id: None,
420 end_cap_id: None,
421 edge_cuts: vec![],
422 pending_edge_cut_ids: vec![],
423 units: exec_state.length_unit(),
424 sectional: false,
425 meta: vec![args.source_range.into()],
426 };
427 record_consumed_solids(
428 exec_state,
429 &selection,
430 ConsumedSolidOperation::JoinSurfaces,
431 std::slice::from_ref(&solid),
432 );
433 Ok(solid)
434 }
435}