2024-09-04 14:34:33 -07:00
|
|
|
//! Standard library lofts.
|
|
|
|
|
|
|
|
use anyhow::Result;
|
|
|
|
use derive_docs::stdlib;
|
2024-09-19 14:06:29 -07:00
|
|
|
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
|
2024-09-18 17:04:04 -05:00
|
|
|
use kittycad_modeling_cmds as kcmc;
|
2024-09-04 14:34:33 -07:00
|
|
|
use schemars::JsonSchema;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
errors::{KclError, KclErrorDetails},
|
2024-09-27 15:44:44 -07:00
|
|
|
executor::{ExecState, KclValue, Sketch, Solid},
|
2024-09-04 14:34:33 -07:00
|
|
|
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
|
|
|
|
};
|
|
|
|
|
2024-09-05 17:29:07 -07:00
|
|
|
const DEFAULT_V_DEGREE: u32 = 2;
|
2024-09-04 14:34:33 -07:00
|
|
|
|
|
|
|
/// Data for a loft.
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
|
|
|
#[ts(export)]
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
pub struct LoftData {
|
|
|
|
/// 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.
|
|
|
|
pub v_degree: Option<std::num::NonZeroU32>,
|
|
|
|
/// 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.
|
|
|
|
#[serde(default)]
|
|
|
|
pub bez_approximate_rational: Option<bool>,
|
|
|
|
/// This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
|
|
|
|
#[serde(default)]
|
|
|
|
pub base_curve_index: Option<u32>,
|
|
|
|
/// Tolerance for the loft operation.
|
|
|
|
#[serde(default)]
|
|
|
|
pub tolerance: Option<f64>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for LoftData {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
// This unwrap is safe because the default value is always greater than zero.
|
|
|
|
v_degree: Some(std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap()),
|
|
|
|
bez_approximate_rational: None,
|
|
|
|
base_curve_index: None,
|
|
|
|
tolerance: None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a 3D surface or solid by interpolating between two or more sketches.
|
2024-09-16 15:10:33 -04:00
|
|
|
pub async fn loft(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
2024-09-27 15:44:44 -07:00
|
|
|
let (sketches, data): (Vec<Sketch>, Option<LoftData>) = args.get_sketches_and_data()?;
|
2024-09-04 14:34:33 -07:00
|
|
|
|
2024-09-27 15:44:44 -07:00
|
|
|
let solid = inner_loft(sketches, data, args).await?;
|
|
|
|
Ok(KclValue::Solid(solid))
|
2024-09-04 14:34:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
/// const squareSketch = startSketchOn('XY')
|
|
|
|
/// |> startProfileAt([-100, 200], %)
|
|
|
|
/// |> line([200, 0], %)
|
|
|
|
/// |> line([0, -200], %)
|
|
|
|
/// |> line([-200, 0], %)
|
|
|
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
|
|
/// |> close(%)
|
|
|
|
///
|
|
|
|
/// const triangleSketch = startSketchOn(offsetPlane('XY', 75))
|
|
|
|
/// |> startProfileAt([0, 125], %)
|
|
|
|
/// |> line([-15, -30], %)
|
|
|
|
/// |> line([30, 0], %)
|
|
|
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
|
|
/// |> close(%)
|
|
|
|
///
|
|
|
|
/// loft([squareSketch, triangleSketch])
|
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
/// ```no_run
|
|
|
|
/// // Loft a square, a circle, and another circle.
|
|
|
|
/// const squareSketch = startSketchOn('XY')
|
|
|
|
/// |> startProfileAt([-100, 200], %)
|
|
|
|
/// |> line([200, 0], %)
|
|
|
|
/// |> line([0, -200], %)
|
|
|
|
/// |> line([-200, 0], %)
|
|
|
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
|
|
/// |> close(%)
|
|
|
|
///
|
|
|
|
/// const circleSketch0 = startSketchOn(offsetPlane('XY', 75))
|
2024-09-23 22:42:51 +10:00
|
|
|
/// |> circle({ center: [0, 100], radius: 50 }, %)
|
2024-09-04 14:34:33 -07:00
|
|
|
///
|
|
|
|
/// const circleSketch1 = startSketchOn(offsetPlane('XY', 150))
|
2024-09-23 22:42:51 +10:00
|
|
|
/// |> circle({ center: [0, 100], radius: 20 }, %)
|
2024-09-04 14:34:33 -07:00
|
|
|
///
|
|
|
|
/// loft([squareSketch, circleSketch0, circleSketch1])
|
|
|
|
/// ```
|
2024-09-05 16:18:03 -07:00
|
|
|
///
|
|
|
|
/// ```no_run
|
|
|
|
/// // Loft a square, a circle, and another circle with options.
|
|
|
|
/// const squareSketch = startSketchOn('XY')
|
|
|
|
/// |> startProfileAt([-100, 200], %)
|
|
|
|
/// |> line([200, 0], %)
|
|
|
|
/// |> line([0, -200], %)
|
|
|
|
/// |> line([-200, 0], %)
|
|
|
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
|
|
/// |> close(%)
|
|
|
|
///
|
|
|
|
/// const circleSketch0 = startSketchOn(offsetPlane('XY', 75))
|
2024-09-23 22:42:51 +10:00
|
|
|
/// |> circle({ center: [0, 100], radius: 50 }, %)
|
2024-09-05 16:18:03 -07:00
|
|
|
///
|
|
|
|
/// const circleSketch1 = startSketchOn(offsetPlane('XY', 150))
|
2024-09-23 22:42:51 +10:00
|
|
|
/// |> circle({ center: [0, 100], radius: 20 }, %)
|
2024-09-05 16:18:03 -07:00
|
|
|
///
|
|
|
|
/// loft([squareSketch, circleSketch0, circleSketch1], {
|
|
|
|
/// // This can be set to override the automatically determined
|
|
|
|
/// // topological base curve, which is usually the first section encountered.
|
|
|
|
/// baseCurveIndex: 0,
|
|
|
|
/// // 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.
|
|
|
|
/// bezApproximateRational: false,
|
|
|
|
/// // Tolerance for the loft operation.
|
|
|
|
/// tolerance: 0.000001,
|
|
|
|
/// // 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.
|
|
|
|
/// vDegree: 2,
|
|
|
|
/// })
|
|
|
|
/// ```
|
2024-09-04 14:34:33 -07:00
|
|
|
#[stdlib {
|
|
|
|
name = "loft",
|
|
|
|
}]
|
2024-09-27 15:44:44 -07:00
|
|
|
async fn inner_loft(sketches: Vec<Sketch>, data: Option<LoftData>, args: Args) -> Result<Box<Solid>, KclError> {
|
2024-09-04 14:34:33 -07:00
|
|
|
// Make sure we have at least two sketches.
|
2024-09-27 15:44:44 -07:00
|
|
|
if sketches.len() < 2 {
|
2024-09-04 14:34:33 -07:00
|
|
|
return Err(KclError::Semantic(KclErrorDetails {
|
|
|
|
message: format!(
|
|
|
|
"Loft requires at least two sketches, but only {} were provided.",
|
2024-09-27 15:44:44 -07:00
|
|
|
sketches.len()
|
2024-09-04 14:34:33 -07:00
|
|
|
),
|
|
|
|
source_ranges: vec![args.source_range],
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the loft data.
|
|
|
|
let data = data.unwrap_or_default();
|
|
|
|
|
|
|
|
let id = uuid::Uuid::new_v4();
|
|
|
|
args.batch_modeling_cmd(
|
|
|
|
id,
|
2024-09-18 17:04:04 -05:00
|
|
|
ModelingCmd::from(mcmd::Loft {
|
2024-09-27 15:44:44 -07:00
|
|
|
section_ids: sketches.iter().map(|group| group.id).collect(),
|
2024-09-04 14:34:33 -07:00
|
|
|
base_curve_index: data.base_curve_index,
|
|
|
|
bez_approximate_rational: data.bez_approximate_rational.unwrap_or(false),
|
2024-09-18 17:04:04 -05:00
|
|
|
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
|
2024-09-04 14:34:33 -07:00
|
|
|
v_degree: data
|
|
|
|
.v_degree
|
2024-09-18 17:04:04 -05:00
|
|
|
.unwrap_or_else(|| std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap()),
|
|
|
|
}),
|
2024-09-04 14:34:33 -07:00
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
// Using the first sketch as the base curve, idk we might want to change this later.
|
2024-09-27 15:44:44 -07:00
|
|
|
do_post_extrude(sketches[0].clone(), 0.0, args).await
|
2024-09-04 14:34:33 -07:00
|
|
|
}
|