KCL: patternTransform for sketches (#4546)

KCL stdlib has a function `patternTransform` which works for 3D solids. This adds a similar function `patternTransform2d` which, as you might have guessed, is like `patternTransform` but for 2D. I know. I'm really, really really good at naming things.

This shares almost all of its implementation with 3D patterns via 💖the power of traits💖

This will assist with https://github.com/KittyCAD/modeling-app/issues/4543
This commit is contained in:
Adam Chalmers
2024-11-22 19:43:09 -06:00
committed by GitHub
parent b415349af6
commit 7b36a5d1ee
7 changed files with 2970 additions and 43 deletions

View File

@ -74,6 +74,7 @@ layout: manual
* [`patternLinear2d`](kcl/patternLinear2d)
* [`patternLinear3d`](kcl/patternLinear3d)
* [`patternTransform`](kcl/patternTransform)
* [`patternTransform2d`](kcl/patternTransform2d)
* [`pi`](kcl/pi)
* [`polar`](kcl/polar)
* [`polygon`](kcl/polygon)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -115,14 +115,10 @@ fn do_stdlib_inner(
let name = metadata.name;
// Fail if the name is not camel case.
let whitelist = [
"mirror2d",
"patternLinear3d",
"patternLinear2d",
"patternCircular3d",
"patternCircular2d",
];
if !name.is_camel_case() && !whitelist.contains(&name.as_str()) {
// Remove some known suffix exceptions first.
let name_cleaned = name.strip_suffix("2d").unwrap_or(name.as_str());
let name_cleaned = name.strip_suffix("3d").unwrap_or(name_cleaned);
if !name_cleaned.is_camel_case() {
errors.push(Error::new_spanned(
&ast.sig.ident,
format!("stdlib function names must be in camel case: `{}`", name),

View File

@ -101,6 +101,7 @@ lazy_static! {
Box::new(crate::std::patterns::PatternCircular2D),
Box::new(crate::std::patterns::PatternCircular3D),
Box::new(crate::std::patterns::PatternTransform),
Box::new(crate::std::patterns::PatternTransform2D),
Box::new(crate::std::array::Reduce),
Box::new(crate::std::array::Map),
Box::new(crate::std::array::Push),

View File

@ -14,10 +14,13 @@ use kittycad_modeling_cmds::{
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExecState, Geometries, Geometry, KclValue, Point3d, Sketch, SketchSet, Solid, SolidSet, SourceRange},
executor::{
ExecState, Geometries, Geometry, KclValue, Point2d, Point3d, Sketch, SketchSet, Solid, SolidSet, SourceRange,
},
function_param::FunctionParam,
std::{types::Uint, Args},
};
@ -83,9 +86,7 @@ impl LinearPattern {
}
}
/// A linear pattern
/// Each element in the pattern repeats a particular piece of geometry.
/// The repetitions can be transformed by the `transform` parameter.
/// Repeat some 3D solid, changing each repetition slightly.
pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (num_repetitions, transform, extr) = args.get_pattern_transform_args()?;
@ -106,6 +107,28 @@ pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result
Ok(KclValue::Solids { value: solids })
}
/// Repeat some 2D sketch, changing each repetition slightly.
pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (num_repetitions, transform, sketch): (u32, super::FnAsArg<'_>, SketchSet) =
super::args::FromArgs::from_args(&args, 0)?;
let sketches = inner_pattern_transform_2d(
num_repetitions,
FunctionParam {
inner: transform.func,
fn_expr: transform.expr,
meta: vec![args.source_range.into()],
ctx: args.ctx.clone(),
memory: *transform.memory,
},
sketch,
exec_state,
&args,
)
.await?;
Ok(KclValue::Sketches { value: sketches })
}
/// Repeat a 3-dimensional solid, changing it each time.
///
/// Replicates the 3D solid, applying a transformation function to each replica.
@ -305,54 +328,88 @@ async fn inner_pattern_transform<'a>(
}));
}
for i in 1..total_instances {
let t = make_transform(i, &transform_function, args.source_range, exec_state).await?;
let t = make_transform::<Box<Solid>>(i, &transform_function, args.source_range, exec_state).await?;
transform.push(t);
}
let transform = transform; // remove mutability
execute_pattern_transform(transform, solid_set, exec_state, args).await
}
async fn execute_pattern_transform<'a>(
transforms: Vec<Vec<Transform>>,
solid_set: SolidSet,
/// Just like patternTransform, but works on 2D sketches not 3D solids.
/// ```no_run
/// // Each instance will be shifted along the X axis.
/// fn transform = (id) => {
/// return { translate: [4 * id, 0] }
/// }
///
/// // Sketch 4 circles.
/// sketch001 = startSketchOn('XZ')
/// |> circle({ center: [0, 0], radius: 2 }, %)
/// |> patternTransform2d(4, transform, %)
/// ```
#[stdlib {
name = "patternTransform2d",
}]
async fn inner_pattern_transform_2d<'a>(
total_instances: u32,
transform_function: FunctionParam<'a>,
solid_set: SketchSet,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<Vec<Box<Solid>>, KclError> {
) -> Result<Vec<Box<Sketch>>, KclError> {
// Build the vec of transforms, one for each repetition.
let mut transform = Vec::with_capacity(usize::try_from(total_instances).unwrap());
if total_instances < 1 {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![args.source_range],
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
}));
}
for i in 1..total_instances {
let t = make_transform::<Box<Sketch>>(i, &transform_function, args.source_range, exec_state).await?;
transform.push(t);
}
execute_pattern_transform(transform, solid_set, exec_state, args).await
}
async fn execute_pattern_transform<'a, T: GeometryTrait>(
transforms: Vec<Vec<Transform>>,
geo_set: T::Set,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<Vec<T>, KclError> {
// Flush the batch for our fillets/chamfers if there are any.
// If we do not flush these, then you won't be able to pattern something with fillets.
// Flush just the fillets/chamfers that apply to these solids.
args.flush_batch_for_solid_set(exec_state, solid_set.clone().into())
.await?;
let starting_solids: Vec<Box<Solid>> = solid_set.into();
T::flush_batch(args, exec_state, geo_set.clone()).await?;
let starting: Vec<T> = geo_set.into();
if args.ctx.context_type == crate::executor::ContextType::Mock {
return Ok(starting_solids);
return Ok(starting);
}
let mut solids = Vec::new();
for e in starting_solids {
let new_solids = send_pattern_transform(transforms.clone(), &e, exec_state, args).await?;
solids.extend(new_solids);
let mut output = Vec::new();
for geo in starting {
let new = send_pattern_transform(transforms.clone(), &geo, exec_state, args).await?;
output.extend(new)
}
Ok(solids)
Ok(output)
}
async fn send_pattern_transform(
async fn send_pattern_transform<T: GeometryTrait>(
// This should be passed via reference, see
// https://github.com/KittyCAD/modeling-app/issues/2821
transforms: Vec<Vec<Transform>>,
solid: &Solid,
solid: &T,
exec_state: &mut ExecState,
args: &Args,
) -> Result<Vec<Box<Solid>>, KclError> {
) -> Result<Vec<T>, KclError> {
let id = exec_state.id_generator.next_uuid();
let resp = args
.send_modeling_cmd(
id,
ModelingCmd::from(mcmd::EntityLinearPatternTransform {
entity_id: solid.id,
entity_id: solid.id(),
transform: Default::default(),
transforms,
}),
@ -369,16 +426,16 @@ async fn send_pattern_transform(
}));
};
let mut geometries = vec![Box::new(solid.clone())];
for id in pattern_info.entity_ids.iter() {
let mut geometries = vec![solid.clone()];
for id in pattern_info.entity_ids.iter().copied() {
let mut new_solid = solid.clone();
new_solid.id = *id;
geometries.push(Box::new(new_solid));
new_solid.set_id(id);
geometries.push(new_solid);
}
Ok(geometries)
}
async fn make_transform<'a>(
async fn make_transform<'a, T: GeometryTrait>(
i: u32,
transform_function: &FunctionParam<'a>,
source_range: SourceRange,
@ -424,11 +481,11 @@ async fn make_transform<'a>(
transforms
.into_iter()
.map(|obj| transform_from_obj_fields(obj, source_ranges.clone()))
.map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone()))
.collect()
}
fn transform_from_obj_fields(
fn transform_from_obj_fields<T: GeometryTrait>(
transform: HashMap<String, KclValue>,
source_ranges: Vec<SourceRange>,
) -> Result<Transform, KclError> {
@ -444,14 +501,17 @@ fn transform_from_obj_fields(
}
None => true,
};
let scale = match transform.get("scale") {
Some(x) => array_to_point3d(x, source_ranges.clone())?,
Some(x) => T::array_to_point3d(x, source_ranges.clone())?,
None => Point3d { x: 1.0, y: 1.0, z: 1.0 },
};
let translate = match transform.get("translate") {
Some(x) => array_to_point3d(x, source_ranges.clone())?,
Some(x) => T::array_to_point3d(x, source_ranges.clone())?,
None => Point3d { x: 0.0, y: 0.0, z: 0.0 },
};
let mut rotation = Rotation::default();
if let Some(rot) = transform.get("rotation") {
let KclValue::Object { value: rot, meta: _ } = rot else {
@ -462,7 +522,7 @@ fn transform_from_obj_fields(
}));
};
if let Some(axis) = rot.get("axis") {
rotation.axis = array_to_point3d(axis, source_ranges.clone())?.into();
rotation.axis = T::array_to_point3d(axis, source_ranges.clone())?.into();
}
if let Some(angle) = rot.get("angle") {
match angle {
@ -482,12 +542,13 @@ fn transform_from_obj_fields(
KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
other => {
let origin = array_to_point3d(other, source_ranges.clone())?.into();
let origin = T::array_to_point3d(other, source_ranges.clone())?.into();
OriginType::Custom { origin }
}
};
}
}
Ok(Transform {
replicate,
scale: scale.into(),
@ -528,6 +589,81 @@ fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<P
Ok(Point3d { x, y, z })
}
fn array_to_point2d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point2d, KclError> {
let KclValue::Array { value: arr, meta } = val else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected an array of 2 numbers (i.e. a 2D point)".to_string(),
source_ranges,
}));
};
let len = arr.len();
if len != 2 {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Expected an array of 2 numbers (i.e. a 2D point) but found {len} items"),
source_ranges,
}));
};
// Gets an f64 from a KCL value.
let f = |k: &KclValue, component: char| {
use super::args::FromKclValue;
if let Some(value) = f64::from_kcl_val(k) {
Ok(value)
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("{component} component of this point was not a number"),
source_ranges: meta.iter().map(|m| m.source_range).collect(),
}))
}
};
let x = f(&arr[0], 'x')?;
let y = f(&arr[1], 'y')?;
Ok(Point2d { x, y })
}
trait GeometryTrait: Clone {
type Set: Into<Vec<Self>> + Clone;
fn id(&self) -> Uuid;
fn set_id(&mut self, id: Uuid);
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError>;
async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: Self::Set) -> Result<(), KclError>;
}
impl GeometryTrait for Box<Sketch> {
type Set = SketchSet;
fn set_id(&mut self, id: Uuid) {
self.id = id;
}
fn id(&self) -> Uuid {
self.id
}
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
let Point2d { x, y } = array_to_point2d(val, source_ranges)?;
Ok(Point3d { x, y, z: 0.0 })
}
async fn flush_batch(_: &Args, _: &mut ExecState, _: Self::Set) -> Result<(), KclError> {
Ok(())
}
}
impl GeometryTrait for Box<Solid> {
type Set = SolidSet;
fn set_id(&mut self, id: Uuid) {
self.id = id;
}
fn id(&self) -> Uuid {
self.id
}
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
array_to_point3d(val, source_ranges)
}
async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: Self::Set) -> Result<(), KclError> {
args.flush_batch_for_solid_set(exec_state, solid_set.into()).await
}
}
#[cfg(test)]
mod tests {
use super::*;

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB