1use anyhow::Result;
4use kcmc::ModelingCmd;
5use kcmc::each_cmd as mcmd;
6use kcmc::ok_response::OkModelingCmdResponse;
7use kcmc::websocket::OkWebSocketResponseData;
8use kittycad_modeling_cmds as kcmc;
9use uuid::Uuid;
10
11use crate::SourceRange;
12use crate::errors::KclError;
13use crate::errors::KclErrorDetails;
14use crate::execution::BoundedEdge;
15use crate::execution::ExecState;
16use crate::execution::ExtrudeSurface;
17use crate::execution::KclValue;
18use crate::execution::ModelingCmdMeta;
19use crate::execution::Solid;
20use crate::execution::TagIdentifier;
21use crate::execution::types::ArrayLen;
22use crate::execution::types::RuntimeType;
23use crate::std::Args;
24use crate::std::args::TyF64;
25use crate::std::fillet::EdgeReference;
26use crate::std::sketch::FaceTag;
27
28pub(super) fn check_tag_not_ambiguous(tag: &TagIdentifier, args: &Args) -> Result<(), KclError> {
30 let all_infos = tag.get_all_cur_info();
31 if all_infos.len() > 1 {
32 return Err(KclError::new_semantic(KclErrorDetails::new(
33 format!(
34 "Tag `{}` is ambiguous: it maps to {} edges in the region. Use a more specific reference.",
35 tag.value,
36 all_infos.len()
37 ),
38 vec![args.source_range],
39 )));
40 }
41 Ok(())
42}
43
44pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
46 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
47
48 let edge = inner_get_opposite_edge(input_edge, exec_state, args.clone()).await?;
49 Ok(KclValue::Uuid {
50 value: edge,
51 meta: vec![args.source_range.into()],
52 })
53}
54
55async fn inner_get_opposite_edge(
56 edge: TagIdentifier,
57 exec_state: &mut ExecState,
58 args: Args,
59) -> Result<Uuid, KclError> {
60 check_tag_not_ambiguous(&edge, &args)?;
61 if args.ctx.no_engine_commands().await {
62 return Ok(exec_state.next_uuid());
63 }
64 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
65
66 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
67 let tagged_path_id = tagged_path.id;
68 let sketch_id = tagged_path.geometry.id();
69
70 let resp = exec_state
71 .send_modeling_cmd(
72 ModelingCmdMeta::from_args(exec_state, &args),
73 ModelingCmd::from(
74 mcmd::Solid3dGetOppositeEdge::builder()
75 .edge_id(tagged_path_id)
76 .object_id(sketch_id)
77 .face_id(face_id)
78 .build(),
79 ),
80 )
81 .await?;
82 let OkWebSocketResponseData::Modeling {
83 modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
84 } = &resp
85 else {
86 return Err(KclError::new_engine(KclErrorDetails::new(
87 format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {resp:?}"),
88 vec![args.source_range],
89 )));
90 };
91
92 Ok(opposite_edge.edge)
93}
94
95pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
97 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
98
99 let edge = inner_get_next_adjacent_edge(input_edge, exec_state, args.clone()).await?;
100 Ok(KclValue::Uuid {
101 value: edge,
102 meta: vec![args.source_range.into()],
103 })
104}
105
106async fn inner_get_next_adjacent_edge(
107 edge: TagIdentifier,
108 exec_state: &mut ExecState,
109 args: Args,
110) -> Result<Uuid, KclError> {
111 check_tag_not_ambiguous(&edge, &args)?;
112 if args.ctx.no_engine_commands().await {
113 return Ok(exec_state.next_uuid());
114 }
115 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
116
117 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
118 let tagged_path_id = tagged_path.id;
119 let sketch_id = tagged_path.geometry.id();
120
121 let resp = exec_state
122 .send_modeling_cmd(
123 ModelingCmdMeta::from_args(exec_state, &args),
124 ModelingCmd::from(
125 mcmd::Solid3dGetNextAdjacentEdge::builder()
126 .edge_id(tagged_path_id)
127 .object_id(sketch_id)
128 .face_id(face_id)
129 .build(),
130 ),
131 )
132 .await?;
133
134 let OkWebSocketResponseData::Modeling {
135 modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
136 } = &resp
137 else {
138 return Err(KclError::new_engine(KclErrorDetails::new(
139 format!("mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {resp:?}"),
140 vec![args.source_range],
141 )));
142 };
143
144 adjacent_edge.edge.ok_or_else(|| {
145 KclError::new_type(KclErrorDetails::new(
146 format!("No edge found next adjacent to tag: `{}`", edge.value),
147 vec![args.source_range],
148 ))
149 })
150}
151
152pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
154 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
155
156 let edge = inner_get_previous_adjacent_edge(input_edge, exec_state, args.clone()).await?;
157 Ok(KclValue::Uuid {
158 value: edge,
159 meta: vec![args.source_range.into()],
160 })
161}
162
163async fn inner_get_previous_adjacent_edge(
164 edge: TagIdentifier,
165 exec_state: &mut ExecState,
166 args: Args,
167) -> Result<Uuid, KclError> {
168 check_tag_not_ambiguous(&edge, &args)?;
169 if args.ctx.no_engine_commands().await {
170 return Ok(exec_state.next_uuid());
171 }
172 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
173
174 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
175 let tagged_path_id = tagged_path.id;
176 let sketch_id = tagged_path.geometry.id();
177
178 let resp = exec_state
179 .send_modeling_cmd(
180 ModelingCmdMeta::from_args(exec_state, &args),
181 ModelingCmd::from(
182 mcmd::Solid3dGetPrevAdjacentEdge::builder()
183 .edge_id(tagged_path_id)
184 .object_id(sketch_id)
185 .face_id(face_id)
186 .build(),
187 ),
188 )
189 .await?;
190 let OkWebSocketResponseData::Modeling {
191 modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
192 } = &resp
193 else {
194 return Err(KclError::new_engine(KclErrorDetails::new(
195 format!("mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {resp:?}"),
196 vec![args.source_range],
197 )));
198 };
199
200 adjacent_edge.edge.ok_or_else(|| {
201 KclError::new_type(KclErrorDetails::new(
202 format!("No edge found previous adjacent to tag: `{}`", edge.value),
203 vec![args.source_range],
204 ))
205 })
206}
207
208pub async fn get_common_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
210 let faces: Vec<FaceTag> = args.get_kw_arg(
211 "faces",
212 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Known(2)),
213 exec_state,
214 )?;
215
216 fn into_tag(face: FaceTag, source_range: SourceRange) -> Result<TagIdentifier, KclError> {
217 match face {
218 FaceTag::StartOrEnd(_) => Err(KclError::new_type(KclErrorDetails::new(
219 "getCommonEdge requires a tagged face, it cannot use `START` or `END` faces".to_owned(),
220 vec![source_range],
221 ))),
222 FaceTag::Tag(tag_identifier) => Ok(*tag_identifier),
223 }
224 }
225
226 let [face1, face2]: [FaceTag; 2] = faces.try_into().map_err(|_: Vec<FaceTag>| {
227 KclError::new_type(KclErrorDetails::new(
228 "getCommonEdge requires exactly two tags for faces".to_owned(),
229 vec![args.source_range],
230 ))
231 })?;
232
233 let face1 = into_tag(face1, args.source_range)?;
234 let face2 = into_tag(face2, args.source_range)?;
235
236 let edge = inner_get_common_edge(face1, face2, exec_state, args.clone()).await?;
237 Ok(KclValue::Uuid {
238 value: edge,
239 meta: vec![args.source_range.into()],
240 })
241}
242
243async fn inner_get_common_edge(
244 face1: TagIdentifier,
245 face2: TagIdentifier,
246 exec_state: &mut ExecState,
247 args: Args,
248) -> Result<Uuid, KclError> {
249 check_tag_not_ambiguous(&face1, &args)?;
250 check_tag_not_ambiguous(&face2, &args)?;
251 let id = exec_state.next_uuid();
252 if args.ctx.no_engine_commands().await {
253 return Ok(id);
254 }
255
256 let first_face_id = args.get_adjacent_face_to_tag(exec_state, &face1, false).await?;
257 let second_face_id = args.get_adjacent_face_to_tag(exec_state, &face2, false).await?;
258
259 let first_tagged_path = args.get_tag_engine_info(exec_state, &face1)?.clone();
260 let second_tagged_path = args.get_tag_engine_info(exec_state, &face2)?;
261
262 if first_tagged_path.geometry.id() != second_tagged_path.geometry.id() {
263 return Err(KclError::new_type(KclErrorDetails::new(
264 "getCommonEdge requires the faces to be in the same original sketch".to_string(),
265 vec![args.source_range],
266 )));
267 }
268
269 if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface {
274 exec_state
275 .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
276 .await?;
277 } else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface {
278 exec_state
279 .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
280 .await?;
281 }
282
283 let resp = exec_state
284 .send_modeling_cmd(
285 ModelingCmdMeta::from_args_id(exec_state, &args, id),
286 ModelingCmd::from(
287 mcmd::Solid3dGetCommonEdge::builder()
288 .object_id(first_tagged_path.geometry.id())
289 .face_ids([first_face_id, second_face_id])
290 .build(),
291 ),
292 )
293 .await?;
294 let OkWebSocketResponseData::Modeling {
295 modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
296 } = &resp
297 else {
298 return Err(KclError::new_engine(KclErrorDetails::new(
299 format!("mcmd::Solid3dGetCommonEdge response was not as expected: {resp:?}"),
300 vec![args.source_range],
301 )));
302 };
303
304 common_edge.edge.ok_or_else(|| {
305 KclError::new_type(KclErrorDetails::new(
306 format!(
307 "No common edge was found between `{}` and `{}`",
308 face1.value, face2.value
309 ),
310 vec![args.source_range],
311 ))
312 })
313}
314
315pub async fn get_bounded_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
316 let face = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
317 let edge = args.get_kw_arg("edge", &RuntimeType::edge(), exec_state)?;
318 let lower_bound = args.get_kw_arg_opt("lowerBound", &RuntimeType::num_any(), exec_state)?;
319 let upper_bound = args.get_kw_arg_opt("upperBound", &RuntimeType::num_any(), exec_state)?;
320
321 let bounded_edge = inner_get_bounded_edge(face, edge, lower_bound, upper_bound, exec_state, args.clone()).await?;
322 Ok(KclValue::BoundedEdge {
323 value: bounded_edge,
324 meta: vec![args.source_range.into()],
325 })
326}
327
328pub async fn inner_get_bounded_edge(
329 face: Solid,
330 edge: EdgeReference,
331 lower_bound: Option<TyF64>,
332 upper_bound: Option<TyF64>,
333 exec_state: &mut ExecState,
334 args: Args,
335) -> Result<BoundedEdge, KclError> {
336 let lower_bound = if let Some(lower_bound) = lower_bound {
337 let val = lower_bound.n as f32;
338 if !(0.0..=1.0).contains(&val) {
339 return Err(KclError::new_semantic(KclErrorDetails::new(
340 format!(
341 "Invalid value: lowerBound must be between 0.0 and 1.0, provided {}",
342 val
343 ),
344 vec![args.source_range],
345 )));
346 }
347 val
348 } else {
349 0.0_f32
350 };
351
352 let upper_bound = if let Some(upper_bound) = upper_bound {
353 let val = upper_bound.n as f32;
354 if !(0.0..=1.0).contains(&val) {
355 return Err(KclError::new_semantic(KclErrorDetails::new(
356 format!(
357 "Invalid value: upperBound must be between 0.0 and 1.0, provided {}",
358 val
359 ),
360 vec![args.source_range],
361 )));
362 }
363 val
364 } else {
365 1.0_f32
366 };
367
368 Ok(BoundedEdge {
369 face_id: face.id,
370 edge_id: edge.get_engine_id(exec_state, &args)?,
371 lower_bound,
372 upper_bound,
373 })
374}