2024-03-05 11:52:45 -08:00
//! Standard library fillets.
use anyhow ::Result ;
2025-03-12 11:24:27 -05:00
use indexmap ::IndexMap ;
2025-06-26 17:02:54 -05:00
use kcmc ::{ ModelingCmd , each_cmd as mcmd , length_unit ::LengthUnit , shared ::CutType } ;
2024-09-18 17:04:04 -05:00
use kittycad_modeling_cmds as kcmc ;
2024-03-05 11:52:45 -08:00
use serde ::{ Deserialize , Serialize } ;
2025-06-26 17:02:54 -05:00
use super ::{ DEFAULT_TOLERANCE_MM , args ::TyF64 } ;
2024-03-05 11:52:45 -08:00
use crate ::{
2025-06-26 17:02:54 -05:00
SourceRange ,
2024-03-05 11:52:45 -08:00
errors ::{ KclError , KclErrorDetails } ,
2025-03-17 17:57:26 +13:00
execution ::{
2025-06-26 17:02:54 -05:00
EdgeCut , ExecState , ExtrudeSurface , FilletSurface , GeoMeta , KclValue , ModelingCmdMeta , Solid , TagIdentifier ,
types ::RuntimeType ,
2025-03-17 17:57:26 +13:00
} ,
2024-12-05 17:56:49 +13:00
parsing ::ast ::types ::TagNode ,
2024-03-05 11:52:45 -08:00
std ::Args ,
} ;
2024-06-24 14:45:07 -07:00
/// A tag or a uuid of an edge.
2025-04-28 17:42:01 -05:00
#[ derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash) ]
2024-03-05 11:52:45 -08:00
#[ serde(untagged) ]
2024-03-26 19:07:16 -07:00
pub enum EdgeReference {
/// A uuid of an edge.
Uuid ( uuid ::Uuid ) ,
2024-06-24 14:45:07 -07:00
/// A tag of an edge.
2024-07-27 22:56:46 -07:00
Tag ( Box < TagIdentifier > ) ,
2024-03-05 11:52:45 -08:00
}
2024-09-25 16:12:18 -07:00
impl EdgeReference {
pub fn get_engine_id ( & self , exec_state : & mut ExecState , args : & Args ) -> Result < uuid ::Uuid , KclError > {
match self {
EdgeReference ::Uuid ( uuid ) = > Ok ( * uuid ) ,
EdgeReference ::Tag ( tag ) = > Ok ( args . get_tag_engine_info ( exec_state , tag ) ? . id ) ,
}
}
}
2025-03-12 11:24:27 -05:00
pub ( super ) fn validate_unique < T : Eq + std ::hash ::Hash > ( tags : & [ ( T , SourceRange ) ] ) -> Result < ( ) , KclError > {
// Check if tags contains any duplicate values.
let mut tag_counts : IndexMap < & T , Vec < SourceRange > > = Default ::default ( ) ;
for tag in tags {
tag_counts . entry ( & tag . 0 ) . or_insert ( Vec ::new ( ) ) . push ( tag . 1 ) ;
}
let mut duplicate_tags_source = Vec ::new ( ) ;
for ( _tag , count ) in tag_counts {
if count . len ( ) > 1 {
duplicate_tags_source . extend ( count )
}
}
if ! duplicate_tags_source . is_empty ( ) {
2025-06-02 13:30:57 -05:00
return Err ( KclError ::new_type ( KclErrorDetails ::new (
2025-05-19 14:13:10 -04:00
" The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge "
. to_string ( ) ,
duplicate_tags_source ,
) ) ) ;
2025-03-12 11:24:27 -05:00
}
Ok ( ( ) )
}
2024-03-05 11:52:45 -08:00
/// Create fillets on tagged paths.
2024-09-16 15:10:33 -04:00
pub async fn fillet ( exec_state : & mut ExecState , args : Args ) -> Result < KclValue , KclError > {
2025-06-06 10:45:58 +12:00
let solid = args . get_unlabeled_kw_arg ( " solid " , & RuntimeType ::solid ( ) , exec_state ) ? ;
let radius : TyF64 = args . get_kw_arg ( " radius " , & RuntimeType ::length ( ) , exec_state ) ? ;
let tolerance : Option < TyF64 > = args . get_kw_arg_opt ( " tolerance " , & RuntimeType ::length ( ) , exec_state ) ? ;
2025-05-29 16:48:47 +12:00
let tags = args . kw_arg_edge_array_and_source ( " tags " ) ? ;
2025-06-06 10:45:58 +12:00
let tag = args . get_kw_arg_opt ( " tag " , & RuntimeType ::tag_decl ( ) , exec_state ) ? ;
2025-03-12 11:24:27 -05:00
// Run the function.
validate_unique ( & tags ) ? ;
let tags : Vec < EdgeReference > = tags . into_iter ( ) . map ( | item | item . 0 ) . collect ( ) ;
2025-04-23 10:58:35 +12:00
let value = inner_fillet ( solid , radius , tags , tolerance , tag , exec_state , args ) . await ? ;
2025-01-22 09:42:09 +13:00
Ok ( KclValue ::Solid { value } )
2024-03-05 11:52:45 -08:00
}
async fn inner_fillet (
2024-09-27 15:44:44 -07:00
solid : Box < Solid > ,
2025-04-14 05:58:19 -04:00
radius : TyF64 ,
2025-02-21 14:41:25 -06:00
tags : Vec < EdgeReference > ,
2025-04-23 10:58:35 +12:00
tolerance : Option < TyF64 > ,
2024-10-30 16:52:17 -04:00
tag : Option < TagNode > ,
2024-09-16 15:10:33 -04:00
exec_state : & mut ExecState ,
2024-03-05 11:52:45 -08:00
args : Args ,
2024-09-27 15:44:44 -07:00
) -> Result < Box < Solid > , KclError > {
Do multiple chamfer/fillet in one API call (#6750)
KCL's `fillet` function takes an array of edges to fillet. Previously this would do `n` fillet API commands, one per edge. This PR combines them all into one call, which should improve performance. You can see the effect in the artifact_commands snapshots, e.g. `rust/kcl-lib/tests/kcl_samples/axial-fan/artifact_commands.snap`
Besides performance, this should fix a bug where some KCL fillets would fail, when they should have succeeded. Example from @max-mrgrsk:
```kcl
sketch001 = startSketchOn(XY)
|> startProfile(at = [-12, -6])
|> line(end = [0, 12], tag = $seg04)
|> line(end = [24, 0], tag = $seg03)
|> line(end = [0, -12], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(
sketch001,
length = 12,
tagEnd = $capEnd001,
tagStart = $capStart001,
)
|> fillet(
radius = 5,
tags = [
getCommonEdge(faces = [seg02, capEnd001]),
getCommonEdge(faces = [seg01, capEnd001]),
getCommonEdge(faces = [seg03, capEnd001]),
getCommonEdge(faces = [seg04, capEnd001])
],
)
```
This program fails on main, but succeeds on this branch.
2025-05-22 16:25:55 -05:00
// If you try and tag multiple edges with a tagged fillet, we want to return an
// error to the user that they can only tag one edge at a time.
if tag . is_some ( ) & & tags . len ( ) > 1 {
2025-06-02 13:30:57 -05:00
return Err ( KclError ::new_type ( KclErrorDetails {
Do multiple chamfer/fillet in one API call (#6750)
KCL's `fillet` function takes an array of edges to fillet. Previously this would do `n` fillet API commands, one per edge. This PR combines them all into one call, which should improve performance. You can see the effect in the artifact_commands snapshots, e.g. `rust/kcl-lib/tests/kcl_samples/axial-fan/artifact_commands.snap`
Besides performance, this should fix a bug where some KCL fillets would fail, when they should have succeeded. Example from @max-mrgrsk:
```kcl
sketch001 = startSketchOn(XY)
|> startProfile(at = [-12, -6])
|> line(end = [0, 12], tag = $seg04)
|> line(end = [24, 0], tag = $seg03)
|> line(end = [0, -12], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(
sketch001,
length = 12,
tagEnd = $capEnd001,
tagStart = $capStart001,
)
|> fillet(
radius = 5,
tags = [
getCommonEdge(faces = [seg02, capEnd001]),
getCommonEdge(faces = [seg01, capEnd001]),
getCommonEdge(faces = [seg03, capEnd001]),
getCommonEdge(faces = [seg04, capEnd001])
],
)
```
This program fails on main, but succeeds on this branch.
2025-05-22 16:25:55 -05:00
message : " You can only tag one edge at a time with a tagged fillet. Either delete the tag for the fillet fn if you don't need it OR separate into individual fillet functions for each tag. " . to_string ( ) ,
source_ranges : vec ! [ args . source_range ] ,
backtrace : Default ::default ( ) ,
} ) ) ;
}
if tags . is_empty ( ) {
2025-06-02 13:30:57 -05:00
return Err ( KclError ::new_semantic ( KclErrorDetails {
Do multiple chamfer/fillet in one API call (#6750)
KCL's `fillet` function takes an array of edges to fillet. Previously this would do `n` fillet API commands, one per edge. This PR combines them all into one call, which should improve performance. You can see the effect in the artifact_commands snapshots, e.g. `rust/kcl-lib/tests/kcl_samples/axial-fan/artifact_commands.snap`
Besides performance, this should fix a bug where some KCL fillets would fail, when they should have succeeded. Example from @max-mrgrsk:
```kcl
sketch001 = startSketchOn(XY)
|> startProfile(at = [-12, -6])
|> line(end = [0, 12], tag = $seg04)
|> line(end = [24, 0], tag = $seg03)
|> line(end = [0, -12], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(
sketch001,
length = 12,
tagEnd = $capEnd001,
tagStart = $capStart001,
)
|> fillet(
radius = 5,
tags = [
getCommonEdge(faces = [seg02, capEnd001]),
getCommonEdge(faces = [seg01, capEnd001]),
getCommonEdge(faces = [seg03, capEnd001]),
getCommonEdge(faces = [seg04, capEnd001])
],
)
```
This program fails on main, but succeeds on this branch.
2025-05-22 16:25:55 -05:00
source_ranges : vec ! [ args . source_range ] ,
message : " You must fillet at least one tag " . to_owned ( ) ,
backtrace : Default ::default ( ) ,
} ) ) ;
}
2024-09-27 15:44:44 -07:00
let mut solid = solid . clone ( ) ;
Do multiple chamfer/fillet in one API call (#6750)
KCL's `fillet` function takes an array of edges to fillet. Previously this would do `n` fillet API commands, one per edge. This PR combines them all into one call, which should improve performance. You can see the effect in the artifact_commands snapshots, e.g. `rust/kcl-lib/tests/kcl_samples/axial-fan/artifact_commands.snap`
Besides performance, this should fix a bug where some KCL fillets would fail, when they should have succeeded. Example from @max-mrgrsk:
```kcl
sketch001 = startSketchOn(XY)
|> startProfile(at = [-12, -6])
|> line(end = [0, 12], tag = $seg04)
|> line(end = [24, 0], tag = $seg03)
|> line(end = [0, -12], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(
sketch001,
length = 12,
tagEnd = $capEnd001,
tagStart = $capStart001,
)
|> fillet(
radius = 5,
tags = [
getCommonEdge(faces = [seg02, capEnd001]),
getCommonEdge(faces = [seg01, capEnd001]),
getCommonEdge(faces = [seg03, capEnd001]),
getCommonEdge(faces = [seg04, capEnd001])
],
)
```
This program fails on main, but succeeds on this branch.
2025-05-22 16:25:55 -05:00
let edge_ids = tags
. into_iter ( )
. map ( | edge_tag | edge_tag . get_engine_id ( exec_state , & args ) )
. collect ::< Result < Vec < _ > , _ > > ( ) ? ;
2024-03-05 11:52:45 -08:00
Do multiple chamfer/fillet in one API call (#6750)
KCL's `fillet` function takes an array of edges to fillet. Previously this would do `n` fillet API commands, one per edge. This PR combines them all into one call, which should improve performance. You can see the effect in the artifact_commands snapshots, e.g. `rust/kcl-lib/tests/kcl_samples/axial-fan/artifact_commands.snap`
Besides performance, this should fix a bug where some KCL fillets would fail, when they should have succeeded. Example from @max-mrgrsk:
```kcl
sketch001 = startSketchOn(XY)
|> startProfile(at = [-12, -6])
|> line(end = [0, 12], tag = $seg04)
|> line(end = [24, 0], tag = $seg03)
|> line(end = [0, -12], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(
sketch001,
length = 12,
tagEnd = $capEnd001,
tagStart = $capStart001,
)
|> fillet(
radius = 5,
tags = [
getCommonEdge(faces = [seg02, capEnd001]),
getCommonEdge(faces = [seg01, capEnd001]),
getCommonEdge(faces = [seg03, capEnd001]),
getCommonEdge(faces = [seg04, capEnd001])
],
)
```
This program fails on main, but succeeds on this branch.
2025-05-22 16:25:55 -05:00
let id = exec_state . next_uuid ( ) ;
let mut extra_face_ids = Vec ::new ( ) ;
let num_extra_ids = edge_ids . len ( ) - 1 ;
for _ in 0 .. num_extra_ids {
extra_face_ids . push ( exec_state . next_uuid ( ) ) ;
}
2025-06-10 21:30:48 -04:00
exec_state
. batch_end_cmd (
ModelingCmdMeta ::from_args_id ( & args , id ) ,
ModelingCmd ::from ( mcmd ::Solid3dFilletEdge {
edge_id : None ,
edge_ids : edge_ids . clone ( ) ,
extra_face_ids ,
strategy : Default ::default ( ) ,
object_id : solid . id ,
radius : LengthUnit ( radius . to_mm ( ) ) ,
2025-06-20 11:42:14 -04:00
tolerance : LengthUnit ( tolerance . as_ref ( ) . map ( | t | t . to_mm ( ) ) . unwrap_or ( DEFAULT_TOLERANCE_MM ) ) ,
2025-06-10 21:30:48 -04:00
cut_type : CutType ::Fillet ,
} ) ,
)
. await ? ;
2024-06-23 19:19:24 -07:00
Do multiple chamfer/fillet in one API call (#6750)
KCL's `fillet` function takes an array of edges to fillet. Previously this would do `n` fillet API commands, one per edge. This PR combines them all into one call, which should improve performance. You can see the effect in the artifact_commands snapshots, e.g. `rust/kcl-lib/tests/kcl_samples/axial-fan/artifact_commands.snap`
Besides performance, this should fix a bug where some KCL fillets would fail, when they should have succeeded. Example from @max-mrgrsk:
```kcl
sketch001 = startSketchOn(XY)
|> startProfile(at = [-12, -6])
|> line(end = [0, 12], tag = $seg04)
|> line(end = [24, 0], tag = $seg03)
|> line(end = [0, -12], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(
sketch001,
length = 12,
tagEnd = $capEnd001,
tagStart = $capStart001,
)
|> fillet(
radius = 5,
tags = [
getCommonEdge(faces = [seg02, capEnd001]),
getCommonEdge(faces = [seg01, capEnd001]),
getCommonEdge(faces = [seg03, capEnd001]),
getCommonEdge(faces = [seg04, capEnd001])
],
)
```
This program fails on main, but succeeds on this branch.
2025-05-22 16:25:55 -05:00
let new_edge_cuts = edge_ids . into_iter ( ) . map ( | edge_id | EdgeCut ::Fillet {
id ,
edge_id ,
radius : radius . clone ( ) ,
tag : Box ::new ( tag . clone ( ) ) ,
} ) ;
solid . edge_cuts . extend ( new_edge_cuts ) ;
2024-07-28 00:30:04 -07:00
Do multiple chamfer/fillet in one API call (#6750)
KCL's `fillet` function takes an array of edges to fillet. Previously this would do `n` fillet API commands, one per edge. This PR combines them all into one call, which should improve performance. You can see the effect in the artifact_commands snapshots, e.g. `rust/kcl-lib/tests/kcl_samples/axial-fan/artifact_commands.snap`
Besides performance, this should fix a bug where some KCL fillets would fail, when they should have succeeded. Example from @max-mrgrsk:
```kcl
sketch001 = startSketchOn(XY)
|> startProfile(at = [-12, -6])
|> line(end = [0, 12], tag = $seg04)
|> line(end = [24, 0], tag = $seg03)
|> line(end = [0, -12], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(
sketch001,
length = 12,
tagEnd = $capEnd001,
tagStart = $capStart001,
)
|> fillet(
radius = 5,
tags = [
getCommonEdge(faces = [seg02, capEnd001]),
getCommonEdge(faces = [seg01, capEnd001]),
getCommonEdge(faces = [seg03, capEnd001]),
getCommonEdge(faces = [seg04, capEnd001])
],
)
```
This program fails on main, but succeeds on this branch.
2025-05-22 16:25:55 -05:00
if let Some ( ref tag ) = tag {
solid . value . push ( ExtrudeSurface ::Fillet ( FilletSurface {
face_id : id ,
tag : Some ( tag . clone ( ) ) ,
geo_meta : GeoMeta {
id ,
metadata : args . source_range . into ( ) ,
} ,
} ) ) ;
2024-03-05 11:52:45 -08:00
}
2024-09-27 15:44:44 -07:00
Ok ( solid )
2024-03-05 11:52:45 -08:00
}
2025-03-12 11:24:27 -05:00
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn test_validate_unique ( ) {
let dup_a = SourceRange ::from ( [ 1 , 3 , 0 ] ) ;
let dup_b = SourceRange ::from ( [ 10 , 30 , 0 ] ) ;
// Two entries are duplicates (abc) with different source ranges.
let tags = vec! [ ( " abc " , dup_a ) , ( " abc " , dup_b ) , ( " def " , SourceRange ::from ( [ 2 , 4 , 0 ] ) ) ] ;
let actual = validate_unique ( & tags ) ;
// Both the duplicates should show up as errors, with both of the
// source ranges they correspond to.
// But the unique source range 'def' should not.
let expected = vec! [ dup_a , dup_b ] ;
assert_eq! ( actual . err ( ) . unwrap ( ) . source_ranges ( ) , expected ) ;
}
}