* add fillet

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* update tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* get end cap info

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* tryu

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* next-adjacent

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix js tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* works

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* u[pdates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* move back to functions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2024-03-05 11:52:45 -08:00
committed by GitHub
parent bc303fbaab
commit 778478757e
14 changed files with 9177 additions and 3 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -80,6 +80,7 @@ const mySketch001 = startSketchOn('XY')
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
endCapId: null, endCapId: null,
startCapId: null, startCapId: null,
sketchGroupValues: expect.any(Array),
xAxis: { x: 1, y: 0, z: 0 }, xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 }, yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }, zAxis: { x: 0, y: 0, z: 1 },
@ -122,6 +123,7 @@ const sk2 = startSketchOn('XY')
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
endCapId: null, endCapId: null,
startCapId: null, startCapId: null,
sketchGroupValues: expect.any(Array),
xAxis: { x: 1, y: 0, z: 0 }, xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 }, yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }, zAxis: { x: 0, y: 0, z: 1 },
@ -137,6 +139,7 @@ const sk2 = startSketchOn('XY')
endCapId: null, endCapId: null,
startCapId: null, startCapId: null,
sketchGroupValues: expect.any(Array),
xAxis: { x: 1, y: 0, z: 0 }, xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 }, yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }, zAxis: { x: 0, y: 0, z: 1 },

View File

@ -1952,9 +1952,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.2.58" version = "0.2.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049c3881ffbe77bf1c3a968372a246ce906eceb79f61cd0bc5fa229bec3504cb" checksum = "4080db4364c103601db486e4a8aa889ea56c011991e4c454373d8050a165d3da"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",

View File

@ -58,7 +58,7 @@ members = [
] ]
[workspace.dependencies] [workspace.dependencies]
kittycad = { version = "0.2.58", default-features = false, features = ["js", "requests"] } kittycad = { version = "0.2.59", default-features = false, features = ["js", "requests"] }
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }

View File

@ -558,6 +558,8 @@ pub struct ExtrudeGroup {
pub id: uuid::Uuid, pub id: uuid::Uuid,
/// The extrude surfaces. /// The extrude surfaces.
pub value: Vec<ExtrudeSurface>, pub value: Vec<ExtrudeSurface>,
/// The sketch group paths.
pub sketch_group_values: Vec<Path>,
/// The height of the extrude group. /// The height of the extrude group.
pub height: f64, pub height: f64,
/// The position of the extrude group. /// The position of the extrude group.

View File

@ -159,6 +159,7 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
// sketch group. // sketch group.
id: sketch_group.id, id: sketch_group.id,
value: new_value, value: new_value,
sketch_group_values: sketch_group.value.clone(),
height: length, height: length,
position: sketch_group.position, position: sketch_group.position,
rotation: sketch_group.rotation, rotation: sketch_group.rotation,

View File

@ -0,0 +1,308 @@
//! Standard library fillets.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ModelingCmd;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, ExtrudeSurface, MemoryItem, UserVal},
std::Args,
};
/// Data for fillets.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct FilletData {
/// The radius of the fillet.
pub radius: f64,
/// The tags of the paths you want to fillet.
pub tags: Vec<StringOrUuid>,
}
/// A string or a uuid.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Ord, PartialOrd, Eq, Hash)]
#[ts(export)]
#[serde(untagged)]
pub enum StringOrUuid {
/// A uuid.
Uuid(Uuid),
/// A string.
String(String),
}
/// Create fillets on tagged paths.
pub async fn fillet(args: Args) -> Result<MemoryItem, KclError> {
let (data, extrude_group): (FilletData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
let extrude_group = inner_fillet(data, extrude_group, args).await?;
Ok(MemoryItem::ExtrudeGroup(extrude_group))
}
/// Create fillets on tagged paths.
#[stdlib {
name = "fillet",
}]
async fn inner_fillet(
data: FilletData,
extrude_group: Box<ExtrudeGroup>,
args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> {
// Check if tags contains any duplicate values.
let mut tags = data.tags.clone();
tags.sort();
tags.dedup();
if tags.len() != data.tags.len() {
return Err(KclError::Type(KclErrorDetails {
message: "Duplicate tags are not allowed.".to_string(),
source_ranges: vec![args.source_range],
}));
}
for tag in data.tags {
let edge_id = match tag {
StringOrUuid::Uuid(uuid) => uuid,
StringOrUuid::String(tag) => {
extrude_group
.sketch_group_values
.iter()
.find(|p| p.get_name() == tag)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found with tag: `{}`", tag),
source_ranges: vec![args.source_range],
})
})?
.get_base()
.geo_meta
.id
}
};
args.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid3DFilletEdge {
edge_id,
object_id: extrude_group.id,
radius: data.radius,
tolerance: 0.0000001, // We can let the user set this in the future.
},
)
.await?;
}
Ok(extrude_group)
}
/// Get the opposite edge to the edge given.
pub async fn get_opposite_edge(args: Args) -> Result<MemoryItem, KclError> {
let (tag, extrude_group): (String, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
let edge = inner_get_opposite_edge(tag, extrude_group, args.clone()).await?;
Ok(MemoryItem::UserVal(UserVal {
value: serde_json::to_value(edge).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert Uuid to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
meta: vec![args.source_range.into()],
}))
}
/// Get the opposite edge to the edge given.
#[stdlib {
name = "getOppositeEdge",
}]
async fn inner_get_opposite_edge(tag: String, extrude_group: Box<ExtrudeGroup>, args: Args) -> Result<Uuid, KclError> {
let tagged_path = extrude_group
.sketch_group_values
.iter()
.find(|p| p.get_name() == tag)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found with tag: `{}`", tag),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let face_id = get_adjacent_face_to_tag(&extrude_group, &tag, &args)?;
let resp = args
.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid3DGetOppositeEdge {
edge_id: tagged_path.geo_meta.id,
object_id: extrude_group.id,
face_id,
},
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetOppositeEdge { data: opposite_edge },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("Solid3DGetOppositeEdge response was not as expected: {:?}", resp),
source_ranges: vec![args.source_range],
}));
};
Ok(opposite_edge.edge)
}
/// Get the next adjacent edge to the edge given.
pub async fn get_next_adjacent_edge(args: Args) -> Result<MemoryItem, KclError> {
let (tag, extrude_group): (String, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
let edge = inner_get_next_adjacent_edge(tag, extrude_group, args.clone()).await?;
Ok(MemoryItem::UserVal(UserVal {
value: serde_json::to_value(edge).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert Uuid to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
meta: vec![args.source_range.into()],
}))
}
/// Get the next adjacent edge to the edge given.
#[stdlib {
name = "getNextAdjacentEdge",
}]
async fn inner_get_next_adjacent_edge(
tag: String,
extrude_group: Box<ExtrudeGroup>,
args: Args,
) -> Result<Uuid, KclError> {
let tagged_path = extrude_group
.sketch_group_values
.iter()
.find(|p| p.get_name() == tag)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found with tag: `{}`", tag),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let face_id = get_adjacent_face_to_tag(&extrude_group, &tag, &args)?;
let resp = args
.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid3DGetNextAdjacentEdge {
edge_id: tagged_path.geo_meta.id,
object_id: extrude_group.id,
face_id,
},
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetNextAdjacentEdge { data: ajacent_edge },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("Solid3DGetNextAdjacentEdge response was not as expected: {:?}", resp),
source_ranges: vec![args.source_range],
}));
};
ajacent_edge.edge.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found next adjacent to tag: `{}`", tag),
source_ranges: vec![args.source_range],
})
})
}
/// Get the previous adjacent edge to the edge given.
pub async fn get_previous_adjacent_edge(args: Args) -> Result<MemoryItem, KclError> {
let (tag, extrude_group): (String, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
let edge = inner_get_previous_adjacent_edge(tag, extrude_group, args.clone()).await?;
Ok(MemoryItem::UserVal(UserVal {
value: serde_json::to_value(edge).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert Uuid to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
meta: vec![args.source_range.into()],
}))
}
/// Get the previous adjacent edge to the edge given.
#[stdlib {
name = "getPreviousAdjacentEdge",
}]
async fn inner_get_previous_adjacent_edge(
tag: String,
extrude_group: Box<ExtrudeGroup>,
args: Args,
) -> Result<Uuid, KclError> {
let tagged_path = extrude_group
.sketch_group_values
.iter()
.find(|p| p.get_name() == tag)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found with tag: `{}`", tag),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let face_id = get_adjacent_face_to_tag(&extrude_group, &tag, &args)?;
let resp = args
.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid3DGetPrevAdjacentEdge {
edge_id: tagged_path.geo_meta.id,
object_id: extrude_group.id,
face_id,
},
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetPrevAdjacentEdge { data: ajacent_edge },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("Solid3DGetPrevAdjacentEdge response was not as expected: {:?}", resp),
source_ranges: vec![args.source_range],
}));
};
ajacent_edge.edge.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found previous adjacent to tag: `{}`", tag),
source_ranges: vec![args.source_range],
})
})
}
fn get_adjacent_face_to_tag(extrude_group: &ExtrudeGroup, tag: &str, args: &Args) -> Result<uuid::Uuid, KclError> {
extrude_group
.value
.iter()
.find_map(|extrude_surface| match extrude_surface {
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == tag => Some(Ok(extrude_plane.face_id)),
ExtrudeSurface::ExtrudeArc(extrude_arc) if extrude_arc.name == tag => Some(Ok(extrude_arc.face_id)),
ExtrudeSurface::ExtrudePlane(_) | ExtrudeSurface::ExtrudeArc(_) => None,
})
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a face with the tag `{}`", tag),
source_ranges: vec![args.source_range],
})
})?
}

View File

@ -1,6 +1,7 @@
//! Functions implemented for language execution. //! Functions implemented for language execution.
pub mod extrude; pub mod extrude;
pub mod fillet;
pub mod import; pub mod import;
pub mod kcl_stdlib; pub mod kcl_stdlib;
pub mod math; pub mod math;
@ -73,6 +74,10 @@ lazy_static! {
Box::new(crate::std::sketch::Hole), Box::new(crate::std::sketch::Hole),
Box::new(crate::std::patterns::PatternLinear), Box::new(crate::std::patterns::PatternLinear),
Box::new(crate::std::patterns::PatternCircular), Box::new(crate::std::patterns::PatternCircular),
Box::new(crate::std::fillet::Fillet),
Box::new(crate::std::fillet::GetOppositeEdge),
Box::new(crate::std::fillet::GetNextAdjacentEdge),
Box::new(crate::std::fillet::GetPreviousAdjacentEdge),
Box::new(crate::std::import::Import), Box::new(crate::std::import::Import),
Box::new(crate::std::math::Cos), Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin), Box::new(crate::std::math::Sin),
@ -573,6 +578,50 @@ impl Args {
Ok((data, sketch_surface)) Ok((data, sketch_surface))
} }
fn get_data_and_extrude_group<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Box<ExtrudeGroup>), KclError> {
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let data: T = serde_json::from_value(first_value).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to deserialize struct from JSON: {}", e),
source_ranges: vec![self.source_range],
})
})?;
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected an ExtrudeGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range],
})
})?;
let extrude_group = if let MemoryItem::ExtrudeGroup(eg) = second_value {
eg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected an ExtrudeGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range],
}));
};
Ok((data, extrude_group))
}
fn get_segment_name_to_number_sketch_group(&self) -> Result<(String, f64, Box<SketchGroup>), KclError> { fn get_segment_name_to_number_sketch_group(&self) -> Result<(String, f64, Box<SketchGroup>), KclError> {
// Iterate over our args, the first argument should be a UserVal with a string value. // Iterate over our args, the first argument should be a UserVal with a string value.
// The second argument should be a number. // The second argument should be a number.

View File

@ -203,6 +203,107 @@ const part002 = startSketchOn(part001, "END")
); );
} }
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_fillet_duplicate_tags() {
let code = r#"const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %)
|> line({to: [0, 10], tag: "thing"}, %)
|> line([10, 0], %)
|> line({to: [0, -10], tag: "thing2"}, %)
|> close(%)
|> extrude(10, %)
|> fillet({radius: 0.5, tags: ["thing", "thing"]}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([227, 277])], message: "Duplicate tags are not allowed." }"#,
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_basic_fillet_cube_start() {
let code = r#"const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %)
|> line({to: [0, 10], tag: "thing"}, %)
|> line([10, 0], %)
|> line({to: [0, -10], tag: "thing2"}, %)
|> close(%)
|> extrude(10, %)
|> fillet({radius: 2, tags: ["thing", "thing2"]}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_start.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_basic_fillet_cube_end() {
let code = r#"const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %)
|> line({to: [0, 10], tag: "thing"}, %)
|> line([10, 0], %)
|> line({to: [0, -10], tag: "thing2"}, %)
|> close(%)
|> extrude(10, %)
|> fillet({radius: 2, tags: ["thing", getOppositeEdge("thing", %)]}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_end.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_basic_fillet_cube_next_adjacent() {
let code = r#"const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %)
|> line({to: [0, 10], tag: "thing"}, %)
|> line({to: [10, 0], tag: "thing1"}, %)
|> line({to: [0, -10], tag: "thing2"}, %)
|> close(%)
|> extrude(10, %)
|> fillet({radius: 2, tags: [getNextAdjacentEdge("thing", %)]}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
"tests/executor/outputs/basic_fillet_cube_next_adjacent.png",
&result,
0.999,
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_basic_fillet_cube_previous_adjacent() {
let code = r#"const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %)
|> line({to: [0, 10], tag: "thing"}, %)
|> line({to: [10, 0], tag: "thing1"}, %)
|> line({to: [0, -10], tag: "thing2"}, %)
|> close(%)
|> extrude(10, %)
|> fillet({radius: 2, tags: [getPreviousAdjacentEdge("thing2", %)]}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
"tests/executor/outputs/basic_fillet_cube_previous_adjacent.png",
&result,
0.999,
);
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn serial_test_execute_with_function_sketch() { async fn serial_test_execute_with_function_sketch() {
let code = r#"fn box = (h, l, w) => { let code = r#"fn box = (h, l, w) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB