Files
modeling-app/rust/kcl-lib/src/std/extrude.rs
Kurt Hutten e17c6e272c Add 3 point arc (#5722)
* bare minimum

* start of segment util added

* remove redundant handle

* some probably buggy handling of arc handles, can fix later

* probably bug implementation of update args, but data flow through is mostly there can fix bugs after

* fix update for arc

* fix math for center handle

* fix up length indicator

* tweak math

* stub out xState logic for arc

* more progress on adding point and click, implemented more of sketchLineHelper for arc

* small unrelated tweak

* fix up draft arc bugs

* fix arc last click

* fix draft segment animation and add comment

* add draft point snapping for arcs

* add helper stuff to arc

* clone arc point and click as base for arc-three-point

* rust change for arc three point

* can draw three point arc

* make arcTo editable

* can add new three point arc, so long as it continues existing profile

* get overlays working

* make snap to for continuing profile work for three point arcs

* add draft animation

* tangent issue fix

* action rename

* tmp test fix up

* fix silly bug

* fix couple problems causing tests to fail

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix up

* add delet segment test for new segments

* update docs

* draft segments should look right

* add test for dragging new segment handles

* arc tools can be chained now

* make three point arc can start a new profile (not only extend existing paths)

* add test for equiping and unequiping the tool plus drawing with it

* fix console noise

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* clean up

* update rust/docs

* put toolbar mode check into fixture

* do thing for lee

* use TEST_COLORSs

* fix colors

* don't await file write

* remove commented code

* remove unneeded template strings

* power to **2

* remove magic numbers

* more string templates

* some odd bits of clean up

* arc should be enable in dev

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* add new simulation test

* fix test code from kwark migration

* issues Frank found

* fix deleting half complete ark

* fix

* small fix on dele index

* tsc post main merge

* fix up snaping to profile start

* add cross hari for three point arc

* block snapping if it's the only segment

* add tests for canceling arcTo halfway through

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-18 11:14:12 +11:00

340 lines
11 KiB
Rust

//! Functions related to extruding.
use std::collections::HashMap;
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{
each_cmd as mcmd,
length_unit::LengthUnit,
ok_response::OkModelingCmdResponse,
output::ExtrusionFaceInfo,
shared::ExtrusionFaceCapType,
websocket::{ModelingCmdReq, OkWebSocketResponseData},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
kcl_value::{ArrayLen, RuntimeType},
ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, PrimitiveType, Sketch, SketchSurface, Solid,
},
std::Args,
};
/// Extrudes by a given amount.
pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketches = args.get_unlabeled_kw_arg_typed(
"sketches",
&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::NonEmpty),
exec_state,
)?;
let length = args.get_kw_arg("length")?;
let result = inner_extrude(sketches, length, exec_state, 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.
///
/// You can provide more than one sketch to extrude, and they will all be
/// extruded in the same direction.
///
/// ```no_run
/// example = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> arc({
/// angleStart = 120,
/// angleEnd = 0,
/// radius = 5,
/// }, %)
/// |> line(end = [5, 0])
/// |> line(end = [0, 10])
/// |> bezierCurve({
/// control1 = [-10, 0],
/// control2 = [2, 10],
/// to = [-5, 10],
/// }, %)
/// |> line(end = [-5, -2])
/// |> close()
/// |> extrude(length = 10)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([-10, 0], %)
/// |> arc({
/// angleStart = 120,
/// angleEnd = -60,
/// radius = 5,
/// }, %)
/// |> line(end = [10, 0])
/// |> line(end = [5, 0])
/// |> bezierCurve({
/// control1 = [-3, 0],
/// control2 = [2, 10],
/// to = [-5, 10],
/// }, %)
/// |> line(end = [-4, 10])
/// |> line(end = [-5, -2])
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "extrude",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
sketches = { docs = "Which sketch or sketches should be extruded"},
length = { docs = "How far to extrude the given sketches"},
}
}]
async fn inner_extrude(
sketches: Vec<Sketch>,
length: f64,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Solid>, KclError> {
// Extrude the element(s).
let mut solids = Vec::new();
for sketch in &sketches {
let id = exec_state.next_uuid();
args.batch_modeling_cmds(&sketch.build_sketch_mode_cmds(
exec_state,
ModelingCmdReq {
cmd_id: id.into(),
cmd: ModelingCmd::from(mcmd::Extrude {
target: sketch.id.into(),
distance: LengthUnit(length),
faces: Default::default(),
}),
},
))
.await?;
solids.push(do_post_extrude(sketch.clone(), id.into(), length, exec_state, args.clone()).await?);
}
Ok(solids)
}
pub(crate) async fn do_post_extrude(
sketch: Sketch,
solid_id: ArtifactId,
length: f64,
exec_state: &mut ExecState,
args: Args,
) -> Result<Solid, KclError> {
// Bring the object to the front of the scene.
// See: https://github.com/KittyCAD/modeling-app/issues/806
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
)
.await?;
// 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 {
return Err(KclError::Type(KclErrorDetails {
message: "Expected a non-empty sketch".to_string(),
source_ranges: vec![args.source_range],
}));
};
let mut sketch = sketch.clone();
// If we were sketching on a face, we need the original face id.
if let SketchSurface::Face(ref face) = sketch.on {
sketch.id = face.solid.sketch.id;
}
let solid3d_info = args
.send_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
edge_id: any_edge_id,
object_id: sketch.id,
}),
)
.await?;
let face_infos = if let OkWebSocketResponseData::Modeling {
modeling_response: 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(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
edge_id: curve_id,
object_id: sketch.id,
face_id,
}),
)
.await?;
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
edge_id: curve_id,
object_id: sketch.id,
face_id,
}),
)
.await?;
}
let Faces {
sides: face_id_map,
start_cap_id,
end_cap_id,
} = analyze_faces(exec_state, &args, face_infos).await;
// Iterate over the sketch.value array and add face_id to GeoMeta
let no_engine_commands = args.ctx.no_engine_commands().await;
let new_value = sketch
.paths
.iter()
.flat_map(|path| {
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
match path {
Path::Arc { .. }
| Path::TangentialArc { .. }
| Path::TangentialArcTo { .. }
| Path::Circle { .. }
| Path::CircleThreePoint { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::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,
},
});
Some(extrude_surface)
}
Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::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,
},
});
Some(extrude_surface)
}
Path::ArcThreePoint { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::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,
},
});
Some(extrude_surface)
}
}
} else if no_engine_commands {
// Only pre-populate the extrude surface if we are in mock mode.
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
// pushing this values with a fake face_id to make extrudes mock-execute safe
face_id: exec_state.next_uuid(),
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata,
},
});
Some(extrude_surface)
} else {
None
}
})
.collect();
Ok(Solid {
// Ok so you would think that the id would be the id of the solid,
// that we passed in to the function, but it's actually the id of the
// sketch.
id: sketch.id,
artifact_id: solid_id,
value: new_value,
meta: sketch.meta.clone(),
units: sketch.units,
sketch,
height: length,
start_cap_id,
end_cap_id,
edge_cuts: vec![],
})
}
#[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>,
}
async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
let mut faces = Faces {
sides: HashMap::with_capacity(face_infos.len()),
..Default::default()
};
if args.ctx.no_engine_commands().await {
// Create fake IDs for start and end caps, to make extrudes mock-execute safe
faces.start_cap_id = Some(exec_state.next_uuid());
faces.end_cap_id = Some(exec_state.next_uuid());
}
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,
ExtrusionFaceCapType::Both => {
faces.end_cap_id = face_info.face_id;
faces.start_cap_id = face_info.face_id;
}
ExtrusionFaceCapType::None => {
if let Some(curve_id) = face_info.curve_id {
faces.sides.insert(curve_id, face_info.face_id);
}
}
}
}
faces
}