kcl_lib/std/sweep.rs
1//! Standard library sweep.
2
3use anyhow::Result;
4use kcl_derive_docs::stdlib;
5use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
6use kittycad_modeling_cmds::{self as kcmc, shared::RelativeTo};
7use schemars::JsonSchema;
8use serde::Serialize;
9
10use super::{args::TyF64, DEFAULT_TOLERANCE};
11use crate::{
12 errors::KclError,
13 execution::{
14 types::{NumericType, RuntimeType},
15 ExecState, Helix, KclValue, Sketch, Solid,
16 },
17 parsing::ast::types::TagNode,
18 std::{extrude::do_post_extrude, Args},
19};
20
21/// A path to sweep along.
22#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
23#[ts(export)]
24#[serde(untagged)]
25pub enum SweepPath {
26 Sketch(Sketch),
27 Helix(Box<Helix>),
28}
29
30/// Extrude a sketch along a path.
31pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
32 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
33 let path: SweepPath = args.get_kw_arg_typed(
34 "path",
35 &RuntimeType::Union(vec![RuntimeType::sketch(), RuntimeType::helix()]),
36 exec_state,
37 )?;
38 let sectional = args.get_kw_arg_opt("sectional")?;
39 let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
40 let relative_to: Option<String> = args.get_kw_arg_opt_typed("relativeTo", &RuntimeType::string(), exec_state)?;
41 let tag_start = args.get_kw_arg_opt("tagStart")?;
42 let tag_end = args.get_kw_arg_opt("tagEnd")?;
43
44 let value = inner_sweep(
45 sketches,
46 path,
47 sectional,
48 tolerance,
49 relative_to,
50 tag_start,
51 tag_end,
52 exec_state,
53 args,
54 )
55 .await?;
56 Ok(value.into())
57}
58
59/// Extrude a sketch along a path.
60///
61/// This, like extrude, is able to create a 3-dimensional solid from a
62/// 2-dimensional sketch. However, unlike extrude, this creates a solid
63/// by using the extent of the sketch as its path. This is useful for
64/// creating more complex shapes that can't be created with a simple
65/// extrusion.
66///
67/// You can provide more than one sketch to sweep, and they will all be
68/// swept along the same path.
69///
70/// ```no_run
71/// // Create a pipe using a sweep.
72///
73/// // Create a path for the sweep.
74/// sweepPath = startSketchOn(XZ)
75/// |> startProfile(at = [0.05, 0.05])
76/// |> line(end = [0, 7])
77/// |> tangentialArc(angle = 90, radius = 5)
78/// |> line(end = [-3, 0])
79/// |> tangentialArc(angle = -90, radius = 5)
80/// |> line(end = [0, 7])
81///
82/// // Create a hole for the pipe.
83/// pipeHole = startSketchOn(XY)
84/// |> circle(
85/// center = [0, 0],
86/// radius = 1.5,
87/// )
88///
89/// sweepSketch = startSketchOn(XY)
90/// |> circle(
91/// center = [0, 0],
92/// radius = 2,
93/// )
94/// |> subtract2d(tool = pipeHole)
95/// |> sweep(path = sweepPath)
96/// ```
97///
98/// ```no_run
99/// // Create a spring by sweeping around a helix path.
100///
101/// // Create a helix around the Z axis.
102/// helixPath = helix(
103/// angleStart = 0,
104/// ccw = true,
105/// revolutions = 4,
106/// length = 10,
107/// radius = 5,
108/// axis = Z,
109/// )
110///
111///
112/// // Create a spring by sweeping around the helix path.
113/// springSketch = startSketchOn(XZ)
114/// |> circle( center = [5, 0], radius = 1)
115/// |> sweep(path = helixPath)
116/// ```
117///
118/// ```no_run
119/// // Sweep two sketches along the same path.
120///
121/// sketch001 = startSketchOn(XY)
122/// rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
123/// |> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
124/// |> angledLine(
125/// angle = segAng(rectangleSegmentA001) - 90,
126/// length = 50.61,
127/// )
128/// |> angledLine(
129/// angle = segAng(rectangleSegmentA001),
130/// length = -segLen(rectangleSegmentA001),
131/// )
132/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
133/// |> close()
134///
135/// circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
136///
137/// sketch002 = startSketchOn(YZ)
138/// sweepPath = startProfile(sketch002, at = [0, 0])
139/// |> yLine(length = 231.81)
140/// |> tangentialArc(radius = 80, angle = -90)
141/// |> xLine(length = 384.93)
142///
143/// sweep([rectangleSketch, circleSketch], path = sweepPath)
144/// ```
145/// ```
146/// // Sectionally sweep one sketch along the path
147///
148/// sketch001 = startSketchOn(XY)
149/// circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
150///
151/// sketch002 = startSketchOn(YZ)
152/// sweepPath = startProfile(sketch002, at = [0, 0])
153/// |> yLine(length = 231.81)
154/// |> tangentialArc(radius = 80, angle = -90)
155/// |> xLine(length = 384.93)
156///
157/// sweep(circleSketch, path = sweepPath, sectional = true)
158/// ```
159
160#[stdlib {
161 name = "sweep",
162 feature_tree_operation = true,
163 keywords = true,
164 unlabeled_first = true,
165 args = {
166 sketches = { docs = "The sketch or set of sketches that should be swept in space" },
167 path = { docs = "The path to sweep the sketch along" },
168 sectional = { docs = "If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components." },
169 tolerance = { docs = "Tolerance for this operation" },
170 relative_to = { docs = "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to trajectoryCurve."},
171 tag_start = { docs = "A named tag for the face at the start of the sweep, i.e. the original sketch" },
172 tag_end = { docs = "A named tag for the face at the end of the sweep" },
173 },
174 tags = ["sketch"]
175}]
176#[allow(clippy::too_many_arguments)]
177async fn inner_sweep(
178 sketches: Vec<Sketch>,
179 path: SweepPath,
180 sectional: Option<bool>,
181 tolerance: Option<TyF64>,
182 relative_to: Option<String>,
183 tag_start: Option<TagNode>,
184 tag_end: Option<TagNode>,
185 exec_state: &mut ExecState,
186 args: Args,
187) -> Result<Vec<Solid>, KclError> {
188 let trajectory = match path {
189 SweepPath::Sketch(sketch) => sketch.id.into(),
190 SweepPath::Helix(helix) => helix.value.into(),
191 };
192 let relative_to = match relative_to.as_deref() {
193 Some("sketchPlane") => RelativeTo::SketchPlane,
194 Some("trajectoryCurve") | None => RelativeTo::TrajectoryCurve,
195 Some(_) => {
196 return Err(KclError::Syntax(crate::errors::KclErrorDetails::new(
197 "If you provide relativeTo, it must either be 'sketchPlane' or 'trajectoryCurve'".to_owned(),
198 vec![args.source_range],
199 )))
200 }
201 };
202
203 let mut solids = Vec::new();
204 for sketch in &sketches {
205 let id = exec_state.next_uuid();
206 args.batch_modeling_cmd(
207 id,
208 ModelingCmd::from(mcmd::Sweep {
209 target: sketch.id.into(),
210 trajectory,
211 sectional: sectional.unwrap_or(false),
212 tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
213 relative_to,
214 }),
215 )
216 .await?;
217
218 solids.push(
219 do_post_extrude(
220 sketch,
221 id.into(),
222 TyF64::new(0.0, NumericType::mm()),
223 sectional.unwrap_or(false),
224 &super::extrude::NamedCapTags {
225 start: tag_start.as_ref(),
226 end: tag_end.as_ref(),
227 },
228 exec_state,
229 &args,
230 None,
231 )
232 .await?,
233 );
234 }
235
236 // Hide the artifact from the sketch or helix.
237 args.batch_modeling_cmd(
238 exec_state.next_uuid(),
239 ModelingCmd::from(mcmd::ObjectVisible {
240 object_id: trajectory.into(),
241 hidden: true,
242 }),
243 )
244 .await?;
245
246 Ok(solids)
247}