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),
tags: [Edge; 1+],
tag?: tag,
strategy?: string,
): Solid
```
@ -28,6 +29,7 @@ a sharp, straight transitional edge.
| `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 |
| [`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

18
rust/Cargo.lock generated
View File

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

View File

@ -332,11 +332,10 @@ pub enum SweepEdgeSubType {
pub struct EdgeCut {
pub id: ArtifactId,
pub sub_type: EdgeCutSubType,
pub consumed_edge_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_ids: Vec<ArtifactId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub surface_id: Option<ArtifactId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub surface_ids: Vec<ArtifactId>,
pub code_ref: CodeRef,
}
@ -679,7 +678,7 @@ impl EdgeCut {
let Artifact::EdgeCut(new) = new else {
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);
None
@ -1270,22 +1269,19 @@ fn artifacts_to_update(
}
ModelingCmd::Solid3dFilletEdge(cmd) => {
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 {
id,
sub_type: cmd.cut_type.into(),
consumed_edge_id: cmd.edge_id.into(),
edge_ids: Vec::new(),
surface_id: None,
edge_ids: cmd.edge_ids.iter().copied().map(From::from).collect(),
surface_ids,
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.
}
return Ok(return_arr);
}
ModelingCmd::EntityMakeHelixFromParams(_) => {

View File

@ -82,7 +82,7 @@ impl Artifact {
Artifact::Wall(a) => vec![a.seg_id, a.sweep_id],
Artifact::Cap(a) => vec![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::Helix(a) => a.axis_id.map(|id| vec![id]).unwrap_or_default(),
}
@ -175,9 +175,7 @@ impl Artifact {
// consumed_edge_id.
let mut ids = Vec::new();
ids.extend(&a.edge_ids);
if let Some(surface_id) = a.surface_id {
ids.push(surface_id);
}
ids.extend(&a.surface_ids);
ids
}
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")),
"units" => Some(include_str!("../std/units.kcl")),
"array" => Some(include_str!("../std/array.kcl")),
"cutStrategy" => Some(include_str!("../std/cutStrategy.kcl")),
"transform" => Some(include_str!("../std/transform.kcl")),
_ => 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 {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
arg.get_tag_identifier().ok()

View File

@ -2,7 +2,7 @@
use anyhow::Result;
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 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> {
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 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 tag = args.get_kw_arg_opt("tag")?;
super::fillet::validate_unique(&tags)?;
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 })
}
#[allow(clippy::too_many_arguments)]
async fn inner_chamfer(
solid: Box<Solid>,
length: TyF64,
tags: Vec<EdgeReference>,
tolerance: Option<TyF64>,
strategy: Option<CutStrategy>,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
@ -46,33 +51,47 @@ async fn inner_chamfer(
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();
for edge_tag in tags {
let edge_id = match edge_tag {
EdgeReference::Uuid(uuid) => uuid,
EdgeReference::Tag(edge_tag) => args.get_tag_engine_info(exec_state, &edge_tag)?.id,
};
let edge_ids: Vec<_> = tags
.into_iter()
.map(|edge_tag| edge_tag.get_engine_id(exec_state, &args))
.collect::<Result<Vec<_>, _>>()?;
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(
id,
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,
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,
}),
)
.await?;
for edge_id in edge_ids {
solid.edge_cuts.push(EdgeCut::Chamfer {
id,
edge_id,
length: length.clone(),
tag: Box::new(tag.clone()),
});
}
if let Some(ref tag) = tag {
solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
@ -84,7 +103,6 @@ async fn inner_chamfer(
},
}));
}
}
Ok(solid)
}

View File

@ -3,7 +3,7 @@
use anyhow::Result;
use indexmap::IndexMap;
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 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 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 strategy: Option<CutStrategy> = args.get_kw_arg_opt("strategy")?;
let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
let tag = args.get_kw_arg_opt("tag")?;
// Run the function.
validate_unique(&tags)?;
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 })
}
#[allow(clippy::too_many_arguments)]
async fn inner_fillet(
solid: Box<Solid>,
radius: TyF64,
tags: Vec<EdgeReference>,
tolerance: Option<TyF64>,
strategy: Option<CutStrategy>,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> 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();
for edge_tag in tags {
let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
let edge_ids: Vec<_> = tags
.into_iter()
.map(|edge_tag| edge_tag.get_engine_id(exec_state, &args))
.collect::<Result<Vec<_>, _>>()?;
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(
id,
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,
radius: LengthUnit(radius.to_mm()),
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
@ -98,12 +126,14 @@ async fn inner_fillet(
)
.await?;
for edge_id in edge_ids {
solid.edge_cuts.push(EdgeCut::Fillet {
id,
edge_id,
radius: radius.clone(),
tag: Box::new(tag.clone()),
});
}
if let Some(ref tag) = tag {
solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
@ -115,7 +145,6 @@ async fn inner_fillet(
},
}));
}
}
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::transform"
export import "std::turns"
export import "std::cutStrategy"
export XY = {
origin = { x = 0, y = 0, z = 0 },

View File

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