Files
modeling-app/src/wasm-lib/kcl/src/std/extrude.rs
Adam Chalmers cf266b17c1 Batch extrudes (#3764)
Adds a new KCL executor benchmark which builds a `10` wide by `n` tall lego, with varying `n`. The benchmark runs a n = 1, 2, 3 etc build, so we can get an idea of how the speed changes with size. 

This change improves execution speed by 25-36% depending on how many bumps there are. Tested by:

* Rust unit tests
* Open up modeling app, sketch a square, use the command palette to extrude it
* Open up the Bambu printer "poop chute" model, it all extrudes and works fine

Also fixes a bug: extrude, loft, revolve all trigger a GetExtrusionFaceInfo command. Due to a bug, the GetExtrusionFaceInfo command reused the Command ID from the previous extrude/loft/revolve. Fixed that.
2024-09-06 16:55:24 -05:00

303 lines
9.9 KiB
Rust

//! Functions related to extruding.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ExtrusionFaceCapType;
use schemars::JsonSchema;
use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{
ExtrudeGroup, ExtrudeGroupSet, ExtrudeSurface, GeoMeta, KclValue, Path, SketchGroup, SketchGroupSet,
SketchSurface,
},
std::Args,
};
/// Extrudes by a given amount.
pub async fn extrude(args: Args) -> Result<KclValue, KclError> {
let (length, sketch_group_set) = args.get_number_sketch_group_set()?;
let result = inner_extrude(length, sketch_group_set, args).await?;
Ok(result.into())
}
/// Extend a 2-dimensional sketch through a third dimension in order to
/// create new 3-dimensional volume, or if extruded into an existing volume,
/// cut into an existing solid.
///
/// ```no_run
/// const example = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line([10, 0], %)
/// |> arc({
/// angleStart: 120,
/// angleEnd: 0,
/// radius: 5,
/// }, %)
/// |> line([5, 0], %)
/// |> line([0, 10], %)
/// |> bezierCurve({
/// control1: [-10, 0],
/// control2: [2, 10],
/// to: [-5, 10],
/// }, %)
/// |> line([-5, -2], %)
/// |> close(%)
/// |> extrude(10, %)
/// ```
///
/// ```no_run
/// const exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([-10, 0], %)
/// |> arc({
/// angleStart: 120,
/// angleEnd: -60,
/// radius: 5,
/// }, %)
/// |> line([10, 0], %)
/// |> line([5, 0], %)
/// |> bezierCurve({
/// control1: [-3, 0],
/// control2: [2, 10],
/// to: [-5, 10],
/// }, %)
/// |> line([-4, 10], %)
/// |> line([-5, -2], %)
/// |> close(%)
///
/// const example = extrude(10, exampleSketch)
/// ```
#[stdlib {
name = "extrude"
}]
async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args) -> Result<ExtrudeGroupSet, KclError> {
let id = uuid::Uuid::new_v4();
// Extrude the element(s).
let sketch_groups: Vec<SketchGroup> = sketch_group_set.into();
let mut extrude_groups = Vec::new();
for sketch_group in &sketch_groups {
// Before we extrude, we need to enable the sketch mode.
// We do this here in case extrude is called out of order.
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::EnableSketchMode {
animated: false,
ortho: false,
entity_id: sketch_group.on.id(),
adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch_group.on {
// We pass in the normal for the plane here.
Some(plane.z_axis.into())
} else {
None
},
},
)
.await?;
args.batch_modeling_cmd(
id,
kittycad::types::ModelingCmd::Extrude {
target: sketch_group.id,
distance: length,
cap: true,
},
)
.await?;
// Disable the sketch mode.
args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
.await?;
extrude_groups.push(do_post_extrude(sketch_group.clone(), length, args.clone()).await?);
}
Ok(extrude_groups.into())
}
pub(crate) async fn do_post_extrude(
sketch_group: SketchGroup,
length: f64,
args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> {
// Bring the object to the front of the scene.
// See: https://github.com/KittyCAD/modeling-app/issues/806
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::ObjectBringToFront {
object_id: sketch_group.id,
},
)
.await?;
if sketch_group.value.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "Expected a non-empty sketch group".to_string(),
source_ranges: vec![args.source_range],
}));
}
let mut edge_id = None;
for segment in sketch_group.value.iter() {
if let Path::ToPoint { base } = segment {
edge_id = Some(base.geo_meta.id);
break;
}
}
let Some(edge_id) = edge_id else {
return Err(KclError::Type(KclErrorDetails {
message: "Expected a Path::ToPoint variant".to_string(),
source_ranges: vec![args.source_range],
}));
};
let mut sketch_group = sketch_group.clone();
// If we were sketching on a face, we need the original face id.
if let SketchSurface::Face(ref face) = sketch_group.on {
sketch_group.id = face.extrude_group.sketch_group.id;
}
let solid3d_info = args
.send_modeling_cmd(
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo {
edge_id,
object_id: sketch_group.id,
},
)
.await?;
let face_infos = if let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetExtrusionFaceInfo { data },
} = solid3d_info
{
data.faces
} else {
vec![]
};
for (curve_id, face_id) in face_infos
.iter()
.filter(|face_info| face_info.cap == ExtrusionFaceCapType::None)
.filter_map(|face_info| {
if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
Some((curve_id, face_id))
} else {
None
}
})
{
// Batch these commands, because the Rust code doesn't actually care about the outcome.
// So, there's no need to await them.
// Instead, the Typescript codebases (which handles WebSocket sends when compiled via Wasm)
// uses this to build the artifact graph, which the UI needs.
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetOppositeEdge {
edge_id: curve_id,
object_id: sketch_group.id,
face_id,
},
)
.await?;
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge {
edge_id: curve_id,
object_id: sketch_group.id,
face_id,
},
)
.await?;
}
// Create a hashmap for quick id lookup
let mut face_id_map = std::collections::HashMap::new();
// creating fake ids for start and end caps is to make extrudes mock-execute safe
let (mut start_cap_id, mut end_cap_id) = if args.ctx.is_mock {
(Some(Uuid::new_v4()), Some(Uuid::new_v4()))
} else {
(None, None)
};
for face_info in face_infos {
match face_info.cap {
ExtrusionFaceCapType::Bottom => start_cap_id = face_info.face_id,
ExtrusionFaceCapType::Top => end_cap_id = face_info.face_id,
ExtrusionFaceCapType::None => {
if let Some(curve_id) = face_info.curve_id {
face_id_map.insert(curve_id, face_info.face_id);
}
}
}
}
// Iterate over the sketch_group.value array and add face_id to GeoMeta
let new_value = sketch_group
.value
.iter()
.flat_map(|path| {
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
match path {
Path::TangentialArc { .. } | Path::TangentialArcTo { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(),
},
});
Some(extrude_surface)
}
Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(),
},
});
Some(extrude_surface)
}
}
} else if args.ctx.is_mock {
// Only pre-populate the extrude surface if we are in mock mode.
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
// pushing this values with a fake face_id to make extrudes mock-execute safe
face_id: Uuid::new_v4(),
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(),
},
});
Some(extrude_surface)
} else {
None
}
})
.collect();
Ok(Box::new(ExtrudeGroup {
// Ok so you would think that the id would be the id of the extrude group,
// that we passed in to the function, but it's actually the id of the
// sketch group.
id: sketch_group.id,
value: new_value,
meta: sketch_group.meta.clone(),
sketch_group,
height: length,
start_cap_id,
end_cap_id,
edge_cuts: vec![],
}))
}