kcl_lib/std/loft.rs
1//! Standard library lofts.
2
3use std::num::NonZeroU32;
4
5use anyhow::Result;
6use derive_docs::stdlib;
7use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
8use kittycad_modeling_cmds as kcmc;
9
10use crate::{
11 errors::{KclError, KclErrorDetails},
12 execution::{ExecState, KclValue, Sketch, Solid},
13 std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
14};
15
16const DEFAULT_V_DEGREE: u32 = 2;
17
18/// Create a 3D surface or solid by interpolating between two or more sketches.
19pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
20 let sketches = args.get_unlabeled_kw_arg("sketches")?;
21 let v_degree: NonZeroU32 = args
22 .get_kw_arg_opt("vDegree")?
23 .unwrap_or(NonZeroU32::new(DEFAULT_V_DEGREE).unwrap());
24 // Attempt to approximate rational curves (such as arcs) using a bezier.
25 // This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios
26 // Over time, this field won't be necessary.
27 let bez_approximate_rational = args.get_kw_arg_opt("bezApproximateRational")?.unwrap_or(false);
28 // This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
29 let base_curve_index: Option<u32> = args.get_kw_arg_opt("baseCurveIndex")?;
30 // Tolerance for the loft operation.
31 let tolerance: Option<f64> = args.get_kw_arg_opt("tolerance")?;
32
33 let value = inner_loft(
34 sketches,
35 v_degree,
36 bez_approximate_rational,
37 base_curve_index,
38 tolerance,
39 exec_state,
40 args,
41 )
42 .await?;
43 Ok(KclValue::Solid { value })
44}
45
46/// Create a 3D surface or solid by interpolating between two or more sketches.
47///
48/// The sketches need to closed and on the same plane.
49///
50/// ```no_run
51/// // Loft a square and a triangle.
52/// squareSketch = startSketchOn('XY')
53/// |> startProfileAt([-100, 200], %)
54/// |> line(end = [200, 0])
55/// |> line(end = [0, -200])
56/// |> line(end = [-200, 0])
57/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
58/// |> close()
59///
60/// triangleSketch = startSketchOn(offsetPlane('XY', offset = 75))
61/// |> startProfileAt([0, 125], %)
62/// |> line(end = [-15, -30])
63/// |> line(end = [30, 0])
64/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
65/// |> close()
66///
67/// loft([squareSketch, triangleSketch])
68/// ```
69///
70/// ```no_run
71/// // Loft a square, a circle, and another circle.
72/// squareSketch = startSketchOn('XY')
73/// |> startProfileAt([-100, 200], %)
74/// |> line(end = [200, 0])
75/// |> line(end = [0, -200])
76/// |> line(end = [-200, 0])
77/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
78/// |> close()
79///
80/// circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
81/// |> circle({ center = [0, 100], radius = 50 }, %)
82///
83/// circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
84/// |> circle({ center = [0, 100], radius = 20 }, %)
85///
86/// loft([squareSketch, circleSketch0, circleSketch1])
87/// ```
88///
89/// ```no_run
90/// // Loft a square, a circle, and another circle with options.
91/// squareSketch = startSketchOn('XY')
92/// |> startProfileAt([-100, 200], %)
93/// |> line(end = [200, 0])
94/// |> line(end = [0, -200])
95/// |> line(end = [-200, 0])
96/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
97/// |> close()
98///
99/// circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
100/// |> circle({ center = [0, 100], radius = 50 }, %)
101///
102/// circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
103/// |> circle({ center = [0, 100], radius = 20 }, %)
104///
105/// loft([squareSketch, circleSketch0, circleSketch1],
106/// baseCurveIndex = 0,
107/// bezApproximateRational = false,
108/// tolerance = 0.000001,
109/// vDegree = 2,
110/// )
111/// ```
112#[stdlib {
113 name = "loft",
114 feature_tree_operation = true,
115 keywords = true,
116 unlabeled_first = true,
117 args = {
118 sketches = {docs = "Which sketches to loft. Must include at least 2 sketches."},
119 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."},
120 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."},
121 base_curve_index = {docs = "This can be set to override the automatically determined topological base curve, which is usually the first section encountered."},
122 tolerance = {docs = "Tolerance for the loft operation."},
123 }
124}]
125async fn inner_loft(
126 sketches: Vec<Sketch>,
127 v_degree: NonZeroU32,
128 bez_approximate_rational: bool,
129 base_curve_index: Option<u32>,
130 tolerance: Option<f64>,
131 exec_state: &mut ExecState,
132 args: Args,
133) -> Result<Box<Solid>, KclError> {
134 // Make sure we have at least two sketches.
135 if sketches.len() < 2 {
136 return Err(KclError::Semantic(KclErrorDetails {
137 message: format!(
138 "Loft requires at least two sketches, but only {} were provided.",
139 sketches.len()
140 ),
141 source_ranges: vec![args.source_range],
142 }));
143 }
144
145 let id = exec_state.next_uuid();
146 args.batch_modeling_cmd(
147 id,
148 ModelingCmd::from(mcmd::Loft {
149 section_ids: sketches.iter().map(|group| group.id).collect(),
150 base_curve_index,
151 bez_approximate_rational,
152 tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
153 v_degree,
154 }),
155 )
156 .await?;
157
158 // Using the first sketch as the base curve, idk we might want to change this later.
159 let mut sketch = sketches[0].clone();
160 // Override its id with the loft id so we can get its faces later
161 sketch.id = id;
162 do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await
163}