Loft (#3681)
* add loft Signed-off-by: Jess Frazelle <github@jessfraz.com> * add offsetPlane as well Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix offset Signed-off-by: Jess Frazelle <github@jessfraz.com> * change to 2 Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * add-docs Signed-off-by: Jess Frazelle <github@jessfraz.com> * docs Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
@ -56,6 +56,7 @@ layout: manual
|
||||
* [`line`](kcl/line)
|
||||
* [`lineTo`](kcl/lineTo)
|
||||
* [`ln`](kcl/ln)
|
||||
* [`loft`](kcl/loft)
|
||||
* [`log`](kcl/log)
|
||||
* [`log10`](kcl/log10)
|
||||
* [`log2`](kcl/log2)
|
||||
@ -63,6 +64,7 @@ layout: manual
|
||||
* [`max`](kcl/max)
|
||||
* [`min`](kcl/min)
|
||||
* [`mm`](kcl/mm)
|
||||
* [`offsetPlane`](kcl/offsetPlane)
|
||||
* [`patternCircular2d`](kcl/patternCircular2d)
|
||||
* [`patternCircular3d`](kcl/patternCircular3d)
|
||||
* [`patternLinear2d`](kcl/patternLinear2d)
|
||||
|
477
docs/kcl/loft.md
Normal file
138
docs/kcl/offsetPlane.md
Normal file
4938
docs/kcl/std.json
2
src/wasm-lib/Cargo.lock
generated
@ -1345,7 +1345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.12"
|
||||
version = "0.2.13"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.12"
|
||||
version = "0.2.13"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -294,6 +294,13 @@ impl Args {
|
||||
FromArgs::from_args(self, 0)
|
||||
}
|
||||
|
||||
pub(crate) fn get_sketch_groups_and_data<'a, T>(&'a self) -> Result<(Vec<SketchGroup>, Option<T>), KclError>
|
||||
where
|
||||
T: FromArgs<'a> + serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
|
||||
{
|
||||
FromArgs::from_args(self, 0)
|
||||
}
|
||||
|
||||
pub(crate) fn get_data_and_optional_tag<'a, T>(&'a self) -> Result<(T, Option<FaceTag>), KclError>
|
||||
where
|
||||
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
|
||||
@ -360,6 +367,13 @@ impl Args {
|
||||
FromArgs::from_args(self, 0)
|
||||
}
|
||||
|
||||
pub(crate) fn get_data_and_float<'a, T>(&'a self) -> Result<(T, f64), KclError>
|
||||
where
|
||||
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
|
||||
{
|
||||
FromArgs::from_args(self, 0)
|
||||
}
|
||||
|
||||
pub(crate) fn get_number_sketch_group_set(&self) -> Result<(f64, SketchGroupSet), KclError> {
|
||||
FromArgs::from_args(self, 0)
|
||||
}
|
||||
@ -620,6 +634,8 @@ impl_from_arg_via_json!(super::revolve::RevolveData);
|
||||
impl_from_arg_via_json!(super::sketch::SketchData);
|
||||
impl_from_arg_via_json!(crate::std::import::ImportFormat);
|
||||
impl_from_arg_via_json!(crate::std::polar::PolarCoordsData);
|
||||
impl_from_arg_via_json!(crate::std::loft::LoftData);
|
||||
impl_from_arg_via_json!(crate::std::planes::StandardPlane);
|
||||
impl_from_arg_via_json!(SketchGroup);
|
||||
impl_from_arg_via_json!(FaceTag);
|
||||
impl_from_arg_via_json!(String);
|
||||
@ -690,3 +706,13 @@ impl<'a> FromKclValue<'a> for SketchSurface {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromKclValue<'a> for Vec<SketchGroup> {
|
||||
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
|
||||
let KclValue::UserVal(uv) = arg else {
|
||||
return None;
|
||||
};
|
||||
|
||||
uv.get::<Vec<SketchGroup>>().map(|x| x.0)
|
||||
}
|
||||
}
|
||||
|
141
src/wasm-lib/kcl/src/std/loft.rs
Normal file
@ -0,0 +1,141 @@
|
||||
//! Standard library lofts.
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kittycad::types::ModelingCmd;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{ExtrudeGroup, KclValue, SketchGroup},
|
||||
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
|
||||
};
|
||||
|
||||
const DEFAULT_V_DEGREE: u32 = 1;
|
||||
|
||||
/// 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.
|
||||
pub async fn loft(args: Args) -> Result<KclValue, KclError> {
|
||||
let (sketch_groups, data): (Vec<SketchGroup>, Option<LoftData>) = args.get_sketch_groups_and_data()?;
|
||||
|
||||
let extrude_group = inner_loft(sketch_groups, data, args).await?;
|
||||
Ok(KclValue::ExtrudeGroup(extrude_group))
|
||||
}
|
||||
|
||||
/// 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))
|
||||
/// |> circle([0, 100], 50, %)
|
||||
///
|
||||
/// const circleSketch1 = startSketchOn(offsetPlane('XY', 150))
|
||||
/// |> circle([0, 100], 20, %)
|
||||
///
|
||||
/// loft([squareSketch, circleSketch0, circleSketch1])
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "loft",
|
||||
}]
|
||||
async fn inner_loft(
|
||||
sketch_groups: Vec<SketchGroup>,
|
||||
data: Option<LoftData>,
|
||||
args: Args,
|
||||
) -> Result<Box<ExtrudeGroup>, KclError> {
|
||||
// Make sure we have at least two sketches.
|
||||
if sketch_groups.len() < 2 {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Loft requires at least two sketches, but only {} were provided.",
|
||||
sketch_groups.len()
|
||||
),
|
||||
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,
|
||||
ModelingCmd::Loft {
|
||||
section_ids: sketch_groups.iter().map(|group| group.id).collect(),
|
||||
base_curve_index: data.base_curve_index,
|
||||
bez_approximate_rational: data.bez_approximate_rational.unwrap_or(false),
|
||||
tolerance: data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units)),
|
||||
v_degree: data
|
||||
.v_degree
|
||||
.unwrap_or_else(|| std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap())
|
||||
.into(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Using the first sketch as the base curve, idk we might want to change this later.
|
||||
do_post_extrude(sketch_groups[0].clone(), 0.0, id, args).await
|
||||
}
|
@ -9,8 +9,10 @@ pub mod fillet;
|
||||
pub mod helix;
|
||||
pub mod import;
|
||||
pub mod kcl_stdlib;
|
||||
pub mod loft;
|
||||
pub mod math;
|
||||
pub mod patterns;
|
||||
pub mod planes;
|
||||
pub mod polar;
|
||||
pub mod revolve;
|
||||
pub mod segment;
|
||||
@ -98,6 +100,8 @@ lazy_static! {
|
||||
Box::new(crate::std::shell::Shell),
|
||||
Box::new(crate::std::shell::Hollow),
|
||||
Box::new(crate::std::revolve::Revolve),
|
||||
Box::new(crate::std::loft::Loft),
|
||||
Box::new(crate::std::planes::OffsetPlane),
|
||||
Box::new(crate::std::import::Import),
|
||||
Box::new(crate::std::math::Cos),
|
||||
Box::new(crate::std::math::Sin),
|
||||
|
168
src/wasm-lib/kcl/src/std/planes.rs
Normal file
@ -0,0 +1,168 @@
|
||||
//! Standard library plane helpers.
|
||||
|
||||
use derive_docs::stdlib;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::KclError,
|
||||
executor::{KclValue, Metadata, Plane, UserVal},
|
||||
std::{sketch::PlaneData, Args},
|
||||
};
|
||||
|
||||
/// One of the standard planes.
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum StandardPlane {
|
||||
/// The XY plane.
|
||||
#[serde(rename = "XY", alias = "xy")]
|
||||
XY,
|
||||
/// The opposite side of the XY plane.
|
||||
#[serde(rename = "-XY", alias = "-xy")]
|
||||
NegXY,
|
||||
/// The XZ plane.
|
||||
#[serde(rename = "XZ", alias = "xz")]
|
||||
XZ,
|
||||
/// The opposite side of the XZ plane.
|
||||
#[serde(rename = "-XZ", alias = "-xz")]
|
||||
NegXZ,
|
||||
/// The YZ plane.
|
||||
#[serde(rename = "YZ", alias = "yz")]
|
||||
YZ,
|
||||
/// The opposite side of the YZ plane.
|
||||
#[serde(rename = "-YZ", alias = "-yz")]
|
||||
NegYZ,
|
||||
}
|
||||
|
||||
impl From<StandardPlane> for PlaneData {
|
||||
fn from(value: StandardPlane) -> Self {
|
||||
match value {
|
||||
StandardPlane::XY => PlaneData::XY,
|
||||
StandardPlane::NegXY => PlaneData::NegXY,
|
||||
StandardPlane::XZ => PlaneData::XZ,
|
||||
StandardPlane::NegXZ => PlaneData::NegXZ,
|
||||
StandardPlane::YZ => PlaneData::YZ,
|
||||
StandardPlane::NegYZ => PlaneData::NegYZ,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset a plane by a distance along its normal.
|
||||
pub async fn offset_plane(args: Args) -> Result<KclValue, KclError> {
|
||||
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
|
||||
|
||||
let plane = inner_offset_plane(std_plane, offset).await?;
|
||||
|
||||
Ok(KclValue::UserVal(UserVal::set(
|
||||
vec![Metadata {
|
||||
source_range: args.source_range,
|
||||
}],
|
||||
plane,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Offset a plane by a distance along its normal.
|
||||
///
|
||||
/// For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ'
|
||||
/// plane and 10 units away from it.
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Loft a square and a circle on the `XY` plane using offset.
|
||||
/// const squareSketch = startSketchOn('XY')
|
||||
/// |> startProfileAt([-100, 200], %)
|
||||
/// |> line([200, 0], %)
|
||||
/// |> line([0, -200], %)
|
||||
/// |> line([-200, 0], %)
|
||||
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
/// |> close(%)
|
||||
///
|
||||
/// const circleSketch = startSketchOn(offsetPlane('XY', 150))
|
||||
/// |> circle([0, 100], 50, %)
|
||||
///
|
||||
/// loft([squareSketch, circleSketch])
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Loft a square and a circle on the `XZ` plane using offset.
|
||||
/// const squareSketch = startSketchOn('XZ')
|
||||
/// |> startProfileAt([-100, 200], %)
|
||||
/// |> line([200, 0], %)
|
||||
/// |> line([0, -200], %)
|
||||
/// |> line([-200, 0], %)
|
||||
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
/// |> close(%)
|
||||
///
|
||||
/// const circleSketch = startSketchOn(offsetPlane('XZ', 150))
|
||||
/// |> circle([0, 100], 50, %)
|
||||
///
|
||||
/// loft([squareSketch, circleSketch])
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Loft a square and a circle on the `YZ` plane using offset.
|
||||
/// const squareSketch = startSketchOn('YZ')
|
||||
/// |> startProfileAt([-100, 200], %)
|
||||
/// |> line([200, 0], %)
|
||||
/// |> line([0, -200], %)
|
||||
/// |> line([-200, 0], %)
|
||||
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
/// |> close(%)
|
||||
///
|
||||
/// const circleSketch = startSketchOn(offsetPlane('YZ', 150))
|
||||
/// |> circle([0, 100], 50, %)
|
||||
///
|
||||
/// loft([squareSketch, circleSketch])
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Loft a square and a circle on the `-XZ` plane using offset.
|
||||
/// const squareSketch = startSketchOn('-XZ')
|
||||
/// |> startProfileAt([-100, 200], %)
|
||||
/// |> line([200, 0], %)
|
||||
/// |> line([0, -200], %)
|
||||
/// |> line([-200, 0], %)
|
||||
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
/// |> close(%)
|
||||
///
|
||||
/// const circleSketch = startSketchOn(offsetPlane('-XZ', -150))
|
||||
/// |> circle([0, 100], 50, %)
|
||||
///
|
||||
/// loft([squareSketch, circleSketch])
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "offsetPlane",
|
||||
}]
|
||||
async fn inner_offset_plane(std_plane: StandardPlane, offset: f64) -> Result<PlaneData, KclError> {
|
||||
// Convert to the plane type.
|
||||
let plane_data: PlaneData = std_plane.into();
|
||||
// Convert to a plane.
|
||||
let mut plane = Plane::from(plane_data);
|
||||
|
||||
match std_plane {
|
||||
StandardPlane::XY => {
|
||||
plane.origin.z += offset;
|
||||
}
|
||||
StandardPlane::XZ => {
|
||||
plane.origin.y -= offset;
|
||||
}
|
||||
StandardPlane::YZ => {
|
||||
plane.origin.x += offset;
|
||||
}
|
||||
StandardPlane::NegXY => {
|
||||
plane.origin.z -= offset;
|
||||
}
|
||||
StandardPlane::NegXZ => {
|
||||
plane.origin.y += offset;
|
||||
}
|
||||
StandardPlane::NegYZ => {
|
||||
plane.origin.x -= offset;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PlaneData::Plane {
|
||||
origin: Box::new(plane.origin),
|
||||
x_axis: Box::new(plane.x_axis),
|
||||
y_axis: Box::new(plane.y_axis),
|
||||
z_axis: Box::new(plane.z_axis),
|
||||
})
|
||||
}
|
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_loft0.png
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_loft1.png
Normal file
After Width: | Height: | Size: 126 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 56 KiB |