Files
modeling-app/rust/kcl-lib/src/std/planes.rs
Adam Chalmers fbcbb341e2 KCL: Add planeOf function to stdlib (#7643)
Gets the plane a face lies on, if any.

Closes #7642
2025-07-01 17:42:12 +00:00

188 lines
6.6 KiB
Rust

//! Standard library plane helpers.
use kcmc::{ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, shared::Color};
use kittycad_modeling_cmds::{self as kcmc, ok_response::OkModelingCmdResponse, websocket::OkWebSocketResponseData};
use super::{
args::TyF64,
sketch::{FaceTag, PlaneData},
};
use crate::{
UnitLen,
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Metadata, ModelingCmdMeta, Plane, PlaneType, types::RuntimeType},
std::Args,
};
/// Find the plane of a given face.
pub async fn plane_of(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
let face = args.get_kw_arg("face", &RuntimeType::tagged_face(), exec_state)?;
inner_plane_of(solid, face, exec_state, &args)
.await
.map(Box::new)
.map(|value| KclValue::Plane { value })
}
async fn inner_plane_of(
solid: crate::execution::Solid,
face: FaceTag,
exec_state: &mut ExecState,
args: &Args,
) -> Result<Plane, KclError> {
// Support mock execution
// Return an arbitrary (incorrect) plane and a non-fatal error.
if args.ctx.no_engine_commands().await {
let plane_id = exec_state.id_generator().next_uuid();
exec_state.err(crate::CompilationError {
source_range: args.source_range,
message: "The engine isn't available, so returning an arbitrary incorrect plane".to_owned(),
suggestion: None,
severity: crate::errors::Severity::Error,
tag: crate::errors::Tag::None,
});
return Ok(Plane {
artifact_id: plane_id.into(),
id: plane_id,
// Engine doesn't know about the ID we created, so set this to Uninit.
value: PlaneType::Uninit,
info: crate::execution::PlaneInfo {
origin: Default::default(),
x_axis: Default::default(),
y_axis: Default::default(),
},
meta: vec![Metadata {
source_range: args.source_range,
}],
});
}
// Query the engine to learn what plane, if any, this face is on.
let face_id = face.get_face_id(&solid, exec_state, args, true).await?;
let meta = args.into();
let cmd = ModelingCmd::FaceIsPlanar(mcmd::FaceIsPlanar { object_id: face_id });
let plane_resp = exec_state.send_modeling_cmd(meta, cmd).await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::FaceIsPlanar(planar),
} = plane_resp
else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Engine returned invalid response, it should have returned FaceIsPlanar but it returned {plane_resp:#?}"
),
vec![args.source_range],
)));
};
// Destructure engine's response to check if the face was on a plane.
let not_planar: Result<_, KclError> = Err(KclError::new_semantic(KclErrorDetails::new(
"The face you provided doesn't lie on any plane. It might be curved.".to_owned(),
vec![args.source_range],
)));
let Some(x_axis) = planar.x_axis else { return not_planar };
let Some(y_axis) = planar.y_axis else { return not_planar };
let Some(origin) = planar.origin else { return not_planar };
// Engine always returns measurements in mm.
let engine_units = UnitLen::Mm;
let x_axis = crate::execution::Point3d {
x: x_axis.x,
y: x_axis.y,
z: x_axis.z,
units: engine_units,
};
let y_axis = crate::execution::Point3d {
x: y_axis.x,
y: y_axis.y,
z: y_axis.z,
units: engine_units,
};
let origin = crate::execution::Point3d {
x: origin.x.0,
y: origin.y.0,
z: origin.z.0,
units: engine_units,
};
// Engine doesn't send back an ID, so let's just make a new plane ID.
let plane_id = exec_state.id_generator().next_uuid();
Ok(Plane {
artifact_id: plane_id.into(),
id: plane_id,
// Engine doesn't know about the ID we created, so set this to Uninit.
value: PlaneType::Uninit,
info: crate::execution::PlaneInfo { origin, x_axis, y_axis },
meta: vec![Metadata {
source_range: args.source_range,
}],
})
}
/// Offset a plane by a distance along its normal.
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let std_plane = args.get_unlabeled_kw_arg("plane", &RuntimeType::plane(), exec_state)?;
let offset: TyF64 = args.get_kw_arg("offset", &RuntimeType::length(), exec_state)?;
let plane = inner_offset_plane(std_plane, offset, exec_state, &args).await?;
Ok(KclValue::Plane { value: Box::new(plane) })
}
async fn inner_offset_plane(
plane: PlaneData,
offset: TyF64,
exec_state: &mut ExecState,
args: &Args,
) -> Result<Plane, KclError> {
let mut plane = Plane::from_plane_data(plane, exec_state)?;
// Though offset planes might be derived from standard planes, they are not
// standard planes themselves.
plane.value = PlaneType::Custom;
let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
plane.info.origin += normal * offset.to_length_units(plane.info.origin.units);
make_offset_plane_in_engine(&plane, exec_state, args).await?;
Ok(plane)
}
// Engine-side effectful creation of an actual plane object.
// offset planes are shown by default, and hidden by default if they
// are used as a sketch plane. That hiding command is sent within inner_start_profile_at
async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> {
// Create new default planes.
let default_size = 100.0;
let color = Color {
r: 0.6,
g: 0.6,
b: 0.6,
a: 0.3,
};
let meta = ModelingCmdMeta::from_args_id(args, plane.id);
exec_state
.batch_modeling_cmd(
meta,
ModelingCmd::from(mcmd::MakePlane {
clobber: false,
origin: plane.info.origin.into(),
size: LengthUnit(default_size),
x_axis: plane.info.x_axis.into(),
y_axis: plane.info.y_axis.into(),
hide: Some(false),
}),
)
.await?;
// Set the color.
exec_state
.batch_modeling_cmd(
args.into(),
ModelingCmd::from(mcmd::PlaneSetColor {
color,
plane_id: plane.id,
}),
)
.await?;
Ok(())
}