2023-08-24 15:34:51 -07:00
|
|
|
//! Functions related to extruding.
|
|
|
|
|
2024-09-06 17:02:04 -05:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2023-08-29 14:12:48 -07:00
|
|
|
use anyhow::Result;
|
|
|
|
use derive_docs::stdlib;
|
2024-09-19 14:06:29 -07:00
|
|
|
use kcmc::{
|
|
|
|
each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, output::ExtrusionFaceInfo,
|
|
|
|
shared::ExtrusionFaceCapType, websocket::OkWebSocketResponseData, ModelingCmd,
|
|
|
|
};
|
2024-09-18 17:04:04 -05:00
|
|
|
use kittycad_modeling_cmds as kcmc;
|
2024-03-22 10:23:04 +11:00
|
|
|
use uuid::Uuid;
|
2023-08-29 14:12:48 -07:00
|
|
|
|
2023-08-24 15:34:51 -07:00
|
|
|
use crate::{
|
|
|
|
errors::{KclError, KclErrorDetails},
|
2024-12-07 07:16:04 +13:00
|
|
|
execution::{
|
|
|
|
ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSet, SketchSurface, Solid, SolidSet,
|
|
|
|
},
|
2023-08-24 15:34:51 -07:00
|
|
|
std::Args,
|
|
|
|
};
|
|
|
|
|
|
|
|
/// Extrudes by a given amount.
|
2024-10-09 19:38:40 -04:00
|
|
|
pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
2024-09-27 15:44:44 -07:00
|
|
|
let (length, sketch_set) = args.get_number_sketch_set()?;
|
2023-08-24 15:34:51 -07:00
|
|
|
|
2024-10-09 19:38:40 -04:00
|
|
|
let result = inner_extrude(length, sketch_set, exec_state, args).await?;
|
2023-08-25 13:41:04 -07:00
|
|
|
|
2024-06-23 23:04:32 -07:00
|
|
|
Ok(result.into())
|
2023-08-25 13:41:04 -07:00
|
|
|
}
|
|
|
|
|
2024-08-06 20:27:26 -04:00
|
|
|
/// 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.
|
2024-03-13 12:56:46 -07:00
|
|
|
///
|
|
|
|
/// ```no_run
|
2024-12-12 11:33:37 -05:00
|
|
|
/// example = startSketchOn('XZ')
|
2024-05-14 17:10:47 -07:00
|
|
|
/// |> startProfileAt([0, 0], %)
|
|
|
|
/// |> line([10, 0], %)
|
|
|
|
/// |> arc({
|
2024-11-25 09:21:55 +13:00
|
|
|
/// angleStart = 120,
|
|
|
|
/// angleEnd = 0,
|
|
|
|
/// radius = 5,
|
2024-05-14 17:10:47 -07:00
|
|
|
/// }, %)
|
|
|
|
/// |> line([5, 0], %)
|
|
|
|
/// |> line([0, 10], %)
|
|
|
|
/// |> bezierCurve({
|
2024-11-25 09:21:55 +13:00
|
|
|
/// control1 = [-10, 0],
|
|
|
|
/// control2 = [2, 10],
|
|
|
|
/// to = [-5, 10],
|
2024-05-14 17:10:47 -07:00
|
|
|
/// }, %)
|
|
|
|
/// |> line([-5, -2], %)
|
|
|
|
/// |> close(%)
|
|
|
|
/// |> extrude(10, %)
|
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
/// ```no_run
|
2024-12-12 11:33:37 -05:00
|
|
|
/// exampleSketch = startSketchOn('XZ')
|
2024-05-14 17:10:47 -07:00
|
|
|
/// |> startProfileAt([-10, 0], %)
|
|
|
|
/// |> arc({
|
2024-11-25 09:21:55 +13:00
|
|
|
/// angleStart = 120,
|
|
|
|
/// angleEnd = -60,
|
|
|
|
/// radius = 5,
|
2024-05-14 17:10:47 -07:00
|
|
|
/// }, %)
|
|
|
|
/// |> line([10, 0], %)
|
|
|
|
/// |> line([5, 0], %)
|
|
|
|
/// |> bezierCurve({
|
2024-11-25 09:21:55 +13:00
|
|
|
/// control1 = [-3, 0],
|
|
|
|
/// control2 = [2, 10],
|
|
|
|
/// to = [-5, 10],
|
2024-05-14 17:10:47 -07:00
|
|
|
/// }, %)
|
|
|
|
/// |> line([-4, 10], %)
|
|
|
|
/// |> line([-5, -2], %)
|
|
|
|
/// |> close(%)
|
|
|
|
///
|
2024-12-12 11:33:37 -05:00
|
|
|
/// example = extrude(10, exampleSketch)
|
2024-03-13 12:56:46 -07:00
|
|
|
/// ```
|
2023-08-25 13:41:04 -07:00
|
|
|
#[stdlib {
|
2024-12-16 13:10:31 -05:00
|
|
|
name = "extrude",
|
|
|
|
feature_tree_operation = true,
|
2023-08-25 13:41:04 -07:00
|
|
|
}]
|
2024-10-09 19:38:40 -04:00
|
|
|
async fn inner_extrude(
|
|
|
|
length: f64,
|
|
|
|
sketch_set: SketchSet,
|
|
|
|
exec_state: &mut ExecState,
|
|
|
|
args: Args,
|
|
|
|
) -> Result<SolidSet, KclError> {
|
|
|
|
let id = exec_state.id_generator.next_uuid();
|
2024-03-26 19:07:16 -07:00
|
|
|
|
2024-04-23 10:31:20 -07:00
|
|
|
// Extrude the element(s).
|
2024-09-27 15:44:44 -07:00
|
|
|
let sketches: Vec<Sketch> = sketch_set.into();
|
|
|
|
let mut solids = Vec::new();
|
|
|
|
for sketch in &sketches {
|
2024-07-09 23:39:59 -04:00
|
|
|
// 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(
|
2024-10-09 19:38:40 -04:00
|
|
|
exec_state.id_generator.next_uuid(),
|
2024-09-18 17:04:04 -05:00
|
|
|
ModelingCmd::from(mcmd::EnableSketchMode {
|
2024-07-09 23:39:59 -04:00
|
|
|
animated: false,
|
|
|
|
ortho: false,
|
2024-09-27 15:44:44 -07:00
|
|
|
entity_id: sketch.on.id(),
|
2024-07-09 23:39:59 -04:00
|
|
|
adjust_camera: false,
|
2024-09-27 15:44:44 -07:00
|
|
|
planar_normal: if let SketchSurface::Plane(plane) = &sketch.on {
|
2024-07-09 23:39:59 -04:00
|
|
|
// We pass in the normal for the plane here.
|
2024-09-04 23:27:12 -05:00
|
|
|
Some(plane.z_axis.into())
|
2024-07-09 23:39:59 -04:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
2024-09-18 17:04:04 -05:00
|
|
|
}),
|
2024-07-09 23:39:59 -04:00
|
|
|
)
|
|
|
|
.await?;
|
2024-07-05 15:37:30 -07:00
|
|
|
|
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
|
|
|
args.batch_modeling_cmd(
|
2024-04-23 10:31:20 -07:00
|
|
|
id,
|
2024-09-18 17:04:04 -05:00
|
|
|
ModelingCmd::from(mcmd::Extrude {
|
2024-09-27 15:44:44 -07:00
|
|
|
target: sketch.id.into(),
|
2024-09-18 17:04:04 -05:00
|
|
|
distance: LengthUnit(length),
|
2024-12-13 13:07:52 -06:00
|
|
|
faces: Default::default(),
|
2024-09-18 17:04:04 -05:00
|
|
|
}),
|
2024-04-23 10:31:20 -07:00
|
|
|
)
|
|
|
|
.await?;
|
2024-07-09 23:39:59 -04:00
|
|
|
|
|
|
|
// Disable the sketch mode.
|
2024-09-18 17:04:04 -05:00
|
|
|
args.batch_modeling_cmd(
|
2024-10-09 19:38:40 -04:00
|
|
|
exec_state.id_generator.next_uuid(),
|
2024-09-18 17:04:04 -05:00
|
|
|
ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable {}),
|
|
|
|
)
|
|
|
|
.await?;
|
2024-10-09 19:38:40 -04:00
|
|
|
solids.push(do_post_extrude(sketch.clone(), length, exec_state, args.clone()).await?);
|
2024-04-23 10:31:20 -07:00
|
|
|
}
|
2023-10-11 18:30:04 -07:00
|
|
|
|
2024-09-27 15:44:44 -07:00
|
|
|
Ok(solids.into())
|
2024-03-26 19:07:16 -07:00
|
|
|
}
|
|
|
|
|
2024-10-09 19:38:40 -04:00
|
|
|
pub(crate) async fn do_post_extrude(
|
|
|
|
sketch: Sketch,
|
|
|
|
length: f64,
|
|
|
|
exec_state: &mut ExecState,
|
|
|
|
args: Args,
|
|
|
|
) -> Result<Box<Solid>, KclError> {
|
2023-10-11 18:30:04 -07:00
|
|
|
// Bring the object to the front of the scene.
|
|
|
|
// See: https://github.com/KittyCAD/modeling-app/issues/806
|
2024-06-19 13:57:50 -07:00
|
|
|
args.batch_modeling_cmd(
|
2024-10-09 19:38:40 -04:00
|
|
|
exec_state.id_generator.next_uuid(),
|
2024-09-27 15:44:44 -07:00
|
|
|
ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
|
2023-10-11 18:30:04 -07:00
|
|
|
)
|
|
|
|
.await?;
|
2023-08-24 15:34:51 -07:00
|
|
|
|
2024-10-26 18:16:21 -05:00
|
|
|
// The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
|
|
|
|
// So, let's just use the first one.
|
|
|
|
let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
|
2024-02-12 18:08:42 -08:00
|
|
|
return Err(KclError::Type(KclErrorDetails {
|
2024-09-27 15:44:44 -07:00
|
|
|
message: "Expected a non-empty sketch".to_string(),
|
2024-02-12 18:08:42 -08:00
|
|
|
source_ranges: vec![args.source_range],
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
2024-09-27 15:44:44 -07:00
|
|
|
let mut sketch = sketch.clone();
|
2024-02-26 14:54:42 -08:00
|
|
|
|
|
|
|
// If we were sketching on a face, we need the original face id.
|
2024-09-27 15:44:44 -07:00
|
|
|
if let SketchSurface::Face(ref face) = sketch.on {
|
|
|
|
sketch.id = face.solid.sketch.id;
|
2024-02-26 14:54:42 -08:00
|
|
|
}
|
|
|
|
|
2024-02-12 18:08:42 -08:00
|
|
|
let solid3d_info = args
|
|
|
|
.send_modeling_cmd(
|
2024-10-09 19:38:40 -04:00
|
|
|
exec_state.id_generator.next_uuid(),
|
2024-09-18 17:04:04 -05:00
|
|
|
ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
|
2024-10-26 18:16:21 -05:00
|
|
|
edge_id: any_edge_id,
|
2024-09-27 15:44:44 -07:00
|
|
|
object_id: sketch.id,
|
2024-09-18 17:04:04 -05:00
|
|
|
}),
|
2024-02-12 18:08:42 -08:00
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
|
2024-09-18 17:04:04 -05:00
|
|
|
let face_infos = if let OkWebSocketResponseData::Modeling {
|
|
|
|
modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
|
2024-02-12 18:08:42 -08:00
|
|
|
} = solid3d_info
|
|
|
|
{
|
|
|
|
data.faces
|
|
|
|
} else {
|
|
|
|
vec![]
|
|
|
|
};
|
|
|
|
|
2024-09-04 23:27:12 -05:00
|
|
|
for (curve_id, face_id) in face_infos
|
|
|
|
.iter()
|
|
|
|
.filter(|face_info| face_info.cap == ExtrusionFaceCapType::None)
|
|
|
|
.filter_map(|face_info| {
|
2024-08-30 19:46:48 +10:00
|
|
|
if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
|
2024-09-04 23:27:12 -05:00
|
|
|
Some((curve_id, face_id))
|
|
|
|
} else {
|
|
|
|
None
|
2024-08-30 19:46:48 +10:00
|
|
|
}
|
2024-09-04 23:27:12 -05:00
|
|
|
})
|
|
|
|
{
|
|
|
|
// 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(
|
2024-10-09 19:38:40 -04:00
|
|
|
exec_state.id_generator.next_uuid(),
|
2024-09-18 17:04:04 -05:00
|
|
|
ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
|
2024-09-04 23:27:12 -05:00
|
|
|
edge_id: curve_id,
|
2024-09-27 15:44:44 -07:00
|
|
|
object_id: sketch.id,
|
2024-09-04 23:27:12 -05:00
|
|
|
face_id,
|
2024-09-18 17:04:04 -05:00
|
|
|
}),
|
2024-09-04 23:27:12 -05:00
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
args.batch_modeling_cmd(
|
2024-10-09 19:38:40 -04:00
|
|
|
exec_state.id_generator.next_uuid(),
|
2024-09-26 18:25:05 +10:00
|
|
|
ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
|
2024-09-04 23:27:12 -05:00
|
|
|
edge_id: curve_id,
|
2024-09-27 15:44:44 -07:00
|
|
|
object_id: sketch.id,
|
2024-09-04 23:27:12 -05:00
|
|
|
face_id,
|
2024-09-18 17:04:04 -05:00
|
|
|
}),
|
2024-09-04 23:27:12 -05:00
|
|
|
)
|
|
|
|
.await?;
|
2024-08-30 19:46:48 +10:00
|
|
|
}
|
|
|
|
|
2024-09-06 17:02:04 -05:00
|
|
|
let Faces {
|
|
|
|
sides: face_id_map,
|
|
|
|
start_cap_id,
|
|
|
|
end_cap_id,
|
2024-10-09 19:38:40 -04:00
|
|
|
} = analyze_faces(exec_state, &args, face_infos);
|
2024-09-27 15:44:44 -07:00
|
|
|
// Iterate over the sketch.value array and add face_id to GeoMeta
|
|
|
|
let new_value = sketch
|
2024-10-23 12:42:54 -05:00
|
|
|
.paths
|
2024-09-04 23:27:12 -05:00
|
|
|
.iter()
|
|
|
|
.flat_map(|path| {
|
|
|
|
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
|
|
|
|
match path {
|
2024-10-25 17:49:30 -05:00
|
|
|
Path::Arc { .. }
|
|
|
|
| Path::TangentialArc { .. }
|
|
|
|
| Path::TangentialArcTo { .. }
|
|
|
|
| Path::Circle { .. } => {
|
2024-12-07 07:16:04 +13:00
|
|
|
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
|
2024-09-04 23:27:12 -05:00
|
|
|
face_id: *actual_face_id,
|
|
|
|
tag: path.get_base().tag.clone(),
|
|
|
|
geo_meta: GeoMeta {
|
|
|
|
id: path.get_base().geo_meta.id,
|
2024-11-14 17:27:19 -06:00
|
|
|
metadata: path.get_base().geo_meta.metadata,
|
2024-09-04 23:27:12 -05:00
|
|
|
},
|
|
|
|
});
|
|
|
|
Some(extrude_surface)
|
|
|
|
}
|
|
|
|
Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
|
2024-12-07 07:16:04 +13:00
|
|
|
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
|
2024-09-04 23:27:12 -05:00
|
|
|
face_id: *actual_face_id,
|
|
|
|
tag: path.get_base().tag.clone(),
|
|
|
|
geo_meta: GeoMeta {
|
|
|
|
id: path.get_base().geo_meta.id,
|
2024-11-14 17:27:19 -06:00
|
|
|
metadata: path.get_base().geo_meta.metadata,
|
2024-09-04 23:27:12 -05:00
|
|
|
},
|
|
|
|
});
|
|
|
|
Some(extrude_surface)
|
|
|
|
}
|
2024-02-22 19:07:17 -08:00
|
|
|
}
|
2024-10-01 12:45:01 -07:00
|
|
|
} else if args.ctx.is_mock() {
|
2024-09-04 23:27:12 -05:00
|
|
|
// Only pre-populate the extrude surface if we are in mock mode.
|
|
|
|
|
2024-12-07 07:16:04 +13:00
|
|
|
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
|
2024-09-04 23:27:12 -05:00
|
|
|
// pushing this values with a fake face_id to make extrudes mock-execute safe
|
2024-10-09 19:38:40 -04:00
|
|
|
face_id: exec_state.id_generator.next_uuid(),
|
2024-09-04 23:27:12 -05:00
|
|
|
tag: path.get_base().tag.clone(),
|
|
|
|
geo_meta: GeoMeta {
|
|
|
|
id: path.get_base().geo_meta.id,
|
2024-11-14 17:27:19 -06:00
|
|
|
metadata: path.get_base().geo_meta.metadata,
|
2024-09-04 23:27:12 -05:00
|
|
|
},
|
|
|
|
});
|
|
|
|
Some(extrude_surface)
|
|
|
|
} else {
|
|
|
|
None
|
2024-02-22 19:07:17 -08:00
|
|
|
}
|
2024-09-04 23:27:12 -05:00
|
|
|
})
|
|
|
|
.collect();
|
2024-02-12 18:08:42 -08:00
|
|
|
|
2024-09-27 15:44:44 -07:00
|
|
|
Ok(Box::new(Solid {
|
|
|
|
// Ok so you would think that the id would be the id of the solid,
|
2024-02-13 10:26:09 -08:00
|
|
|
// that we passed in to the function, but it's actually the id of the
|
2024-09-27 15:44:44 -07:00
|
|
|
// sketch.
|
|
|
|
id: sketch.id,
|
2024-02-12 18:08:42 -08:00
|
|
|
value: new_value,
|
2024-09-27 15:44:44 -07:00
|
|
|
meta: sketch.meta.clone(),
|
|
|
|
sketch,
|
2023-08-24 15:34:51 -07:00
|
|
|
height: length,
|
2024-02-12 18:08:42 -08:00
|
|
|
start_cap_id,
|
|
|
|
end_cap_id,
|
2024-08-12 17:56:45 -05:00
|
|
|
edge_cuts: vec![],
|
2023-09-19 14:20:14 -07:00
|
|
|
}))
|
2023-08-24 15:34:51 -07:00
|
|
|
}
|
2024-09-06 17:02:04 -05:00
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
struct Faces {
|
|
|
|
/// Maps curve ID to face ID for each side.
|
|
|
|
sides: HashMap<Uuid, Option<Uuid>>,
|
|
|
|
/// Top face ID.
|
|
|
|
end_cap_id: Option<Uuid>,
|
|
|
|
/// Bottom face ID.
|
|
|
|
start_cap_id: Option<Uuid>,
|
|
|
|
}
|
|
|
|
|
2024-10-09 19:38:40 -04:00
|
|
|
fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
|
2024-09-06 17:02:04 -05:00
|
|
|
let mut faces = Faces {
|
|
|
|
sides: HashMap::with_capacity(face_infos.len()),
|
|
|
|
..Default::default()
|
|
|
|
};
|
2024-10-01 12:45:01 -07:00
|
|
|
if args.ctx.is_mock() {
|
2024-09-06 17:02:04 -05:00
|
|
|
// Create fake IDs for start and end caps, to make extrudes mock-execute safe
|
2024-10-09 19:38:40 -04:00
|
|
|
faces.start_cap_id = Some(exec_state.id_generator.next_uuid());
|
|
|
|
faces.end_cap_id = Some(exec_state.id_generator.next_uuid());
|
2024-09-06 17:02:04 -05:00
|
|
|
}
|
|
|
|
for face_info in face_infos {
|
|
|
|
match face_info.cap {
|
|
|
|
ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
|
|
|
|
ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
|
2024-11-25 14:10:45 -06:00
|
|
|
ExtrusionFaceCapType::Both => {
|
|
|
|
faces.end_cap_id = face_info.face_id;
|
|
|
|
faces.start_cap_id = face_info.face_id;
|
|
|
|
}
|
2024-09-06 17:02:04 -05:00
|
|
|
ExtrusionFaceCapType::None => {
|
|
|
|
if let Some(curve_id) = face_info.curve_id {
|
|
|
|
faces.sides.insert(curve_id, face_info.face_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
faces
|
|
|
|
}
|