diff --git a/src/wasm-lib/kcl/src/executor.rs b/src/wasm-lib/kcl/src/executor.rs index fd7aa5924..808050dda 100644 --- a/src/wasm-lib/kcl/src/executor.rs +++ b/src/wasm-lib/kcl/src/executor.rs @@ -189,6 +189,15 @@ pub enum SketchGroupSet { SketchGroups(Vec>), } +impl SketchGroupSet { + pub fn ids(&self) -> Vec { + match self { + SketchGroupSet::SketchGroup(s) => vec![s.id], + SketchGroupSet::SketchGroups(s) => s.iter().map(|s| s.id).collect(), + } + } +} + /// A extrude group or a group of extrude groups. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[ts(export)] @@ -198,6 +207,15 @@ pub enum ExtrudeGroupSet { ExtrudeGroups(Vec>), } +impl ExtrudeGroupSet { + pub fn ids(&self) -> Vec { + match self { + ExtrudeGroupSet::ExtrudeGroup(s) => vec![s.id], + ExtrudeGroupSet::ExtrudeGroups(s) => s.iter().map(|s| s.id).collect(), + } + } +} + /// Data for an imported geometry. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[ts(export)] @@ -298,6 +316,34 @@ pub struct UserVal { pub meta: Vec, } +/// Wrap MemoryFunction to give it (shitty) JSON schema. +pub struct MemoryFunctionWrapper<'a> { + pub inner: &'a MemoryFunction, +} + +impl<'a> From<&'a MemoryFunction> for MemoryFunctionWrapper<'a> { + fn from(inner: &'a MemoryFunction) -> Self { + Self { inner } + } +} + +impl<'a> From> for &'a MemoryFunction { + fn from(value: MemoryFunctionWrapper<'a>) -> Self { + value.inner + } +} + +impl<'a> JsonSchema for MemoryFunctionWrapper<'a> { + fn schema_name() -> String { + "Function".to_owned() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + // TODO: Actually generate a reasonable schema. + gen.subschema_for::<()>() + } +} + pub type MemoryFunction = fn( s: Vec, @@ -413,6 +459,84 @@ impl MemoryItem { }; func(args, memory, expression.clone(), meta.clone(), ctx).await } + + fn as_user_val(&self) -> Option<&UserVal> { + if let MemoryItem::UserVal(x) = self { + Some(x) + } else { + None + } + } + + /// If this value is of type function, return it. + pub fn get_function(&self, source_ranges: Vec) -> Result<&MemoryFunction, KclError> { + let MemoryItem::Function { + func, + expression, + meta: _, + } = &self + else { + return Err(KclError::Semantic(KclErrorDetails { + message: "not a in memory function".to_string(), + source_ranges, + })); + }; + func.as_ref().ok_or_else(|| { + KclError::Semantic(KclErrorDetails { + message: format!("Not a function: {:?}", expression), + source_ranges, + }) + }) + } + + /// If this value is of type u32, return it. + pub fn get_u32(&self, source_ranges: Vec) -> Result { + let err = KclError::Semantic(KclErrorDetails { + message: "Expected an integer >= 0".to_owned(), + source_ranges, + }); + self.as_user_val() + .and_then(|uv| uv.value.as_number()) + .and_then(|n| n.as_u64()) + .and_then(|n| u32::try_from(n).ok()) + .ok_or(err) + } + + /// If this contains a sketch group set, return it. + pub(crate) fn as_sketch_group_set(&self, sr: SourceRange) -> Result { + let sketch_set = if let MemoryItem::SketchGroup(sg) = self { + SketchGroupSet::SketchGroup(sg.clone()) + } else if let MemoryItem::SketchGroups { value } = self { + SketchGroupSet::SketchGroups(value.clone()) + } else { + return Err(KclError::Type(KclErrorDetails { + message: format!( + "Expected a SketchGroup or Vector of SketchGroups as this argument, found {:?}", + self, + ), + source_ranges: vec![sr], + })); + }; + Ok(sketch_set) + } + + /// If this contains an extrude group set, return it. + pub(crate) fn as_extrude_group_set(&self, sr: SourceRange) -> Result { + let sketch_set = if let MemoryItem::ExtrudeGroup(sg) = self { + ExtrudeGroupSet::ExtrudeGroup(sg.clone()) + } else if let MemoryItem::ExtrudeGroups { value } = self { + ExtrudeGroupSet::ExtrudeGroups(value.clone()) + } else { + return Err(KclError::Type(KclErrorDetails { + message: format!( + "Expected an ExtrudeGroup or Vector of ExtrudeGroups as this argument, found {:?}", + self, + ), + source_ranges: vec![sr], + })); + }; + Ok(sketch_set) + } } /// A sketch group is a collection of paths. diff --git a/src/wasm-lib/kcl/src/std/mod.rs b/src/wasm-lib/kcl/src/std/mod.rs index 04f0bc461..71b0c65e5 100644 --- a/src/wasm-lib/kcl/src/std/mod.rs +++ b/src/wasm-lib/kcl/src/std/mod.rs @@ -25,14 +25,15 @@ use lazy_static::lazy_static; use parse_display::{Display, FromStr}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use crate::{ ast::types::parse_json_number_as_f64, docs::StdLibFn, errors::{KclError, KclErrorDetails}, executor::{ - ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryItem, Metadata, SketchGroup, SketchGroupSet, - SketchSurface, SourceRange, + ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryFunction, MemoryItem, Metadata, SketchGroup, + SketchGroupSet, SketchSurface, SourceRange, }, std::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag}, }; @@ -84,6 +85,7 @@ lazy_static! { Box::new(crate::std::patterns::PatternLinear3D), Box::new(crate::std::patterns::PatternCircular2D), Box::new(crate::std::patterns::PatternCircular3D), + Box::new(crate::std::patterns::Pattern), Box::new(crate::std::chamfer::Chamfer), Box::new(crate::std::fillet::Fillet), Box::new(crate::std::fillet::GetOppositeEdge), @@ -387,6 +389,41 @@ impl Args { } } + /// Works with either 2D or 3D solids. + fn get_pattern_args(&self) -> std::result::Result<(u32, &MemoryFunction, Vec), KclError> { + let sr = vec![self.source_range]; + let mut args = self.args.iter(); + let num_repetitions = args.next().ok_or_else(|| { + KclError::Type(KclErrorDetails { + message: "Missing first argument (should be the number of repetitions)".to_owned(), + source_ranges: sr.clone(), + }) + })?; + let num_repetitions = num_repetitions.get_u32(sr.clone())?; + let transform = args.next().ok_or_else(|| { + KclError::Type(KclErrorDetails { + message: "Missing second argument (should be the transform function)".to_owned(), + source_ranges: sr.clone(), + }) + })?; + let transform = transform.get_function(sr.clone())?; + let sg = args.next().ok_or_else(|| { + KclError::Type(KclErrorDetails { + message: "Missing third argument (should be a Sketch/ExtrudeGroup or an array of Sketch/ExtrudeGroups)" + .to_owned(), + source_ranges: sr.clone(), + }) + })?; + let sketch_ids = sg.as_sketch_group_set(self.source_range); + let extrude_ids = sg.as_extrude_group_set(self.source_range); + let entity_ids = match (sketch_ids, extrude_ids) { + (Ok(group), _) => group.ids(), + (_, Ok(group)) => group.ids(), + (Err(e), _) => return Err(e), + }; + Ok((num_repetitions, transform, entity_ids)) + } + fn get_segment_name_sketch_group(&self) -> Result<(String, Box), KclError> { // Iterate over our args, the first argument should be a UserVal with a string value. // The second argument should be a SketchGroup. @@ -437,19 +474,7 @@ impl Args { }) })?; - let sketch_set = if let MemoryItem::SketchGroup(sg) = first_value { - SketchGroupSet::SketchGroup(sg.clone()) - } else if let MemoryItem::SketchGroups { value } = first_value { - SketchGroupSet::SketchGroups(value.clone()) - } else { - return Err(KclError::Type(KclErrorDetails { - message: format!( - "Expected a SketchGroup or Vector of SketchGroups as the first argument, found `{:?}`", - self.args - ), - source_ranges: vec![self.source_range], - })); - }; + let sketch_set = first_value.as_sketch_group_set(self.source_range)?; let second_value = self.args.get(1).ok_or_else(|| { KclError::Type(KclErrorDetails { @@ -672,19 +697,7 @@ impl Args { }) })?; - let sketch_set = if let MemoryItem::SketchGroup(sg) = second_value { - SketchGroupSet::SketchGroup(sg.clone()) - } else if let MemoryItem::SketchGroups { value } = second_value { - SketchGroupSet::SketchGroups(value.clone()) - } else { - return Err(KclError::Type(KclErrorDetails { - message: format!( - "Expected a SketchGroup or Vector of SketchGroups as the second argument, found `{:?}`", - self.args - ), - source_ranges: vec![self.source_range], - })); - }; + let sketch_set = second_value.as_sketch_group_set(self.source_range)?; Ok((data, sketch_set)) } @@ -953,19 +966,7 @@ impl Args { }) })?; - let sketch_set = if let MemoryItem::SketchGroup(sg) = second_value { - SketchGroupSet::SketchGroup(sg.clone()) - } else if let MemoryItem::SketchGroups { value } = second_value { - SketchGroupSet::SketchGroups(value.clone()) - } else { - return Err(KclError::Type(KclErrorDetails { - message: format!( - "Expected a SketchGroup or Vector of SketchGroups as the second argument, found `{:?}`", - self.args - ), - source_ranges: vec![self.source_range], - })); - }; + let sketch_set = second_value.as_sketch_group_set(self.source_range)?; Ok((number, sketch_set)) } diff --git a/src/wasm-lib/kcl/src/std/patterns.rs b/src/wasm-lib/kcl/src/std/patterns.rs index 4e509a2b2..0eed77768 100644 --- a/src/wasm-lib/kcl/src/std/patterns.rs +++ b/src/wasm-lib/kcl/src/std/patterns.rs @@ -5,13 +5,38 @@ use derive_docs::stdlib; use kittycad::types::ModelingCmd; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use crate::{ errors::{KclError, KclErrorDetails}, - executor::{ExtrudeGroup, ExtrudeGroupSet, Geometries, Geometry, MemoryItem, SketchGroup, SketchGroupSet}, + executor::{ + ExtrudeGroup, ExtrudeGroupSet, Geometries, Geometry, MemoryFunctionWrapper, MemoryItem, Point3d, SketchGroup, + SketchGroupSet, + }, std::{types::Uint, Args}, }; +const CANNOT_USE_ZERO_VECTOR: &str = + "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."; + +/// How to change each element of a pattern. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct LinearTransform { + /// Translate the replica this far along each dimension. + /// Defaults to zero vector (i.e. same position as the original). + #[serde(default)] + pub translate: Option, + /// Scale the replica's size along each axis. + /// Defaults to (1, 1, 1) (i.e. the same size as the original). + #[serde(default)] + pub scale: Option, + /// Whether to replicate the original solid in this instance. + #[serde(default)] + pub replicate: Option, +} + /// Data for a linear pattern on a 2D sketch. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[ts(export)] @@ -70,15 +95,29 @@ impl LinearPattern { } } +/// A linear pattern, either 2D or 3D. +/// Each element in the pattern repeats a particular piece of geometry. +/// The repetitions can be transformed by the `transform` parameter. +pub async fn pattern(args: Args) -> Result { + let (num_repetitions, transform, entity_ids) = args.get_pattern_args()?; + + let sketch_groups = inner_pattern( + num_repetitions, + MemoryFunctionWrapper::from(transform), + entity_ids, + &args, + ) + .await?; + Ok(MemoryItem::SketchGroups { value: sketch_groups }) +} + /// A linear pattern on a 2D sketch. pub async fn pattern_linear_2d(args: Args) -> Result { let (data, sketch_group_set): (LinearPattern2dData, SketchGroupSet) = args.get_data_and_sketch_group_set()?; if data.axis == [0.0, 0.0] { return Err(KclError::Semantic(KclErrorDetails { - message: - "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place." - .to_string(), + message: CANNOT_USE_ZERO_VECTOR.to_string(), source_ranges: vec![args.source_range], })); } @@ -87,6 +126,44 @@ pub async fn pattern_linear_2d(args: Args) -> Result { Ok(MemoryItem::SketchGroups { value: sketch_groups }) } +/// A linear pattern on a 2D or 3D solid. +/// Each repetition of the pattern can be transformed (e.g. scaled, translated, hidden, etc). +/// +/// ```no_run +/// The vase is 100 layers tall. +/// The 100 layers are replica of each other, with a slight transformation applied to each. +/// let vase = layer() |> pattern(100, transform, %) +/// // base radius +/// const r = 50 +/// // layer height +/// const h = 10 +/// // taper factor [0 - 1) +/// const t = 0.005 +/// // Each layer is just a pretty thin cylinder. +/// fn layer = () => { +/// return startSketchOn("XY") // or some other plane idk +/// |> circle([0, 0], 1, %) +/// |> extrude(h, %) +/// // Change each replica's radius and shift it up the Z axis. +/// fn transform = (replicaId) => { +/// return { +/// translate: [0, 0, replicaId*10] +/// scale: r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8)) +/// } +/// } +/// ``` +#[stdlib { + name = "pattern", +}] +async fn inner_pattern<'a>( + _num_repetitions: u32, + _transform: MemoryFunctionWrapper<'a>, + _ids: Vec, + _args: &'a Args, +) -> Result>, KclError> { + todo!() +} + /// A linear pattern on a 2D sketch. /// /// ```no_run diff --git a/src/wasm-lib/tests/executor/inputs/pattern_vase.kcl b/src/wasm-lib/tests/executor/inputs/pattern_vase.kcl new file mode 100644 index 000000000..43f98685c --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/pattern_vase.kcl @@ -0,0 +1,31 @@ +// Defines a vase. +// The vase is made of 100 layers. + +// Parameters +const r = 50 // base radius +const h = 10 // layer height +const t = 0.005 // taper factor [0-1) + +// Defines how to modify each layer of the vase. +// Each replica is shifted up the Z axis, and has a smoothly-varying radius +fn transform = (replicaId) => { + return { + translate: [0, 0, replicaId * 10], + scale: r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8)) + } +} + +// Each layer is just a pretty thin cylinder with a fillet. +fn layer = () => { + return startSketchOn("XY") // or some other plane idk + |> circle([0, 0], 1, %, 'tag1') + |> extrude(h, %) + |> fillet({ + radius: h / 2.01, + tags: ["tag1", getOppositeEdge("tag1", %)] + }, %) +} + +// The vase is 100 layers tall. +// The 100 layers are replica of each other, with a slight transformation applied to each. +let vase = layer() |> pattern(100, transform, %) diff --git a/src/wasm-lib/tests/executor/main.rs b/src/wasm-lib/tests/executor/main.rs index 96773cb98..d114c3ffb 100644 --- a/src/wasm-lib/tests/executor/main.rs +++ b/src/wasm-lib/tests/executor/main.rs @@ -92,6 +92,13 @@ async fn serial_test_riddle_small() { twenty_twenty::assert_image("tests/executor/outputs/riddle_small.png", &result, 0.999); } +#[tokio::test(flavor = "multi_thread")] +async fn serial_test_pattern_vase() { + let code = include_str!("inputs/pattern_vase.kcl"); + let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); + twenty_twenty::assert_image("tests/executor/outputs/pattern_vase.png", &result, 0.999); +} + #[tokio::test(flavor = "multi_thread")] async fn serial_test_lego() { let code = include_str!("inputs/lego.kcl");