//! Standard library lofts. use std::num::NonZeroU32; use anyhow::Result; use kcl_derive_docs::stdlib; use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd}; use kittycad_modeling_cmds as kcmc; use super::{args::TyF64, DEFAULT_TOLERANCE}; use crate::{ errors::{KclError, KclErrorDetails}, execution::{ types::{NumericType, RuntimeType}, ExecState, KclValue, Sketch, Solid, }, parsing::ast::types::TagNode, std::{extrude::do_post_extrude, Args}, }; const DEFAULT_V_DEGREE: u32 = 2; /// Create a 3D surface or solid by interpolating between two or more sketches. pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result { let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?; let v_degree: NonZeroU32 = args .get_kw_arg_opt_typed("vDegree", &RuntimeType::count(), exec_state)? .unwrap_or(NonZeroU32::new(DEFAULT_V_DEGREE).unwrap()); // Attempt to approximate rational curves (such as arcs) using a bezier. // This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios // Over time, this field won't be necessary. let bez_approximate_rational = args .get_kw_arg_opt_typed("bezApproximateRational", &RuntimeType::bool(), exec_state)? .unwrap_or(false); // This can be set to override the automatically determined topological base curve, which is usually the first section encountered. let base_curve_index: Option = args.get_kw_arg_opt_typed("baseCurveIndex", &RuntimeType::count(), exec_state)?; // Tolerance for the loft operation. let tolerance: Option = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?; let tag_start = args.get_kw_arg_opt("tagStart")?; let tag_end = args.get_kw_arg_opt("tagEnd")?; let value = inner_loft( sketches, v_degree, bez_approximate_rational, base_curve_index, tolerance, tag_start, tag_end, exec_state, args, ) .await?; Ok(KclValue::Solid { value }) } /// Create a 3D surface or solid by interpolating between two or more sketches. /// /// The sketches need to closed and on the same plane. /// /// ```no_run /// // Loft a square and a triangle. /// squareSketch = startSketchOn(XY) /// |> startProfile(at = [-100, 200]) /// |> line(end = [200, 0]) /// |> line(end = [0, -200]) /// |> line(end = [-200, 0]) /// |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) /// |> close() /// /// triangleSketch = startSketchOn(offsetPlane(XY, offset = 75)) /// |> startProfile(at = [0, 125]) /// |> line(end = [-15, -30]) /// |> line(end = [30, 0]) /// |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) /// |> close() /// /// loft([triangleSketch, squareSketch]) /// ``` /// /// ```no_run /// // Loft a square, a circle, and another circle. /// squareSketch = startSketchOn(XY) /// |> startProfile(at = [-100, 200]) /// |> line(end = [200, 0]) /// |> line(end = [0, -200]) /// |> line(end = [-200, 0]) /// |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) /// |> close() /// /// circleSketch0 = startSketchOn(offsetPlane(XY, offset = 75)) /// |> circle( center = [0, 100], radius = 50 ) /// /// circleSketch1 = startSketchOn(offsetPlane(XY, offset = 150)) /// |> circle( center = [0, 100], radius = 20 ) /// /// loft([squareSketch, circleSketch0, circleSketch1]) /// ``` /// /// ```no_run /// // Loft a square, a circle, and another circle with options. /// squareSketch = startSketchOn(XY) /// |> startProfile(at = [-100, 200]) /// |> line(end = [200, 0]) /// |> line(end = [0, -200]) /// |> line(end = [-200, 0]) /// |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) /// |> close() /// /// circleSketch0 = startSketchOn(offsetPlane(XY, offset = 75)) /// |> circle( center = [0, 100], radius = 50 ) /// /// circleSketch1 = startSketchOn(offsetPlane(XY, offset = 150)) /// |> circle( center = [0, 100], radius = 20 ) /// /// loft([squareSketch, circleSketch0, circleSketch1], /// baseCurveIndex = 0, /// bezApproximateRational = false, /// tolerance = 0.000001, /// vDegree = 2, /// ) /// ``` #[stdlib { name = "loft", feature_tree_operation = true, unlabeled_first = true, args = { sketches = {docs = "Which sketches to loft. Must include at least 2 sketches."}, v_degree = {docs = "Degree of the interpolation. Must be greater than zero. For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction. This defaults to 2, if not specified."}, bez_approximate_rational = {docs = "Attempt to approximate rational curves (such as arcs) using a bezier. This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios Over time, this field won't be necessary."}, base_curve_index = {docs = "This can be set to override the automatically determined topological base curve, which is usually the first section encountered."}, tolerance = {docs = "Tolerance for the loft operation."}, tag_start = { docs = "A named tag for the face at the start of the loft, i.e. the original sketch" }, tag_end = { docs = "A named tag for the face at the end of the loft, i.e. the last sketch" }, }, tags = ["sketch"] }] #[allow(clippy::too_many_arguments)] async fn inner_loft( sketches: Vec, v_degree: NonZeroU32, bez_approximate_rational: bool, base_curve_index: Option, tolerance: Option, tag_start: Option, tag_end: Option, exec_state: &mut ExecState, args: Args, ) -> Result, KclError> { // Make sure we have at least two sketches. if sketches.len() < 2 { return Err(KclError::Semantic(KclErrorDetails::new( format!( "Loft requires at least two sketches, but only {} were provided.", sketches.len() ), vec![args.source_range], ))); } let id = exec_state.next_uuid(); args.batch_modeling_cmd( id, ModelingCmd::from(mcmd::Loft { section_ids: sketches.iter().map(|group| group.id).collect(), base_curve_index, bez_approximate_rational, tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)), v_degree, }), ) .await?; // Using the first sketch as the base curve, idk we might want to change this later. let mut sketch = sketches[0].clone(); // Override its id with the loft id so we can get its faces later sketch.id = id; Ok(Box::new( do_post_extrude( &sketch, id.into(), TyF64::new(0.0, NumericType::mm()), false, &super::extrude::NamedCapTags { start: tag_start.as_ref(), end: tag_end.as_ref(), }, exec_state, &args, None, ) .await?, )) }