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 if args.ctx.no_engine_commands().await {
89 return if let Some(body_type) = surface.best_guess_body_type {
92 Ok(expected == body_type)
93 } else {
94 Ok(false)
98 };
99 }
100
101 let meta = ModelingCmdMeta::from_args(exec_state, &args);
102 let cmd = ModelingCmd::from(mcmd::Solid3dGetBodyType::builder().object_id(surface.id).build());
103
104 let response = exec_state.send_modeling_cmd(meta, cmd).await?;
105
106 let OkWebSocketResponseData::Modeling {
107 modeling_response: OkModelingCmdResponse::Solid3dGetBodyType(body),
108 } = response
109 else {
110 return Err(KclError::new_semantic(KclErrorDetails::new(
111 format!(
112 "Engine returned invalid response, it should have returned Solid3dGetBodyType but it returned {response:#?}"
113 ),
114 vec![args.source_range],
115 )));
116 };
117
118 Ok(expected == body.body_type)
119}
120
121pub async fn delete_face(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
122 let body = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
123 let faces: Option<Vec<FaceTag>> = args.get_kw_arg_opt(
124 "faces",
125 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
126 exec_state,
127 )?;
128 let face_indices: Option<Vec<TyF64>> = args.get_kw_arg_opt(
129 "faceIndices",
130 &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Minimum(1)),
131 exec_state,
132 )?;
133 let face_indices = if let Some(face_indices) = face_indices {
134 let faces = face_indices
135 .into_iter()
136 .map(|num| {
137 crate::try_f64_to_u32(num.n).ok_or_else(|| {
138 KclError::new_semantic(KclErrorDetails::new(
139 format!("Face indices must be whole numbers, got {}", num.n),
140 vec![args.source_range],
141 ))
142 })
143 })
144 .collect::<Result<Vec<_>, _>>()?;
145 Some(faces)
146 } else {
147 None
148 };
149 inner_delete_face(body, faces, face_indices, exec_state, args)
150 .await
151 .map(Box::new)
152 .map(|value| KclValue::Solid { value })
153}
154
155async fn inner_delete_face(
156 mut body: Solid,
157 tagged_faces: Option<Vec<FaceTag>>,
158 face_indices: Option<Vec<u32>>,
159 exec_state: &mut ExecState,
160 args: Args,
161) -> Result<Solid, KclError> {
162 if tagged_faces.is_none() && face_indices.is_none() {
165 return Err(KclError::new_semantic(KclErrorDetails::new(
166 "You must use either the `faces` or the `faceIndices` parameter".to_string(),
167 vec![args.source_range],
168 )));
169 }
170
171 let no_engine_commands = args.ctx.no_engine_commands().await;
174 if no_engine_commands {
175 return Ok(body);
176 }
177
178 exec_state
183 .flush_batch_for_solids(
184 ModelingCmdMeta::from_args(exec_state, &args),
185 std::slice::from_ref(&body),
186 )
187 .await?;
188
189 let tagged_faces = tagged_faces.unwrap_or_default();
191 let face_indices = face_indices.unwrap_or_default();
192 let mut face_ids = HashSet::with_capacity(face_indices.len() + tagged_faces.len());
194
195 for tagged_face in tagged_faces {
196 let face_id = tagged_face.get_face_id(&body, exec_state, &args, false).await?;
197 face_ids.insert(face_id);
198 }
199
200 for face_index in face_indices {
201 let face_uuid_response = exec_state
202 .send_modeling_cmd(
203 ModelingCmdMeta::from_args(exec_state, &args),
204 ModelingCmd::from(
205 mcmd::Solid3dGetFaceUuid::builder()
206 .object_id(body.id)
207 .face_index(face_index)
208 .build(),
209 ),
210 )
211 .await?;
212
213 let OkWebSocketResponseData::Modeling {
214 modeling_response: OkModelingCmdResponse::Solid3dGetFaceUuid(inner_resp),
215 } = face_uuid_response
216 else {
217 return Err(KclError::new_semantic(KclErrorDetails::new(
218 format!(
219 "Engine returned invalid response, it should have returned Solid3dGetFaceUuid but it returned {face_uuid_response:?}"
220 ),
221 vec![args.source_range],
222 )));
223 };
224 face_ids.insert(inner_resp.face_id);
225 }
226
227 let delete_face_response = exec_state
229 .send_modeling_cmd(
230 ModelingCmdMeta::from_args(exec_state, &args),
231 ModelingCmd::from(
232 mcmd::EntityDeleteChildren::builder()
233 .entity_id(body.id)
234 .child_entity_ids(face_ids)
235 .build(),
236 ),
237 )
238 .await?;
239
240 let OkWebSocketResponseData::Modeling {
241 modeling_response: OkModelingCmdResponse::EntityDeleteChildren(mout::EntityDeleteChildren { .. }),
242 } = delete_face_response
243 else {
244 return Err(KclError::new_semantic(KclErrorDetails::new(
245 format!(
246 "Engine returned invalid response, it should have returned EntityDeleteChildren but it returned {delete_face_response:?}"
247 ),
248 vec![args.source_range],
249 )));
250 };
251
252 body.best_guess_body_type = Some(BodyType::Surface);
257 Ok(body)
258}
259
260pub async fn blend(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
262 let edges: Vec<KclValue> = args.get_unlabeled_kw_arg(
263 "edges",
264 &RuntimeType::Array(
265 Box::new(RuntimeType::Union(vec![
266 RuntimeType::Primitive(PrimitiveType::BoundedEdge),
267 RuntimeType::tagged_edge(),
268 RuntimeType::Primitive(PrimitiveType::Any),
269 ])),
270 ArrayLen::Known(2),
271 ),
272 exec_state,
273 )?;
274
275 let mut bounded_edges = Vec::with_capacity(edges.len());
276 for edge in edges {
277 bounded_edges.push(resolve_blend_edge(edge, exec_state, &args).await?);
278 }
279
280 inner_blend(bounded_edges, exec_state, args.clone())
281 .await
282 .map(Box::new)
283 .map(|value| KclValue::Solid { value })
284}
285
286async fn resolve_blend_edge(edge: KclValue, exec_state: &mut ExecState, args: &Args) -> Result<BoundedEdge, KclError> {
289 match edge {
290 KclValue::BoundedEdge { value, .. } => Ok(value),
291 KclValue::TagIdentifier(tag) => {
292 let tagged_edge = args.get_tag_engine_info(exec_state, &tag)?;
293 Ok(BoundedEdge {
294 face_id: tagged_edge.geometry.id(),
295 edge_id: Some(tagged_edge.id),
296 edge_specifier: None,
297 lower_bound: 0.0,
298 upper_bound: 1.0,
299 })
300 }
301 KclValue::Object { value: obj, .. } => {
302 let spec = edge::parse_edge_specifier_object(&obj, args)?;
303 let face_id = edge::face_id_from_first_side_face(&spec, exec_state, args)?;
304 Ok(BoundedEdge {
305 face_id,
306 edge_id: None,
307 edge_specifier: Some(spec),
308 lower_bound: 0.0,
309 upper_bound: 1.0,
310 })
311 }
312 _ => Err(KclError::new_internal(KclErrorDetails::new(
313 "Unexpected edge value while preparing blend edges. Expected BoundedEdge, tagged edge, or edge specifier object (e.g. { sideFaces = [...], endFaces = [...], index = 0 }).".to_owned(),
314 vec![args.source_range],
315 ))),
316 }
317}
318
319async fn inner_blend(edges: Vec<BoundedEdge>, exec_state: &mut ExecState, args: Args) -> Result<Solid, KclError> {
320 let id = exec_state.next_uuid();
321
322 let mut surface_refs = Vec::with_capacity(edges.len());
323 for edge in &edges {
324 let fraction = if let Some(eid) = edge.edge_id {
325 FractionOfEdge::builder()
326 .edge_id(eid)
327 .lower_bound(edge.lower_bound)
328 .upper_bound(edge.upper_bound)
329 .build()
330 } else if let Some(ref spec) = edge.edge_specifier {
331 let resolved = edge::resolve_unresolved_edge_specifier(edge.face_id, spec, exec_state, &args).await?;
332 FractionOfEdge::builder()
333 .edge_specifier(resolved)
334 .lower_bound(edge.lower_bound)
335 .upper_bound(edge.upper_bound)
336 .build()
337 } else {
338 return Err(KclError::new_internal(KclErrorDetails::new(
339 "BoundedEdge must have edge_id or edge_specifier".to_owned(),
340 vec![args.source_range],
341 )));
342 };
343 surface_refs.push(
344 SurfaceEdgeReference::builder()
345 .object_id(edge.face_id)
346 .edges(vec![fraction])
347 .build(),
348 );
349 }
350
351 exec_state
352 .batch_modeling_cmd(
353 ModelingCmdMeta::from_args_id(exec_state, &args, id),
354 ModelingCmd::from(mcmd::SurfaceBlend::builder().surfaces(surface_refs).build()),
355 )
356 .await?;
357
358 let solid = Solid {
359 id,
360 value_id: id,
361 topology_id: id,
362 pattern_source_artifact_id: None,
363 best_guess_body_type: Some(BodyType::Surface),
364 artifact_id: id.into(),
365 value: vec![],
366 faces: Default::default(),
367 creator: SolidCreator::Procedural,
368 start_cap_id: None,
369 end_cap_id: None,
370 edge_cuts: vec![],
371 pending_edge_cut_ids: vec![],
372 units: exec_state.length_unit(),
373 sectional: false,
374 meta: vec![crate::execution::Metadata {
375 source_range: args.source_range,
376 }],
377 };
378 Ok(solid)
380}
381
382pub async fn join(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
384 let selection: Vec<Solid> = args.get_unlabeled_kw_arg("selection", &RuntimeType::solids(), exec_state)?;
385 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
386
387 inner_join(selection, tolerance, exec_state, args)
388 .await
389 .map(Box::new)
390 .map(|value| KclValue::Solid { value })
391}
392
393async fn inner_join(
394 selection: Vec<Solid>,
395 tolerance: Option<TyF64>,
396 exec_state: &mut ExecState,
397 args: Args,
398) -> Result<Solid, KclError> {
399 if selection.len() == 1 {
400 let cmd = mcmd::Solid3dJoin::builder().object_id(selection[0].id).build();
401
402 exec_state
403 .batch_modeling_cmd(ModelingCmdMeta::from_args(exec_state, &args), ModelingCmd::from(cmd))
404 .await?;
405
406 Ok(selection[0].clone())
407 } else {
408 let body_out_id = exec_state.next_uuid();
409
410 exec_state
411 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &selection)
412 .await?;
413
414 let body_ids = selection.iter().map(|body| body.id).collect();
415 let tolerance = tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM);
416 let cmd = mcmd::Solid3dMultiJoin::builder()
417 .object_ids(body_ids)
418 .tolerance(LengthUnit(tolerance))
419 .build();
420
421 exec_state
422 .batch_modeling_cmd(
423 ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
424 ModelingCmd::from(cmd),
425 )
426 .await?;
427
428 let solid = Solid {
429 id: body_out_id,
430 value_id: body_out_id,
431 topology_id: body_out_id,
432 pattern_source_artifact_id: None,
433 best_guess_body_type: None,
434 artifact_id: body_out_id.into(),
435 value: vec![],
436 faces: Default::default(),
437 creator: SolidCreator::Procedural,
438 start_cap_id: None,
439 end_cap_id: None,
440 edge_cuts: vec![],
441 pending_edge_cut_ids: vec![],
442 units: exec_state.length_unit(),
443 sectional: false,
444 meta: vec![args.source_range.into()],
445 };
446 record_consumed_solids(
447 exec_state,
448 &selection,
449 ConsumedSolidOperation::JoinSurfaces,
450 std::slice::from_ref(&solid),
451 );
452 Ok(solid)
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use crate::execution::MockConfig;
459
460 #[tokio::test(flavor = "multi_thread")]
461 async fn mock_body_type_queries_use_locally_known_type() {
462 let code = r#"
463@settings(defaultLengthUnit = mm, kclVersion = 2.0)
464
465solidSketch = sketch(on = XY) {
466 profile = circle(
467 start = [var 5mm, var 0mm],
468 center = [var 0mm, var 0mm],
469 )
470}
471solid = extrude(
472 region(segments = [solidSketch.profile]),
473 length = 5mm,
474 bodyType = "solid",
475)
476
477surfaceSketch = sketch(on = XY) {
478 profile = circle(
479 start = [var 20mm, var 0mm],
480 center = [var 15mm, var 0mm],
481 )
482}
483surface = extrude(
484 region(segments = [surfaceSketch.profile]),
485 length = 5mm,
486 bodyType = "surface",
487)
488
489assertIs(isSolid(solid))
490assertIs(!isSurface(solid))
491assertIs(isSurface(surface))
492assertIs(!isSolid(surface))
493"#;
494
495 let program = crate::Program::parse_no_errs(code).unwrap();
496 let ctx = crate::ExecutorContext::new_mock(None).await;
497 let result = ctx.run_mock(&program, &MockConfig::default()).await;
498 ctx.close().await;
499 result.unwrap();
500 }
501}