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