Update fillet+chamfer with new API options

This commit is contained in:
Adam Chalmers
2025-05-07 12:13:16 -05:00
parent 43d5a72514
commit 2eeffe92ba
11 changed files with 155 additions and 85 deletions

View File

@ -13,6 +13,7 @@ chamfer(
length: number(Length), length: number(Length),
tags: [Edge; 1+], tags: [Edge; 1+],
tag?: tag, tag?: tag,
strategy?: string,
): Solid ): Solid
``` ```
@ -28,6 +29,7 @@ a sharp, straight transitional edge.
| `length` | `number(Length)` | The length of the chamfer | Yes | | `length` | `number(Length)` | The length of the chamfer | Yes |
| `tags` | [`[Edge; 1+]`](/docs/kcl-std/types/std-types-Edge) | The paths you want to chamfer | Yes | | `tags` | [`[Edge; 1+]`](/docs/kcl-std/types/std-types-Edge) | The paths you want to chamfer | Yes |
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this chamfer | No | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this chamfer | No |
| `strategy` | [`string`](/docs/kcl-std/types/std-types-string) | Which strategy should be used to perform this chamfer? | No |
### Returns ### Returns

18
rust/Cargo.lock generated
View File

@ -535,7 +535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -963,7 +963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -1746,7 +1746,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2079,9 +2079,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds" name = "kittycad-modeling-cmds"
version = "0.2.115" version = "0.2.116"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e95dfcade93787f8a7529ad7b9b81f038823e273e7684297085ef720962b7497" checksum = "6eed6c8a18f47919c0f873d58226438b737e3f873359b264bcf377f8843727b0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -2986,7 +2986,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -3305,7 +3305,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -3899,7 +3899,7 @@ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -4752,7 +4752,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]

View File

@ -332,11 +332,10 @@ pub enum SweepEdgeSubType {
pub struct EdgeCut { pub struct EdgeCut {
pub id: ArtifactId, pub id: ArtifactId,
pub sub_type: EdgeCutSubType, pub sub_type: EdgeCutSubType,
pub consumed_edge_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_ids: Vec<ArtifactId>, pub edge_ids: Vec<ArtifactId>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub surface_id: Option<ArtifactId>, pub surface_ids: Vec<ArtifactId>,
pub code_ref: CodeRef, pub code_ref: CodeRef,
} }
@ -679,7 +678,7 @@ impl EdgeCut {
let Artifact::EdgeCut(new) = new else { let Artifact::EdgeCut(new) = new else {
return Some(new); return Some(new);
}; };
merge_opt_id(&mut self.surface_id, new.surface_id); merge_ids(&mut self.surface_ids, new.surface_ids);
merge_ids(&mut self.edge_ids, new.edge_ids); merge_ids(&mut self.edge_ids, new.edge_ids);
None None
@ -1270,22 +1269,19 @@ fn artifacts_to_update(
} }
ModelingCmd::Solid3dFilletEdge(cmd) => { ModelingCmd::Solid3dFilletEdge(cmd) => {
let mut return_arr = Vec::new(); let mut return_arr = Vec::new();
let mut surface_ids = Vec::with_capacity(cmd.extra_face_ids.len() + 1);
surface_ids.push(id);
for id in &cmd.extra_face_ids {
surface_ids.push(id.into());
}
return_arr.push(Artifact::EdgeCut(EdgeCut { return_arr.push(Artifact::EdgeCut(EdgeCut {
id, id,
sub_type: cmd.cut_type.into(), sub_type: cmd.cut_type.into(),
consumed_edge_id: cmd.edge_id.into(), edge_ids: cmd.edge_ids.iter().copied().map(From::from).collect(),
edge_ids: Vec::new(), surface_ids,
surface_id: None,
code_ref, code_ref,
})); }));
let consumed_edge = artifacts.get(&ArtifactId::new(cmd.edge_id));
if let Some(Artifact::Segment(consumed_edge)) = consumed_edge {
let mut new_segment = consumed_edge.clone();
new_segment.edge_cut_id = Some(id);
return_arr.push(Artifact::Segment(new_segment));
} else {
// TODO: Handle other types like SweepEdge. // TODO: Handle other types like SweepEdge.
}
return Ok(return_arr); return Ok(return_arr);
} }
ModelingCmd::EntityMakeHelixFromParams(_) => { ModelingCmd::EntityMakeHelixFromParams(_) => {

View File

@ -82,7 +82,7 @@ impl Artifact {
Artifact::Wall(a) => vec![a.seg_id, a.sweep_id], Artifact::Wall(a) => vec![a.seg_id, a.sweep_id],
Artifact::Cap(a) => vec![a.sweep_id], Artifact::Cap(a) => vec![a.sweep_id],
Artifact::SweepEdge(a) => vec![a.seg_id, a.sweep_id], Artifact::SweepEdge(a) => vec![a.seg_id, a.sweep_id],
Artifact::EdgeCut(a) => vec![a.consumed_edge_id], Artifact::EdgeCut(a) => a.edge_ids.clone(),
Artifact::EdgeCutEdge(a) => vec![a.edge_cut_id], Artifact::EdgeCutEdge(a) => vec![a.edge_cut_id],
Artifact::Helix(a) => a.axis_id.map(|id| vec![id]).unwrap_or_default(), Artifact::Helix(a) => a.axis_id.map(|id| vec![id]).unwrap_or_default(),
} }
@ -175,9 +175,7 @@ impl Artifact {
// consumed_edge_id. // consumed_edge_id.
let mut ids = Vec::new(); let mut ids = Vec::new();
ids.extend(&a.edge_ids); ids.extend(&a.edge_ids);
if let Some(surface_id) = a.surface_id { ids.extend(&a.surface_ids);
ids.push(surface_id);
}
ids ids
} }
Artifact::EdgeCutEdge(a) => { Artifact::EdgeCutEdge(a) => {

View File

@ -96,6 +96,7 @@ pub(crate) fn read_std(mod_name: &str) -> Option<&'static str> {
"solid" => Some(include_str!("../std/solid.kcl")), "solid" => Some(include_str!("../std/solid.kcl")),
"units" => Some(include_str!("../std/units.kcl")), "units" => Some(include_str!("../std/units.kcl")),
"array" => Some(include_str!("../std/array.kcl")), "array" => Some(include_str!("../std/array.kcl")),
"cutStrategy" => Some(include_str!("../std/cutStrategy.kcl")),
"transform" => Some(include_str!("../std/transform.kcl")), "transform" => Some(include_str!("../std/transform.kcl")),
_ => None, _ => None,
} }

View File

@ -778,6 +778,18 @@ impl<'a> FromKclValue<'a> for TagNode {
} }
} }
impl<'a> FromKclValue<'a> for kcmc::shared::CutStrategy {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg.as_str() {
Some("automatic") => Some(Self::Automatic),
Some("basic") => Some(Self::Basic),
Some("csg") => Some(Self::Csg),
Some(_) => None,
None => None,
}
}
}
impl<'a> FromKclValue<'a> for TagIdentifier { impl<'a> FromKclValue<'a> for TagIdentifier {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
arg.get_tag_identifier().ok() arg.get_tag_identifier().ok()

View File

@ -2,7 +2,7 @@
use anyhow::Result; use anyhow::Result;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd}; use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd};
use kittycad_modeling_cmds as kcmc; use kittycad_modeling_cmds::{self as kcmc, shared::CutStrategy};
use super::args::TyF64; use super::args::TyF64;
use crate::{ use crate::{
@ -21,19 +21,24 @@ pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::Primitive(PrimitiveType::Solid), exec_state)?; let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::Primitive(PrimitiveType::Solid), exec_state)?;
let length: TyF64 = args.get_kw_arg_typed("length", &RuntimeType::length(), exec_state)?; let length: TyF64 = args.get_kw_arg_typed("length", &RuntimeType::length(), exec_state)?;
let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
let strategy: Option<CutStrategy> = args.get_kw_arg_opt("strategy")?;
let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?; let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
let tag = args.get_kw_arg_opt("tag")?; let tag = args.get_kw_arg_opt("tag")?;
super::fillet::validate_unique(&tags)?; super::fillet::validate_unique(&tags)?;
let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect(); let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
let value = inner_chamfer(solid, length, tags, tag, exec_state, args).await?; let value = inner_chamfer(solid, length, tags, tolerance, strategy, tag, exec_state, args).await?;
Ok(KclValue::Solid { value }) Ok(KclValue::Solid { value })
} }
#[allow(clippy::too_many_arguments)]
async fn inner_chamfer( async fn inner_chamfer(
solid: Box<Solid>, solid: Box<Solid>,
length: TyF64, length: TyF64,
tags: Vec<EdgeReference>, tags: Vec<EdgeReference>,
tolerance: Option<TyF64>,
strategy: Option<CutStrategy>,
tag: Option<TagNode>, tag: Option<TagNode>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
@ -46,33 +51,47 @@ async fn inner_chamfer(
source_ranges: vec![args.source_range], source_ranges: vec![args.source_range],
})); }));
} }
if tags.is_empty() {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![args.source_range],
message: "You must chamfer at least one tag".to_owned(),
}));
}
let mut solid = solid.clone(); let mut solid = solid.clone();
for edge_tag in tags { let edge_ids: Vec<_> = tags
let edge_id = match edge_tag { .into_iter()
EdgeReference::Uuid(uuid) => uuid, .map(|edge_tag| edge_tag.get_engine_id(exec_state, &args))
EdgeReference::Tag(edge_tag) => args.get_tag_engine_info(exec_state, &edge_tag)?.id, .collect::<Result<Vec<_>, _>>()?;
};
let id = exec_state.next_uuid(); 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());
}
let strategy = strategy.unwrap_or_default();
args.batch_end_cmd( args.batch_end_cmd(
id, id,
ModelingCmd::from(mcmd::Solid3dFilletEdge { ModelingCmd::from(mcmd::Solid3dFilletEdge {
edge_id, edge_id: None,
edge_ids: edge_ids.clone(),
extra_face_ids: extra_face_ids.clone(),
strategy,
object_id: solid.id, object_id: solid.id,
radius: LengthUnit(length.to_mm()), radius: LengthUnit(length.to_mm()),
tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future. tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
cut_type: CutType::Chamfer, cut_type: CutType::Chamfer,
}), }),
) )
.await?; .await?;
for edge_id in edge_ids {
solid.edge_cuts.push(EdgeCut::Chamfer { solid.edge_cuts.push(EdgeCut::Chamfer {
id, id,
edge_id, edge_id,
length: length.clone(), length: length.clone(),
tag: Box::new(tag.clone()), tag: Box::new(tag.clone()),
}); });
}
if let Some(ref tag) = tag { if let Some(ref tag) = tag {
solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface { solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
@ -84,7 +103,6 @@ async fn inner_chamfer(
}, },
})); }));
} }
}
Ok(solid) Ok(solid)
} }

View File

@ -3,7 +3,7 @@
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd}; use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd};
use kittycad_modeling_cmds as kcmc; use kittycad_modeling_cmds::{self as kcmc, shared::CutStrategy};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{args::TyF64, DEFAULT_TOLERANCE}; use super::{args::TyF64, DEFAULT_TOLERANCE};
@ -62,34 +62,62 @@ pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::solid(), exec_state)?; let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::solid(), exec_state)?;
let radius: TyF64 = args.get_kw_arg_typed("radius", &RuntimeType::length(), exec_state)?; let radius: TyF64 = args.get_kw_arg_typed("radius", &RuntimeType::length(), exec_state)?;
let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?; let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
let strategy: Option<CutStrategy> = args.get_kw_arg_opt("strategy")?;
let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?; let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
let tag = args.get_kw_arg_opt("tag")?; let tag = args.get_kw_arg_opt("tag")?;
// Run the function. // Run the function.
validate_unique(&tags)?; validate_unique(&tags)?;
let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect(); let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?; let value = inner_fillet(solid, radius, tags, tolerance, strategy, tag, exec_state, args).await?;
Ok(KclValue::Solid { value }) Ok(KclValue::Solid { value })
} }
#[allow(clippy::too_many_arguments)]
async fn inner_fillet( async fn inner_fillet(
solid: Box<Solid>, solid: Box<Solid>,
radius: TyF64, radius: TyF64,
tags: Vec<EdgeReference>, tags: Vec<EdgeReference>,
tolerance: Option<TyF64>, tolerance: Option<TyF64>,
strategy: Option<CutStrategy>,
tag: Option<TagNode>, tag: Option<TagNode>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<Box<Solid>, KclError> { ) -> Result<Box<Solid>, KclError> {
// 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 {
return Err(KclError::Type(KclErrorDetails {
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],
}));
}
if tags.is_empty() {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![args.source_range],
message: "You must fillet at least one tag".to_owned(),
}));
}
let mut solid = solid.clone(); let mut solid = solid.clone();
for edge_tag in tags { let edge_ids: Vec<_> = tags
let edge_id = edge_tag.get_engine_id(exec_state, &args)?; .into_iter()
.map(|edge_tag| edge_tag.get_engine_id(exec_state, &args))
.collect::<Result<Vec<_>, _>>()?;
let id = exec_state.next_uuid(); 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());
}
let strategy = strategy.unwrap_or_default();
args.batch_end_cmd( args.batch_end_cmd(
id, id,
ModelingCmd::from(mcmd::Solid3dFilletEdge { ModelingCmd::from(mcmd::Solid3dFilletEdge {
edge_id, edge_id: None,
edge_ids: edge_ids.clone(),
extra_face_ids: extra_face_ids.clone(),
strategy,
object_id: solid.id, object_id: solid.id,
radius: LengthUnit(radius.to_mm()), radius: LengthUnit(radius.to_mm()),
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)), tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
@ -98,12 +126,14 @@ async fn inner_fillet(
) )
.await?; .await?;
for edge_id in edge_ids {
solid.edge_cuts.push(EdgeCut::Fillet { solid.edge_cuts.push(EdgeCut::Fillet {
id, id,
edge_id, edge_id,
radius: radius.clone(), radius: radius.clone(),
tag: Box::new(tag.clone()), tag: Box::new(tag.clone()),
}); });
}
if let Some(ref tag) = tag { if let Some(ref tag) = tag {
solid.value.push(ExtrudeSurface::Fillet(FilletSurface { solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
@ -115,7 +145,6 @@ async fn inner_fillet(
}, },
})); }));
} }
}
Ok(solid) Ok(solid)
} }

View File

@ -0,0 +1,9 @@
@no_std
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
/// Try the fast but simple Basic strategy, and if that fails, try the slow and complex CSG strategy.
export automatic = "automatic"
/// The basic strategy is fastest, but it can't handle complicated cases (like chamfering two edges that touch).
export basic = "basic"
/// The CSG strategy is slowest, but it can handle complex cases (like chamfering two edges that touch).
export csg = "csg"

View File

@ -16,6 +16,7 @@ export import * from "std::sketch"
export import * from "std::solid" export import * from "std::solid"
export import * from "std::transform" export import * from "std::transform"
export import "std::turns" export import "std::turns"
export import "std::cutStrategy"
export XY = { export XY = {
origin = { x = 0, y = 0, z = 0 }, origin = { x = 0, y = 0, z = 0 },

View File

@ -71,6 +71,8 @@ export fn fillet(
tolerance?: number(Length), tolerance?: number(Length),
/// Create a new tag which refers to this fillet /// Create a new tag which refers to this fillet
tag?: tag, tag?: tag,
/// Which strategy should be used to perform this chamfer?
strategy?: string,
): Solid {} ): Solid {}
/// Cut a straight transitional edge along a tagged path. /// Cut a straight transitional edge along a tagged path.
@ -146,6 +148,8 @@ export fn chamfer(
tags: [Edge; 1+], tags: [Edge; 1+],
/// Create a new tag which refers to this chamfer /// Create a new tag which refers to this chamfer
tag?: tag, tag?: tag,
/// Which strategy should be used to perform this chamfer?
strategy?: string,
): Solid {} ): Solid {}
/// Remove volume from a 3-dimensional shape such that a wall of the /// Remove volume from a 3-dimensional shape such that a wall of the