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:
Adam Chalmers
2024-06-19 17:19:18 -05:00
parent 2c5a8d439f
commit 54e160e8d2
5 changed files with 285 additions and 45 deletions

View File

@ -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.

View File

@ -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))
}

View File

@ -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

View 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, %)

View File

@ -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");