Define transform patterns
Defines a `pattern` stdlib fn and parses args for it. TODO: The actual body of the `pattern` stdlib fn.
This commit is contained in:
		| @ -189,6 +189,15 @@ pub enum SketchGroupSet { | ||||
|     SketchGroups(Vec<Box<SketchGroup>>), | ||||
| } | ||||
|  | ||||
| impl SketchGroupSet { | ||||
|     pub fn ids(&self) -> Vec<uuid::Uuid> { | ||||
|         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<Box<ExtrudeGroup>>), | ||||
| } | ||||
|  | ||||
| impl ExtrudeGroupSet { | ||||
|     pub fn ids(&self) -> Vec<uuid::Uuid> { | ||||
|         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<Metadata>, | ||||
| } | ||||
|  | ||||
| /// 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<MemoryFunctionWrapper<'a>> 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<MemoryItem>, | ||||
| @ -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<SourceRange>) -> 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<SourceRange>) -> Result<u32, KclError> { | ||||
|         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<SketchGroupSet, KclError> { | ||||
|         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<ExtrudeGroupSet, KclError> { | ||||
|         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. | ||||
|  | ||||
| @ -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<Uuid>), 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<SketchGroup>), 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)) | ||||
|     } | ||||
|  | ||||
| @ -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<Point3d>, | ||||
|     /// 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<Point3d>, | ||||
|     /// Whether to replicate the original solid in this instance. | ||||
|     #[serde(default)] | ||||
|     pub replicate: Option<bool>, | ||||
| } | ||||
|  | ||||
| /// 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<MemoryItem, KclError> { | ||||
|     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<MemoryItem, KclError> { | ||||
|     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<MemoryItem, KclError> { | ||||
|     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<Uuid>, | ||||
|     _args: &'a Args, | ||||
| ) -> Result<Vec<Box<SketchGroup>>, KclError> { | ||||
|     todo!() | ||||
| } | ||||
|  | ||||
| /// A linear pattern on a 2D sketch. | ||||
| /// | ||||
| /// ```no_run | ||||
|  | ||||
							
								
								
									
										31
									
								
								src/wasm-lib/tests/executor/inputs/pattern_vase.kcl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/wasm-lib/tests/executor/inputs/pattern_vase.kcl
									
									
									
									
									
										Normal file
									
								
							| @ -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, %) | ||||
| @ -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"); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user