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