KCL stdlib and circle function (#1029)

Allows stdlib functions to be written as KCL, not as Rust. 

Rust stdlib functions will hereafter be referred to as "core" not "std".

Right now the only stdlib function I implemented is a circle function (it's a wrapper around the core arc function which sets the arc's start/end to 0 and 360 respectively). I know I want to change this function as soon as KCL has enums, which is my next task. So, I don't want users to start using this right away. To that end, I've named this function "unstable_stdlib_circle" not "circle". Once the function is ready to be stabilized, I can rename it to just "circle".

Note that this PR modifies the existing "sketch and extrude a cylinder" KCL test so that instead of using a user-defined circle function, it uses the unstable_stdlib_circle function now. And the twenty-twenty tests pass, so we know my stdlib is working.

https://github.com/KittyCAD/modeling-app/issues/922
This commit is contained in:
Adam Chalmers
2023-11-09 09:58:20 -06:00
committed by GitHub
parent 0db5db2181
commit b925ed9b65
9 changed files with 356 additions and 413 deletions

View File

@ -6,10 +6,10 @@
serial-integration = { max-threads = 4 }
[profile.default]
slow-timeout = { period = "60s", terminate-after = 1 }
slow-timeout = { period = "10s", terminate-after = 1 }
[profile.ci]
slow-timeout = { period = "120s", terminate-after = 10 }
slow-timeout = { period = "30s", terminate-after = 5 }
[[profile.default.overrides]]
filter = "test(serial_test_)"
@ -20,3 +20,7 @@ threads-required = 4
filter = "test(serial_test_)"
test-group = "serial-integration"
threads-required = 4
[[profile.default.overrides]]
filter = "test(parser::parser_impl::snapshot_tests)"
slow-timeout = { period = "1s", terminate-after = 5 }

View File

@ -11,9 +11,11 @@ use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, DocumentSymbol, R
pub use self::literal_value::LiteralValue;
use crate::{
docs::StdLibFn,
errors::{KclError, KclErrorDetails},
executor::{ExecutorContext, MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange, UserVal},
executor::{BodyType, ExecutorContext, MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange, UserVal},
parser::PIPE_OPERATOR,
std::{kcl_stdlib::KclStdLibFn, FunctionKind},
};
mod literal_value;
@ -960,8 +962,8 @@ impl CallExpression {
fn_args.push(result);
}
match ctx.stdlib.get(&self.callee.name) {
Some(func) => {
match ctx.stdlib.get_either(&self.callee.name) {
FunctionKind::Core(func) => {
// Attempt to call the function.
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
let result = func.std_lib_fn()(args).await?;
@ -973,15 +975,54 @@ impl CallExpression {
Ok(result)
}
}
// Must be user-defined then
None => {
FunctionKind::Std(func) => {
let function_expression = func.function();
if fn_args.len() != function_expression.params.len() {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Expected {} arguments, got {}",
function_expression.params.len(),
fn_args.len(),
),
source_ranges: vec![(function_expression).into()],
}));
}
// Add the arguments to the memory.
let mut fn_memory = memory.clone();
for (index, param) in function_expression.params.iter().enumerate() {
fn_memory.add(&param.name, fn_args.get(index).unwrap().clone(), param.into())?;
}
// Call the stdlib function
let p = func.function().clone().body;
let results = crate::executor::execute(p, &mut fn_memory, BodyType::Block, ctx).await?;
let out = results.return_;
let result = out.ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of stdlib function {} is undefined", fn_name),
source_ranges: vec![self.into()],
})
})?;
let result = result.get_value()?;
if pipe_info.is_in_pipe {
pipe_info.index += 1;
pipe_info.previous_results.push(result);
execute_pipe_body(memory, &pipe_info.body.clone(), pipe_info, self.into(), ctx).await
} else {
Ok(result)
}
}
FunctionKind::UserDefined => {
let func = memory.get(&fn_name, self.into())?;
let result = func
.call_fn(fn_args, memory.clone(), ctx.clone())
.await?
.ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of function {} is undefined", fn_name),
message: format!("Result of user-defined function {} is undefined", fn_name),
source_ranges: vec![self.into()],
})
})?;
@ -1056,10 +1097,15 @@ impl CallExpression {
#[ts(export)]
#[serde(tag = "type")]
pub enum Function {
/// A stdlib function.
/// A stdlib function written in Rust (aka core lib).
StdLib {
/// The function.
func: Box<dyn crate::docs::StdLibFn>,
func: Box<dyn StdLibFn>,
},
/// A stdlib function written in KCL.
StdLibKcl {
/// The function.
func: Box<dyn KclStdLibFn>,
},
/// A function that is defined in memory.
#[default]

View File

@ -3,6 +3,7 @@
use std::{collections::HashMap, sync::Arc};
use anyhow::Result;
use async_recursion::async_recursion;
use kittycad::types::{Color, ModelingCmd, Point3D};
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
@ -13,7 +14,7 @@ use crate::{
ast::types::{BodyItem, FunctionExpression, Value},
engine::{EngineConnection, EngineManager},
errors::{KclError, KclErrorDetails},
std::StdLib,
std::{FunctionKind, StdLib},
};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -781,6 +782,7 @@ pub struct ExecutorContext {
}
/// Execute a AST's program.
#[async_recursion(?Send)]
pub async fn execute(
program: crate::ast::types::Program,
memory: &mut ProgramMemory,
@ -828,27 +830,37 @@ pub async fn execute(
}
}
let _show_fn = Box::new(crate::std::Show);
if let Some(func) = ctx.stdlib.get(&call_expr.callee.name) {
use crate::docs::StdLibFn;
if func.name() == _show_fn.name() {
if options != BodyType::Root {
match ctx.stdlib.get_either(&call_expr.callee.name) {
FunctionKind::Core(func) => {
use crate::docs::StdLibFn;
if func.name() == _show_fn.name() {
if options != BodyType::Root {
return Err(KclError::Semantic(KclErrorDetails {
message: "Cannot call show outside of a root".to_string(),
source_ranges: vec![call_expr.into()],
}));
}
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
}
}
FunctionKind::Std(func) => {
let mut newmem = memory.clone();
let result = execute(func.program().to_owned(), &mut newmem, BodyType::Block, ctx).await?;
memory.return_ = result.return_;
}
FunctionKind::UserDefined => {
if let Some(func) = memory.clone().root.get(&fn_name) {
let result = func.call_fn(args.clone(), memory.clone(), ctx.clone()).await?;
memory.return_ = result;
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Cannot call show outside of a root".to_string(),
message: format!("No such name {} defined", fn_name),
source_ranges: vec![call_expr.into()],
}));
}
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
}
} else if let Some(func) = memory.clone().root.get(&fn_name) {
let result = func.call_fn(args.clone(), memory.clone(), ctx.clone()).await?;
memory.return_ = result;
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("No such name {} defined", fn_name),
source_ranges: vec![call_expr.into()],
}));
}
}
}

View File

@ -1388,7 +1388,7 @@ const mySk1 = startSketchAt([0, 0])"#;
let Value::PipeExpression(pipe) = val else {
panic!("expected pipe");
};
let mut noncode = dbg!(pipe.non_code_meta);
let mut noncode = pipe.non_code_meta;
assert_eq!(noncode.non_code_nodes.len(), 1);
let comment = noncode.non_code_nodes.remove(&0).unwrap().pop().unwrap();
assert_eq!(
@ -2575,8 +2575,7 @@ thing(false)
let tokens = crate::token::lexer(test_program);
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
let e = result.unwrap_err();
eprintln!("{e:?}")
let _e = result.unwrap_err();
}
#[test]

View File

@ -4,385 +4,69 @@ expression: actual
---
{
"start": 0,
"end": 330,
"end": 90,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 254,
"end": 74,
"declarations": [
{
"type": "VariableDeclarator",
"start": 3,
"end": 254,
"start": 6,
"end": 74,
"id": {
"type": "Identifier",
"start": 3,
"end": 9,
"name": "circle"
},
"init": {
"type": "FunctionExpression",
"type": "FunctionExpression",
"start": 12,
"end": 254,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 18,
"name": "plane"
},
{
"type": "Identifier",
"start": 20,
"end": 26,
"name": "center"
},
{
"type": "Identifier",
"start": 28,
"end": 34,
"name": "radius"
}
],
"body": {
"start": 39,
"end": 254,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 43,
"end": 240,
"declarations": [
{
"type": "VariableDeclarator",
"start": 49,
"end": 240,
"id": {
"type": "Identifier",
"start": 49,
"end": 51,
"name": "sg"
},
"init": {
"type": "PipeExpression",
"type": "PipeExpression",
"start": 54,
"end": 240,
"body": [
{
"type": "CallExpression",
"type": "CallExpression",
"start": 54,
"end": 74,
"callee": {
"type": "Identifier",
"start": 54,
"end": 67,
"name": "startSketchOn"
},
"arguments": [
{
"type": "Identifier",
"type": "Identifier",
"start": 68,
"end": 73,
"name": "plane"
}
],
"optional": false
},
{
"type": "CallExpression",
"type": "CallExpression",
"start": 82,
"end": 132,
"callee": {
"type": "Identifier",
"start": 82,
"end": 96,
"name": "startProfileAt"
},
"arguments": [
{
"type": "ArrayExpression",
"type": "ArrayExpression",
"start": 97,
"end": 128,
"elements": [
{
"type": "BinaryExpression",
"type": "BinaryExpression",
"start": 98,
"end": 116,
"operator": "+",
"left": {
"type": "MemberExpression",
"type": "MemberExpression",
"start": 98,
"end": 107,
"object": {
"type": "Identifier",
"type": "Identifier",
"start": 98,
"end": 104,
"name": "center"
},
"property": {
"type": "Literal",
"type": "Literal",
"start": 105,
"end": 106,
"value": 0,
"raw": "0"
},
"computed": false
},
"right": {
"type": "Identifier",
"type": "Identifier",
"start": 110,
"end": 116,
"name": "radius"
}
},
{
"type": "MemberExpression",
"type": "MemberExpression",
"start": 118,
"end": 127,
"object": {
"type": "Identifier",
"type": "Identifier",
"start": 118,
"end": 124,
"name": "center"
},
"property": {
"type": "Literal",
"type": "Literal",
"start": 125,
"end": 126,
"value": 1,
"raw": "1"
},
"computed": false
}
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 130,
"end": 131
}
],
"optional": false
},
{
"type": "CallExpression",
"type": "CallExpression",
"start": 140,
"end": 224,
"callee": {
"type": "Identifier",
"start": 140,
"end": 143,
"name": "arc"
},
"arguments": [
{
"type": "ObjectExpression",
"type": "ObjectExpression",
"start": 144,
"end": 220,
"properties": [
{
"type": "ObjectProperty",
"start": 153,
"end": 167,
"key": {
"type": "Identifier",
"start": 153,
"end": 162,
"name": "angle_end"
},
"value": {
"type": "Literal",
"type": "Literal",
"start": 164,
"end": 167,
"value": 360,
"raw": "360"
}
},
{
"type": "ObjectProperty",
"start": 176,
"end": 190,
"key": {
"type": "Identifier",
"start": 176,
"end": 187,
"name": "angle_start"
},
"value": {
"type": "Literal",
"type": "Literal",
"start": 189,
"end": 190,
"value": 0,
"raw": "0"
}
},
{
"type": "ObjectProperty",
"start": 199,
"end": 213,
"key": {
"type": "Identifier",
"start": 199,
"end": 205,
"name": "radius"
},
"value": {
"type": "Identifier",
"type": "Identifier",
"start": 207,
"end": 213,
"name": "radius"
}
}
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 222,
"end": 223
}
],
"optional": false
},
{
"type": "CallExpression",
"type": "CallExpression",
"start": 232,
"end": 240,
"callee": {
"type": "Identifier",
"start": 232,
"end": 237,
"name": "close"
},
"arguments": [
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 238,
"end": 239
}
],
"optional": false
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": []
}
}
}
],
"kind": "const"
},
{
"type": "ReturnStatement",
"type": "ReturnStatement",
"start": 243,
"end": 252,
"argument": {
"type": "Identifier",
"type": "Identifier",
"start": 250,
"end": 252,
"name": "sg"
}
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": []
}
}
}
}
],
"kind": "fn"
},
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 256,
"end": 314,
"declarations": [
{
"type": "VariableDeclarator",
"start": 262,
"end": 314,
"id": {
"type": "Identifier",
"start": 262,
"end": 270,
"start": 6,
"end": 14,
"name": "cylinder"
},
"init": {
"type": "PipeExpression",
"type": "PipeExpression",
"start": 273,
"end": 314,
"start": 17,
"end": 74,
"body": [
{
"type": "CallExpression",
"type": "CallExpression",
"start": 273,
"end": 296,
"start": 17,
"end": 56,
"callee": {
"type": "Identifier",
"start": 273,
"end": 279,
"name": "circle"
"start": 17,
"end": 39,
"name": "unstable_stdlib_circle"
},
"arguments": [
{
"type": "Literal",
"type": "Literal",
"start": 280,
"end": 284,
"start": 40,
"end": 44,
"value": "XY",
"raw": "'XY'"
},
{
"type": "ArrayExpression",
"type": "ArrayExpression",
"start": 286,
"end": 291,
"start": 46,
"end": 51,
"elements": [
{
"type": "Literal",
"type": "Literal",
"start": 287,
"end": 288,
"start": 47,
"end": 48,
"value": 0,
"raw": "0"
},
{
"type": "Literal",
"type": "Literal",
"start": 289,
"end": 290,
"start": 49,
"end": 50,
"value": 0,
"raw": "0"
}
@ -391,8 +75,8 @@ expression: actual
{
"type": "Literal",
"type": "Literal",
"start": 293,
"end": 295,
"start": 53,
"end": 55,
"value": 22,
"raw": "22"
}
@ -402,28 +86,28 @@ expression: actual
{
"type": "CallExpression",
"type": "CallExpression",
"start": 300,
"end": 314,
"start": 60,
"end": 74,
"callee": {
"type": "Identifier",
"start": 300,
"end": 307,
"start": 60,
"end": 67,
"name": "extrude"
},
"arguments": [
{
"type": "Literal",
"type": "Literal",
"start": 308,
"end": 310,
"start": 68,
"end": 70,
"value": 14,
"raw": "14"
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 312,
"end": 313
"start": 72,
"end": 73
}
],
"optional": false
@ -441,25 +125,25 @@ expression: actual
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 315,
"end": 329,
"start": 75,
"end": 89,
"expression": {
"type": "CallExpression",
"type": "CallExpression",
"start": 315,
"end": 329,
"start": 75,
"end": 89,
"callee": {
"type": "Identifier",
"start": 315,
"end": 319,
"start": 75,
"end": 79,
"name": "show"
},
"arguments": [
{
"type": "Identifier",
"type": "Identifier",
"start": 320,
"end": 328,
"start": 80,
"end": 88,
"name": "cylinder"
}
],
@ -468,18 +152,7 @@ expression: actual
}
],
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"type": "NonCodeNode",
"start": 254,
"end": 256,
"value": {
"type": "newLine"
}
}
]
},
"nonCodeNodes": {},
"start": []
}
}

View File

@ -0,0 +1,82 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
ast::types::{BodyItem, FunctionExpression, Program, Value},
docs::{StdLibFn, StdLibFnData},
token::lexer,
};
pub trait KclStdLibFn: StdLibFn {
fn kcl_clone_box(&self) -> Box<dyn KclStdLibFn>;
fn function(&self) -> &FunctionExpression;
fn program(&self) -> &Program;
}
impl ts_rs::TS for dyn KclStdLibFn {
const EXPORT_TO: Option<&'static str> = Some("bindings/StdLibFnData");
fn name() -> String {
"StdLibFnData".to_string()
}
fn dependencies() -> Vec<ts_rs::Dependency>
where
Self: 'static,
{
StdLibFnData::dependencies()
}
fn transparent() -> bool {
StdLibFnData::transparent()
}
}
impl Clone for Box<dyn KclStdLibFn> {
fn clone(&self) -> Box<dyn KclStdLibFn> {
self.kcl_clone_box()
}
}
impl JsonSchema for dyn KclStdLibFn {
fn schema_name() -> String {
"KclStdLibFn".to_string()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
gen.subschema_for::<StdLibFnData>()
}
}
impl<'de> Deserialize<'de> for Box<dyn KclStdLibFn> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let data = StdLibFnData::deserialize(deserializer)?;
let stdlib = crate::std::StdLib::new();
let stdlib_fn = stdlib
.get_kcl(&data.name)
.ok_or_else(|| serde::de::Error::custom(format!("StdLibFn {} not found", data.name)))?;
Ok(stdlib_fn)
}
}
impl Serialize for Box<dyn KclStdLibFn> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.to_json().unwrap().serialize(serializer)
}
}
/// Parse a KCL program. Expect it to have a single body item, which is a function.
/// Return the program and its single function.
/// Return None if those expectations aren't met.
pub fn extract_function(source: &str) -> Option<(Program, Box<FunctionExpression>)> {
let tokens = lexer(source);
let src = crate::parser::Parser::new(tokens).ast().ok()?;
assert_eq!(src.body.len(), 1);
let BodyItem::ExpressionStatement(expr) = src.body.last()? else {
panic!("expected expression statement");
};
let Value::FunctionExpression(function) = expr.expression.clone() else {
panic!("expected function expr");
};
Some((src, function))
}

View File

@ -1,8 +1,10 @@
//! Functions implemented for language execution.
pub mod extrude;
pub mod kcl_stdlib;
pub mod math;
pub mod segment;
pub mod shapes;
pub mod sketch;
pub mod utils;
@ -16,6 +18,7 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use self::kcl_stdlib::KclStdLibFn;
use crate::{
ast::types::parse_json_number_as_f64,
docs::StdLibFn,
@ -92,12 +95,16 @@ pub fn name_in_stdlib(name: &str) -> bool {
}
pub struct StdLib {
pub fns: HashMap<String, Box<(dyn StdLibFn)>>,
pub fns: HashMap<String, Box<dyn StdLibFn>>,
pub kcl_fns: HashMap<String, Box<dyn KclStdLibFn>>,
}
impl std::fmt::Debug for StdLib {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StdLib").field("fns.len()", &self.fns.len()).finish()
f.debug_struct("StdLib")
.field("fns.len()", &self.fns.len())
.field("kcl_fns.len()", &self.kcl_fns.len())
.finish()
}
}
@ -109,12 +116,36 @@ impl StdLib {
.map(|internal_fn| (internal_fn.name(), internal_fn))
.collect();
Self { fns }
let kcl_internal_fns: [Box<dyn KclStdLibFn>; 1] = [Box::<shapes::Circle>::default()];
let kcl_fns = kcl_internal_fns
.into_iter()
.map(|internal_fn| (internal_fn.name(), internal_fn))
.collect();
Self { fns, kcl_fns }
}
pub fn get(&self, name: &str) -> Option<Box<dyn StdLibFn>> {
self.fns.get(name).cloned()
}
pub fn get_kcl(&self, name: &str) -> Option<Box<dyn KclStdLibFn>> {
self.kcl_fns.get(name).cloned()
}
pub fn get_either(&self, name: &str) -> FunctionKind {
if let Some(f) = self.get(name) {
FunctionKind::Core(f)
} else if let Some(f) = self.get_kcl(name) {
FunctionKind::Std(f)
} else {
FunctionKind::UserDefined
}
}
pub fn contains_key(&self, key: &str) -> bool {
self.fns.contains_key(key) || self.kcl_fns.contains_key(key)
}
}
impl Default for StdLib {
@ -123,6 +154,12 @@ impl Default for StdLib {
}
}
pub enum FunctionKind {
Core(Box<dyn StdLibFn>),
Std(Box<dyn KclStdLibFn>),
UserDefined,
}
#[derive(Debug, Clone)]
pub struct Args {
pub args: Vec<MemoryItem>,

View File

@ -0,0 +1,102 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::kcl_stdlib::KclStdLibFn;
use crate::{
ast::types::{FunctionExpression, Program},
docs::StdLibFn,
};
pub const CIRCLE_FN: &str = r#"
(plane, center, radius) => {
const sg = startSketchOn(plane)
|> startProfileAt([center[0] + radius, center[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
"#;
#[derive(Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
pub struct Circle {
function: FunctionExpression,
program: Program,
}
impl Default for Circle {
fn default() -> Self {
// TODO in https://github.com/KittyCAD/modeling-app/issues/1018
// Don't unwrap here, parse it at compile-time.
let (src, function) = super::kcl_stdlib::extract_function(CIRCLE_FN).unwrap();
Self {
function: *function,
program: src,
}
}
}
impl std::fmt::Debug for Circle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
"circle".fmt(f)
}
}
/// TODO: Parse the KCL in a macro and generate these
impl StdLibFn for Circle {
fn name(&self) -> String {
"unstable_stdlib_circle".to_owned()
}
fn summary(&self) -> String {
"Sketch a circle on the given plane".to_owned()
}
fn description(&self) -> String {
String::new()
}
fn tags(&self) -> Vec<String> {
Vec::new()
}
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
Vec::new() // TODO
}
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
None
}
fn unpublished(&self) -> bool {
true
}
fn deprecated(&self) -> bool {
false
}
fn std_lib_fn(&self) -> crate::std::StdFn {
todo!()
}
fn clone_box(&self) -> Box<dyn StdLibFn> {
Box::new(self.to_owned())
}
}
impl KclStdLibFn for Circle {
fn function(&self) -> &FunctionExpression {
&self.function
}
fn program(&self) -> &Program {
&self.program
}
fn kcl_clone_box(&self) -> Box<dyn KclStdLibFn> {
Box::new(self.clone())
}
}

View File

@ -1,14 +1,2 @@
fn circle = (plane, center, radius) => {
const sg = startSketchOn(plane)
|> startProfileAt([center[0] + radius, center[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
const cylinder = circle('XY', [0,0], 22) |> extrude(14, %)
const cylinder = unstable_stdlib_circle('XY', [0,0], 22) |> extrude(14, %)
show(cylinder)