Transformable patterns (#2824)
This commit is contained in:
@ -55,6 +55,7 @@ layout: manual
|
|||||||
* [`patternCircular3d`](kcl/patternCircular3d)
|
* [`patternCircular3d`](kcl/patternCircular3d)
|
||||||
* [`patternLinear2d`](kcl/patternLinear2d)
|
* [`patternLinear2d`](kcl/patternLinear2d)
|
||||||
* [`patternLinear3d`](kcl/patternLinear3d)
|
* [`patternLinear3d`](kcl/patternLinear3d)
|
||||||
|
* [`patternTransform`](kcl/patternTransform)
|
||||||
* [`pi`](kcl/pi)
|
* [`pi`](kcl/pi)
|
||||||
* [`pow`](kcl/pow)
|
* [`pow`](kcl/pow)
|
||||||
* [`profileStart`](kcl/profileStart)
|
* [`profileStart`](kcl/profileStart)
|
||||||
|
356
docs/kcl/patternTransform.md
Normal file
356
docs/kcl/patternTransform.md
Normal file
File diff suppressed because one or more lines are too long
4230
docs/kcl/std.json
4230
docs/kcl/std.json
File diff suppressed because it is too large
Load Diff
@ -110,6 +110,8 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send the modeling cmd and wait for the response.
|
/// Send the modeling cmd and wait for the response.
|
||||||
|
// TODO: This should only borrow `cmd`.
|
||||||
|
// See https://github.com/KittyCAD/modeling-app/issues/2821
|
||||||
async fn send_modeling_cmd(
|
async fn send_modeling_cmd(
|
||||||
&self,
|
&self,
|
||||||
id: uuid::Uuid,
|
id: uuid::Uuid,
|
||||||
|
@ -16,7 +16,7 @@ use crate::{
|
|||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
fs::FileManager,
|
fs::FileManager,
|
||||||
settings::types::UnitLength,
|
settings::types::UnitLength,
|
||||||
std::{FunctionKind, StdLib},
|
std::{FnAsArg, FunctionKind, StdLib},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
@ -640,6 +640,52 @@ impl MemoryItem {
|
|||||||
.map(Some)
|
.map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_user_val(&self) -> Option<&UserVal> {
|
||||||
|
if let MemoryItem::UserVal(x) = self {
|
||||||
|
Some(x)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 value is of type function, return it.
|
||||||
|
pub fn get_function(&self, source_ranges: Vec<SourceRange>) -> Result<FnAsArg<'_>, KclError> {
|
||||||
|
let MemoryItem::Function {
|
||||||
|
func,
|
||||||
|
expression,
|
||||||
|
meta: _,
|
||||||
|
} = &self
|
||||||
|
else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "not an in-memory function".to_string(),
|
||||||
|
source_ranges,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
let func = func.as_ref().ok_or_else(|| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("Not an in-memory function: {:?}", expression),
|
||||||
|
source_ranges,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
Ok(FnAsArg {
|
||||||
|
func,
|
||||||
|
expr: expression.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Backwards compatibility for getting a tag from a memory item.
|
/// Backwards compatibility for getting a tag from a memory item.
|
||||||
pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
|
pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
|
||||||
match self {
|
match self {
|
||||||
|
45
src/wasm-lib/kcl/src/function_param.rs
Normal file
45
src/wasm-lib/kcl/src/function_param.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ast::types::FunctionExpression,
|
||||||
|
errors::KclError,
|
||||||
|
executor::{ExecutorContext, MemoryFunction, MemoryItem, Metadata, ProgramMemory, ProgramReturn},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A function being used as a parameter into a stdlib function.
|
||||||
|
pub struct FunctionParam<'a> {
|
||||||
|
pub inner: &'a MemoryFunction,
|
||||||
|
pub memory: ProgramMemory,
|
||||||
|
pub fn_expr: Box<FunctionExpression>,
|
||||||
|
pub meta: Vec<Metadata>,
|
||||||
|
pub ctx: ExecutorContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FunctionParam<'a> {
|
||||||
|
pub async fn call(
|
||||||
|
&self,
|
||||||
|
args: Vec<MemoryItem>,
|
||||||
|
) -> Result<(Option<ProgramReturn>, HashMap<String, MemoryItem>), KclError> {
|
||||||
|
(self.inner)(
|
||||||
|
args,
|
||||||
|
self.memory.clone(),
|
||||||
|
self.fn_expr.clone(),
|
||||||
|
self.meta.clone(),
|
||||||
|
self.ctx.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> JsonSchema for FunctionParam<'a> {
|
||||||
|
fn schema_name() -> String {
|
||||||
|
"FunctionParam".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||||
|
// TODO: Actually generate a reasonable schema.
|
||||||
|
gen.subschema_for::<()>()
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ pub mod engine;
|
|||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
mod function_param;
|
||||||
pub mod lint;
|
pub mod lint;
|
||||||
pub mod lsp;
|
pub mod lsp;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
@ -28,7 +28,7 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::types::{parse_json_number_as_f64, TagDeclarator},
|
ast::types::{parse_json_number_as_f64, FunctionExpression, TagDeclarator},
|
||||||
docs::StdLibFn,
|
docs::StdLibFn,
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::{
|
executor::{
|
||||||
@ -85,6 +85,7 @@ lazy_static! {
|
|||||||
Box::new(crate::std::patterns::PatternLinear3D),
|
Box::new(crate::std::patterns::PatternLinear3D),
|
||||||
Box::new(crate::std::patterns::PatternCircular2D),
|
Box::new(crate::std::patterns::PatternCircular2D),
|
||||||
Box::new(crate::std::patterns::PatternCircular3D),
|
Box::new(crate::std::patterns::PatternCircular3D),
|
||||||
|
Box::new(crate::std::patterns::PatternTransform),
|
||||||
Box::new(crate::std::chamfer::Chamfer),
|
Box::new(crate::std::chamfer::Chamfer),
|
||||||
Box::new(crate::std::fillet::Fillet),
|
Box::new(crate::std::fillet::Fillet),
|
||||||
Box::new(crate::std::fillet::GetOppositeEdge),
|
Box::new(crate::std::fillet::GetOppositeEdge),
|
||||||
@ -351,6 +352,39 @@ impl Args {
|
|||||||
Ok(numbers)
|
Ok(numbers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_pattern_transform_args(&self) -> Result<(u32, FnAsArg<'_>, ExtrudeGroupSet), 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 func = transform.get_function(sr.clone())?;
|
||||||
|
let eg = 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 eg = eg.get_extrude_group_set().map_err(|_e| {
|
||||||
|
KclError::Type(KclErrorDetails {
|
||||||
|
message: "Third argument was not an ExtrudeGroup".to_owned(),
|
||||||
|
source_ranges: sr.clone(),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
Ok((num_repetitions, func, eg))
|
||||||
|
}
|
||||||
|
|
||||||
fn get_hypotenuse_leg(&self) -> Result<(f64, f64), KclError> {
|
fn get_hypotenuse_leg(&self) -> Result<(f64, f64), KclError> {
|
||||||
let numbers = self.get_number_array()?;
|
let numbers = self.get_number_array()?;
|
||||||
|
|
||||||
@ -1242,6 +1276,11 @@ pub enum Primitive {
|
|||||||
Uuid,
|
Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FnAsArg<'a> {
|
||||||
|
pub func: &'a crate::executor::MemoryFunction,
|
||||||
|
pub expr: Box<FunctionExpression>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
@ -8,7 +8,11 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::{ExtrudeGroup, ExtrudeGroupSet, Geometries, Geometry, MemoryItem, SketchGroup, SketchGroupSet},
|
executor::{
|
||||||
|
ExtrudeGroup, ExtrudeGroupSet, Geometries, Geometry, MemoryItem, Point3d, ProgramReturn, SketchGroup,
|
||||||
|
SketchGroupSet, SourceRange, UserVal,
|
||||||
|
},
|
||||||
|
function_param::FunctionParam,
|
||||||
std::{types::Uint, Args},
|
std::{types::Uint, Args},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -70,6 +74,233 @@ 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.
|
||||||
|
pub async fn pattern_transform(args: Args) -> Result<MemoryItem, KclError> {
|
||||||
|
let (num_repetitions, transform, extr) = args.get_pattern_transform_args()?;
|
||||||
|
|
||||||
|
let extrude_groups = inner_pattern_transform(
|
||||||
|
num_repetitions,
|
||||||
|
FunctionParam {
|
||||||
|
inner: transform.func,
|
||||||
|
fn_expr: transform.expr,
|
||||||
|
meta: vec![args.source_range.into()],
|
||||||
|
ctx: args.ctx.clone(),
|
||||||
|
memory: args.current_program_memory.clone(),
|
||||||
|
},
|
||||||
|
extr,
|
||||||
|
&args,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(MemoryItem::ExtrudeGroups { value: extrude_groups })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A linear pattern on a 3D solid.
|
||||||
|
/// Each repetition of the pattern can be transformed (e.g. scaled, translated, hidden, etc).
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// // 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) => {
|
||||||
|
/// let scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8))
|
||||||
|
/// return {
|
||||||
|
/// translate: [0, 0, replicaId * 10],
|
||||||
|
/// scale: [scale, scale, 0],
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// // Each layer is just a pretty thin cylinder.
|
||||||
|
/// fn layer = () => {
|
||||||
|
/// return startSketchOn("XY") // or some other plane idk
|
||||||
|
/// |> circle([0, 0], 1, %, 'tag1')
|
||||||
|
/// |> extrude(h, %)
|
||||||
|
/// }
|
||||||
|
/// // The vase is 100 layers tall.
|
||||||
|
/// // The 100 layers are replica of each other, with a slight transformation applied to each.
|
||||||
|
/// let vase = layer() |> patternTransform(100, transform, %)
|
||||||
|
/// ```
|
||||||
|
#[stdlib {
|
||||||
|
name = "patternTransform",
|
||||||
|
}]
|
||||||
|
async fn inner_pattern_transform<'a>(
|
||||||
|
num_repetitions: u32,
|
||||||
|
transform_function: FunctionParam<'a>,
|
||||||
|
extrude_group_set: ExtrudeGroupSet,
|
||||||
|
args: &'a Args,
|
||||||
|
) -> Result<Vec<Box<ExtrudeGroup>>, KclError> {
|
||||||
|
// Build the vec of transforms, one for each repetition.
|
||||||
|
let mut transform = Vec::new();
|
||||||
|
for i in 0..num_repetitions {
|
||||||
|
let t = make_transform(i, &transform_function, args.source_range).await?;
|
||||||
|
transform.push(t);
|
||||||
|
}
|
||||||
|
// 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 extrude groups.
|
||||||
|
args.flush_batch_for_extrude_group_set(extrude_group_set.clone().into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let starting_extrude_groups: Vec<Box<ExtrudeGroup>> = extrude_group_set.into();
|
||||||
|
|
||||||
|
if args.ctx.is_mock {
|
||||||
|
return Ok(starting_extrude_groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut extrude_groups = Vec::new();
|
||||||
|
for e in starting_extrude_groups {
|
||||||
|
let new_extrude_groups = send_pattern_transform(transform.clone(), &e, args).await?;
|
||||||
|
extrude_groups.extend(new_extrude_groups);
|
||||||
|
}
|
||||||
|
Ok(extrude_groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_pattern_transform(
|
||||||
|
// This should be passed via reference, see
|
||||||
|
// https://github.com/KittyCAD/modeling-app/issues/2821
|
||||||
|
transform: Vec<kittycad::types::LinearTransform>,
|
||||||
|
extrude_group: &ExtrudeGroup,
|
||||||
|
args: &Args,
|
||||||
|
) -> Result<Vec<Box<ExtrudeGroup>>, KclError> {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let resp = args
|
||||||
|
.send_modeling_cmd(
|
||||||
|
id,
|
||||||
|
ModelingCmd::EntityLinearPatternTransform {
|
||||||
|
entity_id: extrude_group.id,
|
||||||
|
transform,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let kittycad::types::OkWebSocketResponseData::Modeling {
|
||||||
|
modeling_response: kittycad::types::OkModelingCmdResponse::EntityLinearPatternTransform { data: pattern_info },
|
||||||
|
} = &resp
|
||||||
|
else {
|
||||||
|
return Err(KclError::Engine(KclErrorDetails {
|
||||||
|
message: format!("EntityLinearPattern response was not as expected: {:?}", resp),
|
||||||
|
source_ranges: vec![args.source_range],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut geometries = vec![Box::new(extrude_group.clone())];
|
||||||
|
for id in pattern_info.entity_ids.iter() {
|
||||||
|
let mut new_extrude_group = extrude_group.clone();
|
||||||
|
new_extrude_group.id = *id;
|
||||||
|
geometries.push(Box::new(new_extrude_group));
|
||||||
|
}
|
||||||
|
Ok(geometries)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn make_transform<'a>(
|
||||||
|
i: u32,
|
||||||
|
transform_function: &FunctionParam<'a>,
|
||||||
|
source_range: SourceRange,
|
||||||
|
) -> Result<kittycad::types::LinearTransform, KclError> {
|
||||||
|
// Call the transform fn for this repetition.
|
||||||
|
let repetition_num = MemoryItem::UserVal(UserVal {
|
||||||
|
value: serde_json::Value::Number(i.into()),
|
||||||
|
meta: vec![source_range.into()],
|
||||||
|
});
|
||||||
|
let transform_fn_args = vec![repetition_num];
|
||||||
|
let transform_fn_return = transform_function.call(transform_fn_args).await?.0;
|
||||||
|
|
||||||
|
// Unpack the returned transform object.
|
||||||
|
let source_ranges = vec![source_range];
|
||||||
|
let transform_fn_return = transform_fn_return.ok_or_else(|| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "Transform function must return a value".to_string(),
|
||||||
|
source_ranges: source_ranges.clone(),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let ProgramReturn::Value(transform_fn_return) = transform_fn_return else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "Transform function must return a value".to_string(),
|
||||||
|
source_ranges: source_ranges.clone(),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
let MemoryItem::UserVal(transform) = transform_fn_return else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "Transform function must return a transform object".to_string(),
|
||||||
|
source_ranges: source_ranges.clone(),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply defaults to the transform.
|
||||||
|
let replicate = match transform.value.get("replicate") {
|
||||||
|
Some(serde_json::Value::Bool(true)) => true,
|
||||||
|
Some(serde_json::Value::Bool(false)) => false,
|
||||||
|
Some(_) => {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "The 'replicate' key must be a bool".to_string(),
|
||||||
|
source_ranges: source_ranges.clone(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
let scale = match transform.value.get("scale") {
|
||||||
|
Some(x) => array_to_point3d(x, source_ranges.clone())?,
|
||||||
|
None => Point3d { x: 1.0, y: 1.0, z: 1.0 },
|
||||||
|
};
|
||||||
|
let translate = match transform.value.get("translate") {
|
||||||
|
Some(x) => array_to_point3d(x, source_ranges.clone())?,
|
||||||
|
None => Point3d { x: 0.0, y: 0.0, z: 0.0 },
|
||||||
|
};
|
||||||
|
let t = kittycad::types::LinearTransform {
|
||||||
|
replicate,
|
||||||
|
scale: Some(scale.into()),
|
||||||
|
translate: Some(translate.into()),
|
||||||
|
};
|
||||||
|
Ok(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn array_to_point3d(json: &serde_json::Value, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
|
||||||
|
let serde_json::Value::Array(arr) = dbg!(json) else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "Expected an array of 3 numbers (i.e. a 3D point)".to_string(),
|
||||||
|
source_ranges,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
let len = arr.len();
|
||||||
|
if len != 3 {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("Expected an array of 3 numbers (i.e. a 3D point) but found {len} items"),
|
||||||
|
source_ranges,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
// Gets an f64 from a JSON value, returns Option.
|
||||||
|
let f = |j: &serde_json::Value| j.as_number().and_then(|num| num.as_f64()).map(|x| x.to_owned());
|
||||||
|
let err = |component| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("{component} component of this point was not a number"),
|
||||||
|
source_ranges: source_ranges.clone(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let x = f(&arr[0]).ok_or_else(|| err("X"))?;
|
||||||
|
let y = f(&arr[1]).ok_or_else(|| err("Y"))?;
|
||||||
|
let z = f(&arr[2]).ok_or_else(|| err("Z"))?;
|
||||||
|
Ok(Point3d { x, y, z })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_array_to_point3d() {
|
||||||
|
let input = serde_json::json! {
|
||||||
|
[1.1, 2.2, 3.3]
|
||||||
|
};
|
||||||
|
let expected = Point3d { x: 1.1, y: 2.2, z: 3.3 };
|
||||||
|
let actual = array_to_point3d(&input, Vec::new());
|
||||||
|
assert_eq!(actual.unwrap(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A linear pattern on a 2D sketch.
|
/// A linear pattern on a 2D sketch.
|
||||||
pub async fn pattern_linear_2d(args: Args) -> Result<MemoryItem, KclError> {
|
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()?;
|
let (data, sketch_group_set): (LinearPattern2dData, SketchGroupSet) = args.get_data_and_sketch_group_set()?;
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 333 KiB |
26
src/wasm-lib/tests/executor/inputs/pattern_vase.kcl
Normal file
26
src/wasm-lib/tests/executor/inputs/pattern_vase.kcl
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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) => {
|
||||||
|
let scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8))
|
||||||
|
return {
|
||||||
|
translate: [0, 0, replicaId * 10],
|
||||||
|
scale: [scale, scale, 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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() |> patternTransform(100, transform, %)
|
@ -2464,3 +2464,10 @@ async fn serial_test_global_tags() {
|
|||||||
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
||||||
twenty_twenty::assert_image("tests/executor/outputs/global_tags.png", &result, 0.999);
|
twenty_twenty::assert_image("tests/executor/outputs/global_tags.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);
|
||||||
|
}
|
||||||
|
BIN
src/wasm-lib/tests/executor/outputs/pattern_vase.png
Normal file
BIN
src/wasm-lib/tests/executor/outputs/pattern_vase.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 333 KiB |
Reference in New Issue
Block a user