use std::collections::HashMap;
use anyhow::Result;
use indexmap::IndexMap;
use kcmc::ModelingCmd;
use kcmc::each_cmd as mcmd;
use kcmc::length_unit::LengthUnit;
use kcmc::ok_response::OkModelingCmdResponse;
use kcmc::output::ExtrusionFaceInfo;
use kcmc::shared::ExtrudeReference;
use kcmc::shared::ExtrusionFaceCapType;
use kcmc::shared::Opposite;
use kcmc::shared::Point3d as KPoint3d; use kcmc::websocket::ModelingCmdReq;
use kcmc::websocket::OkWebSocketResponseData;
use kittycad_modeling_cmds::shared::Angle;
use kittycad_modeling_cmds::shared::BodyType;
use kittycad_modeling_cmds::shared::ExtrudeMethod;
use kittycad_modeling_cmds::shared::Point2d;
use kittycad_modeling_cmds::{self as kcmc};
use uuid::Uuid;
use super::DEFAULT_TOLERANCE_MM;
use super::args::TyF64;
use super::utils::point_to_mm;
use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::execution::ArtifactId;
use crate::execution::CreatorFace;
use crate::execution::ExecState;
use crate::execution::ExecutorContext;
use crate::execution::Extrudable;
use crate::execution::ExtrudeSurface;
use crate::execution::GeoMeta;
use crate::execution::KclValue;
use crate::execution::ModelingCmdMeta;
use crate::execution::Path;
use crate::execution::ProfileClosed;
use crate::execution::Segment;
use crate::execution::SegmentKind;
use crate::execution::Sketch;
use crate::execution::SketchSurface;
use crate::execution::Solid;
use crate::execution::SolidCreator;
use crate::execution::annotations;
use crate::execution::types::ArrayLen;
use crate::execution::types::PrimitiveType;
use crate::execution::types::RuntimeType;
use crate::parsing::ast::types::TagDeclarator;
use crate::parsing::ast::types::TagNode;
use crate::std::Args;
use crate::std::args::FromKclValue;
use crate::std::axis_or_reference::Point3dAxis3dOrGeometryReference;
use crate::std::solver::create_segments_in_engine;
pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_values: Vec<KclValue> = args.get_unlabeled_kw_arg(
"sketches",
&RuntimeType::Array(
Box::new(RuntimeType::Union(vec![
RuntimeType::sketch(),
RuntimeType::face(),
RuntimeType::tagged_face(),
RuntimeType::segment(),
])),
ArrayLen::Minimum(1),
),
exec_state,
)?;
let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
let to = args.get_kw_arg_opt(
"to",
&RuntimeType::Union(vec![
RuntimeType::point3d(),
RuntimeType::Primitive(PrimitiveType::Axis3d),
RuntimeType::Primitive(PrimitiveType::Edge),
RuntimeType::plane(),
RuntimeType::Primitive(PrimitiveType::Face),
RuntimeType::sketch(),
RuntimeType::Primitive(PrimitiveType::Solid),
RuntimeType::tagged_edge(),
RuntimeType::tagged_face(),
]),
exec_state,
)?;
let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
let bidirectional_length: Option<TyF64> =
args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
let method: Option<String> = args.get_kw_arg_opt("method", &RuntimeType::string(), exec_state)?;
let hide_seams: Option<bool> = args.get_kw_arg_opt("hideSeams", &RuntimeType::bool(), exec_state)?;
let body_type: Option<BodyType> = args.get_kw_arg_opt("bodyType", &RuntimeType::string(), exec_state)?;
let sketches = coerce_extrude_targets(
sketch_values,
body_type.unwrap_or_default(),
tag_start.as_ref(),
tag_end.as_ref(),
exec_state,
&args.ctx,
args.source_range,
)
.await?;
let result = inner_extrude(
sketches,
length,
to,
symmetric,
bidirectional_length,
tag_start,
tag_end,
twist_angle,
twist_angle_step,
twist_center,
tolerance,
method,
hide_seams,
body_type,
exec_state,
args,
)
.await?;
Ok(result.into())
}
async fn coerce_extrude_targets(
sketch_values: Vec<KclValue>,
body_type: BodyType,
tag_start: Option<&TagNode>,
tag_end: Option<&TagNode>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
source_range: crate::SourceRange,
) -> Result<Vec<Extrudable>, KclError> {
let mut extrudables = Vec::new();
let mut segments = Vec::new();
for value in sketch_values {
if let Some(segment) = value.clone().into_segment() {
segments.push(segment);
continue;
}
let Some(extrudable) = Extrudable::from_kcl_val(&value) else {
return Err(KclError::new_type(KclErrorDetails::new(
"Expected sketches, faces, tagged faces, or solved sketch segments for extrusion.".to_owned(),
vec![source_range],
)));
};
extrudables.push(extrudable);
}
if !segments.is_empty() && !extrudables.is_empty() {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot extrude sketch segments together with sketches or faces in the same call. Use separate `extrude()` calls.".to_owned(),
vec![source_range],
)));
}
if !segments.is_empty() {
if !matches!(body_type, BodyType::Surface) {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Extruding sketch segments is only supported for surface extrudes. Set `bodyType = SURFACE`."
.to_owned(),
vec![source_range],
)));
}
if tag_start.is_some() || tag_end.is_some() {
return Err(KclError::new_semantic(KclErrorDetails::new(
"`tagStart` and `tagEnd` are not supported when extruding sketch segments. Segment surface extrudes do not create start or end caps."
.to_owned(),
vec![source_range],
)));
}
let synthetic_sketch = build_segment_surface_sketch(segments, exec_state, ctx, source_range).await?;
return Ok(vec![Extrudable::from(synthetic_sketch)]);
}
Ok(extrudables)
}
pub(crate) async fn build_segment_surface_sketch(
mut segments: Vec<Segment>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
source_range: crate::SourceRange,
) -> Result<Sketch, KclError> {
let Some(first_segment) = segments.first() else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Expected at least one sketch segment.".to_owned(),
vec![source_range],
)));
};
let sketch_id = first_segment.sketch_id;
let sketch_surface = first_segment.surface.clone();
for segment in &segments {
if segment.sketch_id != sketch_id {
return Err(KclError::new_semantic(KclErrorDetails::new(
"All sketch segments passed to this operation must come from the same sketch.".to_owned(),
vec![source_range],
)));
}
if segment.surface != sketch_surface {
return Err(KclError::new_semantic(KclErrorDetails::new(
"All sketch segments passed to this operation must lie on the same sketch surface.".to_owned(),
vec![source_range],
)));
}
if matches!(segment.kind, SegmentKind::Point { .. }) {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Point segments cannot be used here. Select line, arc, or circle segments instead.".to_owned(),
vec![source_range],
)));
}
if segment.is_construction() {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Construction segments cannot be used here. Select non-construction sketch segments instead."
.to_owned(),
vec![source_range],
)));
}
}
let synthetic_sketch_id = exec_state.next_uuid();
let segment_tags = IndexMap::from_iter(segments.iter().filter_map(|segment| {
segment
.tag
.as_ref()
.map(|tag| (segment.object_id, TagDeclarator::new(&tag.value)))
}));
for segment in &mut segments {
segment.id = exec_state.next_uuid();
segment.sketch_id = synthetic_sketch_id;
segment.sketch = None;
}
create_segments_in_engine(
&sketch_surface,
synthetic_sketch_id,
&mut segments,
&segment_tags,
ctx,
exec_state,
source_range,
)
.await?
.ok_or_else(|| {
KclError::new_semantic(KclErrorDetails::new(
"Expected at least one usable sketch segment.".to_owned(),
vec![source_range],
))
})
}
#[allow(clippy::too_many_arguments)]
async fn inner_extrude(
extrudables: Vec<Extrudable>,
length: Option<TyF64>,
to: Option<Point3dAxis3dOrGeometryReference>,
symmetric: Option<bool>,
bidirectional_length: Option<TyF64>,
tag_start: Option<TagNode>,
tag_end: Option<TagNode>,
twist_angle: Option<TyF64>,
twist_angle_step: Option<TyF64>,
twist_center: Option<[TyF64; 2]>,
tolerance: Option<TyF64>,
method: Option<String>,
hide_seams: Option<bool>,
body_type: Option<BodyType>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Solid>, KclError> {
let body_type = body_type.unwrap_or_default();
if matches!(body_type, BodyType::Solid) && extrudables.iter().any(|sk| matches!(sk.is_closed(), ProfileClosed::No))
{
return Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot solid extrude an open profile. Either close the profile, or use a surface extrude.".to_owned(),
vec![args.source_range],
)));
}
let mut solids = Vec::new();
let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
let extrude_method = match method.as_deref() {
Some("new" | "NEW") => ExtrudeMethod::New,
Some("merge" | "MERGE") => ExtrudeMethod::Merge,
None => ExtrudeMethod::default(),
Some(other) => {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
vec![args.source_range],
)));
}
};
if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
return Err(KclError::new_semantic(KclErrorDetails::new(
"You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
.to_owned(),
vec![args.source_range],
)));
}
if (length.is_some() || twist_angle.is_some()) && to.is_some() {
return Err(KclError::new_semantic(KclErrorDetails::new(
"You cannot give `length` or `twist` params with the `to` param, you have to choose one or the other"
.to_owned(),
vec![args.source_range],
)));
}
let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
let opposite = match (symmetric, bidirection) {
(Some(true), _) => Opposite::Symmetric,
(None, None) => Opposite::None,
(Some(false), None) => Opposite::None,
(None, Some(length)) => Opposite::Other(length),
(Some(false), Some(length)) => Opposite::Other(length),
};
for extrudable in &extrudables {
let extrude_cmd_id = exec_state.next_uuid();
let sketch_or_face_id = extrudable.id_to_extrude(exec_state, &args, false).await?;
let cmd = match (&twist_angle, &twist_angle_step, &twist_center, length.clone(), &to) {
(Some(angle), angle_step, center, Some(length), None) => {
let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
let total_rotation_angle = Angle::from_degrees(angle.to_degrees(exec_state, args.source_range));
let angle_step_size = Angle::from_degrees(
angle_step
.clone()
.map(|a| a.to_degrees(exec_state, args.source_range))
.unwrap_or(15.0),
);
ModelingCmd::from(
mcmd::TwistExtrude::builder()
.target(sketch_or_face_id.into())
.distance(LengthUnit(length.to_mm()))
.center_2d(center)
.total_rotation_angle(total_rotation_angle)
.angle_step_size(angle_step_size)
.tolerance(tolerance)
.body_type(body_type)
.build(),
)
}
(None, None, None, Some(length), None) => ModelingCmd::from(
mcmd::Extrude::builder()
.target(sketch_or_face_id.into())
.distance(LengthUnit(length.to_mm()))
.opposite(opposite.clone())
.extrude_method(extrude_method)
.body_type(body_type)
.maybe_merge_coplanar_faces(hide_seams)
.build(),
),
(None, None, None, None, Some(to)) => match to {
Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(
mcmd::ExtrudeToReference::builder()
.target(sketch_or_face_id.into())
.reference(ExtrudeReference::Point {
point: KPoint3d {
x: LengthUnit(point[0].to_mm()),
y: LengthUnit(point[1].to_mm()),
z: LengthUnit(point[2].to_mm()),
},
})
.extrude_method(extrude_method)
.body_type(body_type)
.build(),
),
Point3dAxis3dOrGeometryReference::Axis { direction, origin } => ModelingCmd::from(
mcmd::ExtrudeToReference::builder()
.target(sketch_or_face_id.into())
.reference(ExtrudeReference::Axis {
axis: KPoint3d {
x: direction[0].to_mm(),
y: direction[1].to_mm(),
z: direction[2].to_mm(),
},
point: KPoint3d {
x: LengthUnit(origin[0].to_mm()),
y: LengthUnit(origin[1].to_mm()),
z: LengthUnit(origin[2].to_mm()),
},
})
.extrude_method(extrude_method)
.body_type(body_type)
.build(),
),
Point3dAxis3dOrGeometryReference::Plane(plane) => {
let plane_id = if plane.is_uninitialized() {
if plane.info.origin.units.is_none() {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Origin of plane has unknown units".to_string(),
vec![args.source_range],
)));
}
let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
plane.clone().info.into_plane_data(),
exec_state,
&args,
)
.await?;
sketch_plane.id
} else {
plane.id
};
ModelingCmd::from(
mcmd::ExtrudeToReference::builder()
.target(sketch_or_face_id.into())
.reference(ExtrudeReference::EntityReference { entity_id: plane_id })
.extrude_method(extrude_method)
.body_type(body_type)
.build(),
)
}
Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
ModelingCmd::from(
mcmd::ExtrudeToReference::builder()
.target(sketch_or_face_id.into())
.reference(ExtrudeReference::EntityReference { entity_id: edge_id })
.extrude_method(extrude_method)
.body_type(body_type)
.build(),
)
}
Point3dAxis3dOrGeometryReference::Face(face_tag) => {
let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
ModelingCmd::from(
mcmd::ExtrudeToReference::builder()
.target(sketch_or_face_id.into())
.reference(ExtrudeReference::EntityReference { entity_id: face_id })
.extrude_method(extrude_method)
.body_type(body_type)
.build(),
)
}
Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(
mcmd::ExtrudeToReference::builder()
.target(sketch_or_face_id.into())
.reference(ExtrudeReference::EntityReference {
entity_id: sketch_ref.id,
})
.extrude_method(extrude_method)
.body_type(body_type)
.build(),
),
Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(
mcmd::ExtrudeToReference::builder()
.target(sketch_or_face_id.into())
.reference(ExtrudeReference::EntityReference { entity_id: solid.id })
.extrude_method(extrude_method)
.body_type(body_type)
.build(),
),
Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
let tagged_edge_or_face_id = tagged_edge_or_face.id;
ModelingCmd::from(
mcmd::ExtrudeToReference::builder()
.target(sketch_or_face_id.into())
.reference(ExtrudeReference::EntityReference {
entity_id: tagged_edge_or_face_id,
})
.extrude_method(extrude_method)
.body_type(body_type)
.build(),
)
}
},
(Some(_), _, _, None, None) => {
return Err(KclError::new_semantic(KclErrorDetails::new(
"The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
vec![args.source_range],
)));
}
(_, _, _, None, None) => {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
vec![args.source_range],
)));
}
(_, _, _, Some(_), Some(_)) => {
return Err(KclError::new_semantic(KclErrorDetails::new(
"You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
vec![args.source_range],
)));
}
(_, _, _, _, _) => {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Invalid combination of parameters for extrusion.".to_owned(),
vec![args.source_range],
)));
}
};
let being_extruded = match extrudable {
Extrudable::Sketch(..) => BeingExtruded::Sketch,
Extrudable::Face(face_tag) => {
let face_id = sketch_or_face_id;
let solid_id = match face_tag.geometry() {
Some(crate::execution::Geometry::Solid(solid)) => solid.id,
Some(crate::execution::Geometry::Sketch(sketch)) => match sketch.on {
SketchSurface::Face(face) => face.solid.id,
SketchSurface::Plane(_) => sketch.id,
},
None => face_id,
};
BeingExtruded::Face { face_id, solid_id }
}
};
if let Some(post_extr_sketch) = extrudable.as_sketch() {
let cmds = post_extr_sketch.build_sketch_mode_cmds(
exec_state,
ModelingCmdReq {
cmd_id: extrude_cmd_id.into(),
cmd,
},
);
exec_state
.batch_modeling_cmds(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), &cmds)
.await?;
solids.push(
do_post_extrude(
&post_extr_sketch,
extrude_cmd_id.into(),
false,
&NamedCapTags {
start: tag_start.as_ref(),
end: tag_end.as_ref(),
},
extrude_method,
exec_state,
&args,
None,
None,
body_type,
being_extruded,
)
.await?,
);
} else {
return Err(KclError::new_type(KclErrorDetails::new(
"Expected a sketch for extrusion".to_owned(),
vec![args.source_range],
)));
}
}
Ok(solids)
}
#[derive(Debug, Default)]
pub(crate) struct NamedCapTags<'a> {
pub start: Option<&'a TagNode>,
pub end: Option<&'a TagNode>,
}
#[derive(Debug, Clone, Copy)]
pub enum BeingExtruded {
Sketch,
Face { face_id: Uuid, solid_id: Uuid },
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn do_post_extrude<'a>(
sketch: &Sketch,
extrude_cmd_id: ArtifactId,
sectional: bool,
named_cap_tags: &'a NamedCapTags<'a>,
extrude_method: ExtrudeMethod,
exec_state: &mut ExecState,
args: &Args,
edge_id: Option<Uuid>,
clone_id_map: Option<&HashMap<Uuid, Uuid>>, body_type: BodyType,
being_extruded: BeingExtruded,
) -> Result<Solid, KclError> {
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args(exec_state, args),
ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(sketch.id).build()),
)
.await?;
let any_edge_id = if let Some(edge_id) = sketch.mirror {
edge_id
} else if let Some(id) = edge_id {
id
} else {
let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
return Err(KclError::new_type(KclErrorDetails::new(
"Expected a non-empty sketch".to_owned(),
vec![args.source_range],
)));
};
any_edge_id
};
let mut extrusion_info_edge_id = any_edge_id;
if sketch.clone.is_some() && clone_id_map.is_some() {
extrusion_info_edge_id = if let Some(clone_map) = clone_id_map {
if let Some(new_edge_id) = clone_map.get(&extrusion_info_edge_id) {
*new_edge_id
} else {
extrusion_info_edge_id
}
} else {
any_edge_id
};
}
let mut sketch = sketch.clone();
match body_type {
BodyType::Solid => {
sketch.is_closed = ProfileClosed::Explicitly;
}
BodyType::Surface => {}
_other => {
}
}
match (extrude_method, being_extruded) {
(ExtrudeMethod::Merge, BeingExtruded::Face { .. }) => {
if let SketchSurface::Face(ref face) = sketch.on {
sketch.id = face.solid.sketch_id().unwrap_or(face.solid.id);
}
}
(ExtrudeMethod::New, BeingExtruded::Face { .. }) => {
sketch.id = extrude_cmd_id.into();
}
(ExtrudeMethod::New, BeingExtruded::Sketch) => {
}
(ExtrudeMethod::Merge, BeingExtruded::Sketch) => {
if let SketchSurface::Face(ref face) = sketch.on {
sketch.id = face.solid.sketch_id().unwrap_or(face.solid.id);
}
}
(other, _) => {
return Err(KclError::new_internal(KclErrorDetails::new(
format!("Zoo does not yet support creating bodies via {other:?}"),
vec![args.source_range],
)));
}
}
let sketch_id = if let Some(cloned_from) = sketch.clone
&& clone_id_map.is_some()
{
cloned_from
} else {
sketch.id
};
let solid3d_info = exec_state
.send_modeling_cmd(
ModelingCmdMeta::from_args(exec_state, args),
ModelingCmd::from(
mcmd::Solid3dGetExtrusionFaceInfo::builder()
.edge_id(extrusion_info_edge_id)
.object_id(sketch_id)
.build(),
),
)
.await?;
let face_infos = if let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
} = solid3d_info
{
data.faces
} else {
vec![]
};
#[cfg(feature = "artifact-graph")]
{
if !sectional {
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args(exec_state, args),
ModelingCmd::from(
mcmd::Solid3dGetAdjacencyInfo::builder()
.object_id(sketch.id)
.edge_id(any_edge_id)
.build(),
),
)
.await?;
}
}
let Faces {
sides: mut face_id_map,
start_cap_id,
end_cap_id,
} = analyze_faces(exec_state, args, face_infos).await;
if sketch.clone.is_some()
&& let Some(clone_id_map) = clone_id_map
{
face_id_map = face_id_map
.into_iter()
.filter_map(|(k, v)| {
let fe_key = clone_id_map.get(&k)?;
let fe_value = clone_id_map.get(&(v?)).copied();
Some((*fe_key, fe_value))
})
.collect::<HashMap<Uuid, Option<Uuid>>>();
}
let no_engine_commands = args.ctx.no_engine_commands().await;
let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
let outer_surfaces = sketch.paths.iter().flat_map(|path| {
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
surface_of(path, *actual_face_id)
} else if no_engine_commands {
crate::log::logln!(
"No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
path.get_base().geo_meta.id
);
fake_extrude_surface(exec_state, path)
} else if sketch.clone.is_some()
&& let Some(clone_map) = clone_id_map
{
let new_path = clone_map.get(&(path.get_base().geo_meta.id));
if let Some(new_path) = new_path {
match face_id_map.get(new_path) {
Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
_ => {
let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
if let Some(value) = value {
if value == new_path { Some(key) } else { None }
} else {
None
}
});
match actual_face_id {
Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
None => {
crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
None
}
}
}
}
} else {
None
}
} else {
crate::log::logln!(
"No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
path.get_base().geo_meta.id
);
None
}
});
new_value.extend(outer_surfaces);
let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
surface_of(path, *actual_face_id)
} else if no_engine_commands {
fake_extrude_surface(exec_state, path)
} else {
None
}
});
new_value.extend(inner_surfaces);
if let Some(tag_start) = named_cap_tags.start {
let Some(start_cap_id) = start_cap_id else {
return Err(KclError::new_type(KclErrorDetails::new(
format!(
"Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
tag_start.name, sketch.id
),
vec![args.source_range],
)));
};
new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
face_id: start_cap_id,
tag: Some(tag_start.clone()),
geo_meta: GeoMeta {
id: start_cap_id,
metadata: args.source_range.into(),
},
}));
}
if let Some(tag_end) = named_cap_tags.end {
let Some(end_cap_id) = end_cap_id else {
return Err(KclError::new_type(KclErrorDetails::new(
format!(
"Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
tag_end.name, sketch.id
),
vec![args.source_range],
)));
};
new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
face_id: end_cap_id,
tag: Some(tag_end.clone()),
geo_meta: GeoMeta {
id: end_cap_id,
metadata: args.source_range.into(),
},
}));
}
let meta = sketch.meta.clone();
let units = sketch.units;
let id = sketch.id;
let creator = match being_extruded {
BeingExtruded::Sketch => SolidCreator::Sketch(sketch),
BeingExtruded::Face { face_id, solid_id } => SolidCreator::Face(CreatorFace {
face_id,
solid_id,
sketch,
}),
};
Ok(Solid {
id,
artifact_id: extrude_cmd_id,
value: new_value,
meta,
units,
sectional,
creator,
start_cap_id,
end_cap_id,
edge_cuts: vec![],
})
}
#[derive(Default)]
struct Faces {
sides: HashMap<Uuid, Option<Uuid>>,
end_cap_id: Option<Uuid>,
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 {
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);
}
}
other => {
exec_state.warn(
crate::CompilationIssue {
source_range: args.source_range,
message: format!("unknown extrusion face type {other:?}"),
suggestion: None,
severity: crate::errors::Severity::Warning,
tag: crate::errors::Tag::Unnecessary,
},
annotations::WARN_NOT_YET_SUPPORTED,
);
}
}
}
faces
}
fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
match path {
Path::Arc { .. }
| Path::TangentialArc { .. }
| Path::TangentialArcTo { .. }
| Path::Ellipse { .. }
| Path::Conic {.. }
| 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 { .. } | Path::Bezier { .. } => {
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)
}
}
}
fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
match path {
Path::Arc { .. }
| Path::TangentialArc { .. }
| Path::TangentialArcTo { .. }
| Path::Ellipse { .. }
| Path::Conic {.. }
| 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: clone_path_id,
metadata: path.get_base().geo_meta.metadata,
},
});
Some(extrude_surface)
}
Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
face_id: actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: clone_path_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: clone_path_id,
metadata: path.get_base().geo_meta.metadata,
},
});
Some(extrude_surface)
}
}
}
fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
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)
}
#[cfg(test)]
mod tests {
use kittycad_modeling_cmds::units::UnitLength;
use super::*;
use crate::execution::AbstractSegment;
use crate::execution::Plane;
use crate::execution::SegmentRepr;
use crate::execution::types::NumericType;
use crate::front::Expr;
use crate::front::Number;
use crate::front::ObjectId;
use crate::front::Point2d;
use crate::front::PointCtor;
use crate::std::sketch::PlaneData;
fn point_expr(x: f64, y: f64) -> Point2d<Expr> {
Point2d {
x: Expr::Var(Number::from((x, UnitLength::Millimeters))),
y: Expr::Var(Number::from((y, UnitLength::Millimeters))),
}
}
fn segment_value(exec_state: &mut ExecState) -> KclValue {
let plane = Plane::from_plane_data_skipping_engine(PlaneData::XY, exec_state).unwrap();
let segment = Segment {
id: exec_state.next_uuid(),
object_id: ObjectId(1),
kind: SegmentKind::Point {
position: [TyF64::new(0.0, NumericType::mm()), TyF64::new(0.0, NumericType::mm())],
ctor: Box::new(PointCtor {
position: point_expr(0.0, 0.0),
}),
freedom: None,
},
surface: SketchSurface::Plane(Box::new(plane)),
sketch_id: exec_state.next_uuid(),
sketch: None,
tag: None,
node_path: None,
meta: vec![],
};
KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(segment),
},
meta: vec![],
}),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn segment_extrude_rejects_cap_tags() {
let ctx = ExecutorContext::new_mock(None).await;
let mut exec_state = ExecState::new(&ctx);
let err = coerce_extrude_targets(
vec![segment_value(&mut exec_state)],
BodyType::Surface,
Some(&TagDeclarator::new("cap_start")),
None,
&mut exec_state,
&ctx,
crate::SourceRange::default(),
)
.await
.unwrap_err();
assert!(
err.message()
.contains("`tagStart` and `tagEnd` are not supported when extruding sketch segments"),
"{err:?}"
);
ctx.close().await;
}
}