1use anyhow::Result;
4use indexmap::IndexMap;
5use kcl_derive_docs::stdlib;
6use kcmc::{
7 each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::CutType,
8 websocket::OkWebSocketResponseData, ModelingCmd,
9};
10use kittycad_modeling_cmds as kcmc;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15use crate::{
16 errors::{KclError, KclErrorDetails},
17 execution::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier},
18 parsing::ast::types::TagNode,
19 settings::types::UnitLength,
20 std::Args,
21 SourceRange,
22};
23
24#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Hash)]
26#[ts(export)]
27#[serde(untagged)]
28pub enum EdgeReference {
29 Uuid(uuid::Uuid),
31 Tag(Box<TagIdentifier>),
33}
34
35impl EdgeReference {
36 pub fn get_engine_id(&self, exec_state: &mut ExecState, args: &Args) -> Result<uuid::Uuid, KclError> {
37 match self {
38 EdgeReference::Uuid(uuid) => Ok(*uuid),
39 EdgeReference::Tag(tag) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
40 }
41 }
42}
43
44pub(super) fn validate_unique<T: Eq + std::hash::Hash>(tags: &[(T, SourceRange)]) -> Result<(), KclError> {
45 let mut tag_counts: IndexMap<&T, Vec<SourceRange>> = Default::default();
47 for tag in tags {
48 tag_counts.entry(&tag.0).or_insert(Vec::new()).push(tag.1);
49 }
50 let mut duplicate_tags_source = Vec::new();
51 for (_tag, count) in tag_counts {
52 if count.len() > 1 {
53 duplicate_tags_source.extend(count)
54 }
55 }
56 if !duplicate_tags_source.is_empty() {
57 return Err(KclError::Type(KclErrorDetails {
58 message: "The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge".to_string(),
59 source_ranges: duplicate_tags_source,
60 }));
61 }
62 Ok(())
63}
64
65pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
67 let solid = args.get_unlabeled_kw_arg("solid")?;
69 let radius = args.get_kw_arg("radius")?;
70 let tolerance = args.get_kw_arg_opt("tolerance")?;
71 let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
72 let tag = args.get_kw_arg_opt("tag")?;
73
74 validate_unique(&tags)?;
76 let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
77 let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
78 Ok(KclValue::Solid { value })
79}
80
81#[stdlib {
138 name = "fillet",
139 feature_tree_operation = true,
140 keywords = true,
141 unlabeled_first = true,
142 args = {
143 solid = { docs = "The solid whose edges should be filletted" },
144 radius = { docs = "The radius of the fillet" },
145 tags = { docs = "The paths you want to fillet" },
146 tolerance = { docs = "The tolerance for this fillet" },
147 tag = { docs = "Create a new tag which refers to this fillet"},
148 }
149}]
150async fn inner_fillet(
151 solid: Box<Solid>,
152 radius: f64,
153 tags: Vec<EdgeReference>,
154 tolerance: Option<f64>,
155 tag: Option<TagNode>,
156 exec_state: &mut ExecState,
157 args: Args,
158) -> Result<Box<Solid>, KclError> {
159 let mut solid = solid.clone();
160 for edge_tag in tags {
161 let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
162
163 let id = exec_state.next_uuid();
164 args.batch_end_cmd(
165 id,
166 ModelingCmd::from(mcmd::Solid3dFilletEdge {
167 edge_id,
168 object_id: solid.id,
169 radius: LengthUnit(radius),
170 tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
171 cut_type: CutType::Fillet,
172 face_id: None,
174 }),
175 )
176 .await?;
177
178 solid.edge_cuts.push(EdgeCut::Fillet {
179 id,
180 edge_id,
181 radius,
182 tag: Box::new(tag.clone()),
183 });
184
185 if let Some(ref tag) = tag {
186 solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
187 face_id: id,
188 tag: Some(tag.clone()),
189 geo_meta: GeoMeta {
190 id,
191 metadata: args.source_range.into(),
192 },
193 }));
194 }
195 }
196
197 Ok(solid)
198}
199
200pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
202 let tag: TagIdentifier = args.get_data()?;
203
204 let edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?;
205 Ok(KclValue::Uuid {
206 value: edge,
207 meta: vec![args.source_range.into()],
208 })
209}
210
211#[stdlib {
239 name = "getOppositeEdge",
240}]
241async fn inner_get_opposite_edge(tag: TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<Uuid, KclError> {
242 if args.ctx.no_engine_commands().await {
243 return Ok(exec_state.next_uuid());
244 }
245 let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
246
247 let id = exec_state.next_uuid();
248 let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
249
250 let resp = args
251 .send_modeling_cmd(
252 id,
253 ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
254 edge_id: tagged_path.id,
255 object_id: tagged_path.sketch,
256 face_id,
257 }),
258 )
259 .await?;
260 let OkWebSocketResponseData::Modeling {
261 modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
262 } = &resp
263 else {
264 return Err(KclError::Engine(KclErrorDetails {
265 message: format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {:?}", resp),
266 source_ranges: vec![args.source_range],
267 }));
268 };
269
270 Ok(opposite_edge.edge)
271}
272
273pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
275 let tag: TagIdentifier = args.get_data()?;
276
277 let edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?;
278 Ok(KclValue::Uuid {
279 value: edge,
280 meta: vec![args.source_range.into()],
281 })
282}
283
284#[stdlib {
312 name = "getNextAdjacentEdge",
313}]
314async fn inner_get_next_adjacent_edge(
315 tag: TagIdentifier,
316 exec_state: &mut ExecState,
317 args: Args,
318) -> Result<Uuid, KclError> {
319 if args.ctx.no_engine_commands().await {
320 return Ok(exec_state.next_uuid());
321 }
322 let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
323
324 let id = exec_state.next_uuid();
325 let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
326
327 let resp = args
328 .send_modeling_cmd(
329 id,
330 ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
331 edge_id: tagged_path.id,
332 object_id: tagged_path.sketch,
333 face_id,
334 }),
335 )
336 .await?;
337 let OkWebSocketResponseData::Modeling {
338 modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
339 } = &resp
340 else {
341 return Err(KclError::Engine(KclErrorDetails {
342 message: format!(
343 "mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {:?}",
344 resp
345 ),
346 source_ranges: vec![args.source_range],
347 }));
348 };
349
350 adjacent_edge.edge.ok_or_else(|| {
351 KclError::Type(KclErrorDetails {
352 message: format!("No edge found next adjacent to tag: `{}`", tag.value),
353 source_ranges: vec![args.source_range],
354 })
355 })
356}
357
358pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
360 let tag: TagIdentifier = args.get_data()?;
361
362 let edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?;
363 Ok(KclValue::Uuid {
364 value: edge,
365 meta: vec![args.source_range.into()],
366 })
367}
368
369#[stdlib {
397 name = "getPreviousAdjacentEdge",
398}]
399async fn inner_get_previous_adjacent_edge(
400 tag: TagIdentifier,
401 exec_state: &mut ExecState,
402 args: Args,
403) -> Result<Uuid, KclError> {
404 if args.ctx.no_engine_commands().await {
405 return Ok(exec_state.next_uuid());
406 }
407 let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
408
409 let id = exec_state.next_uuid();
410 let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
411
412 let resp = args
413 .send_modeling_cmd(
414 id,
415 ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge {
416 edge_id: tagged_path.id,
417 object_id: tagged_path.sketch,
418 face_id,
419 }),
420 )
421 .await?;
422 let OkWebSocketResponseData::Modeling {
423 modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
424 } = &resp
425 else {
426 return Err(KclError::Engine(KclErrorDetails {
427 message: format!(
428 "mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {:?}",
429 resp
430 ),
431 source_ranges: vec![args.source_range],
432 }));
433 };
434
435 adjacent_edge.edge.ok_or_else(|| {
436 KclError::Type(KclErrorDetails {
437 message: format!("No edge found previous adjacent to tag: `{}`", tag.value),
438 source_ranges: vec![args.source_range],
439 })
440 })
441}
442
443pub(crate) fn default_tolerance(units: &UnitLength) -> f64 {
444 match units {
445 UnitLength::Mm => 0.0000001,
446 UnitLength::Cm => 0.0000001,
447 UnitLength::In => 0.0000001,
448 UnitLength::Ft => 0.0001,
449 UnitLength::Yd => 0.001,
450 UnitLength::M => 0.001,
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_validate_unique() {
460 let dup_a = SourceRange::from([1, 3, 0]);
461 let dup_b = SourceRange::from([10, 30, 0]);
462 let tags = vec![("abc", dup_a), ("abc", dup_b), ("def", SourceRange::from([2, 4, 0]))];
464 let actual = validate_unique(&tags);
465 let expected = vec![dup_a, dup_b];
469 assert_eq!(actual.err().unwrap().source_ranges(), expected);
470 }
471}