Add support for line, xLine, yLine, xLineTo, yLineTo (#1754)

* Add support for line, xLine, yLine, xLineTo, yLineTo

* Fix minor memory misalignment

* Address PR comments
This commit is contained in:
49fl
2024-03-19 12:11:45 -04:00
committed by GitHub
parent cefa6f85fe
commit be3fed8427
11 changed files with 995 additions and 633 deletions

1082
src/wasm-lib/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ description = "A new executor for KCL which compiles to Execution Plans"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
image = { version = "0.24.7", default-features = false, features = ["png"] }
kcl-lib = { path = "../kcl" }
kittycad = { workspace = true }
kittycad-execution-plan = { workspace = true }
@ -15,6 +16,7 @@ kittycad-modeling-cmds = { workspace = true }
kittycad-modeling-session = { workspace = true }
thiserror = "1.0.57"
tokio = { version = "1.36.0", features = ["macros", "rt"] }
twenty-twenty = "0.7.0"
uuid = "1.7"
[dev-dependencies]

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -113,6 +113,26 @@ impl BindingScope {
"lineTo".into(),
EpBinding::from(KclFunction::LineTo(native_functions::sketch::LineTo)),
),
(
"line".into(),
EpBinding::from(KclFunction::Line(native_functions::sketch::Line)),
),
(
"xLineTo".into(),
EpBinding::from(KclFunction::XLineTo(native_functions::sketch::XLineTo)),
),
(
"xLine".into(),
EpBinding::from(KclFunction::XLine(native_functions::sketch::XLine)),
),
(
"yLineTo".into(),
EpBinding::from(KclFunction::YLineTo(native_functions::sketch::YLineTo)),
),
(
"yLine".into(),
EpBinding::from(KclFunction::YLine(native_functions::sketch::YLine)),
),
(
"extrude".into(),
EpBinding::from(KclFunction::Extrude(native_functions::sketch::Extrude)),

View File

@ -76,7 +76,7 @@ impl From<ExecutionFailed> for Error {
) -> Self {
Self::Execution {
error,
instruction,
instruction: instruction.expect("no instruction"),
instruction_index,
}
}

View File

@ -262,6 +262,11 @@ impl Planner {
KclFunction::StartSketchAt(f) => f.call(&mut ctx, args)?,
KclFunction::Extrude(f) => f.call(&mut ctx, args)?,
KclFunction::LineTo(f) => f.call(&mut ctx, args)?,
KclFunction::Line(f) => f.call(&mut ctx, args)?,
KclFunction::XLineTo(f) => f.call(&mut ctx, args)?,
KclFunction::XLine(f) => f.call(&mut ctx, args)?,
KclFunction::YLineTo(f) => f.call(&mut ctx, args)?,
KclFunction::YLine(f) => f.call(&mut ctx, args)?,
KclFunction::Add(f) => f.call(&mut ctx, args)?,
KclFunction::Close(f) => f.call(&mut ctx, args)?,
KclFunction::UserDefined(f) => {
@ -631,6 +636,11 @@ enum KclFunction {
Id(native_functions::Id),
StartSketchAt(native_functions::sketch::StartSketchAt),
LineTo(native_functions::sketch::LineTo),
Line(native_functions::sketch::Line),
XLineTo(native_functions::sketch::XLineTo),
XLine(native_functions::sketch::XLine),
YLineTo(native_functions::sketch::YLineTo),
YLine(native_functions::sketch::YLine),
Add(native_functions::Add),
UserDefined(UserDefinedFunction),
Extrude(native_functions::sketch::Extrude),

View File

@ -3,4 +3,4 @@
pub mod helpers;
pub mod stdlib_functions;
pub use stdlib_functions::{Close, Extrude, LineTo, StartSketchAt};
pub use stdlib_functions::{Close, Extrude, Line, LineTo, StartSketchAt, XLine, XLineTo, YLine, YLineTo};

View File

@ -139,7 +139,7 @@ pub fn sequence_binding(
}
}
/// Extract a 2D point from an argument to a Cabble.
/// Extract a 2D point from an argument to a KCL Function.
pub fn arg_point2d(
arg: EpBinding,
fn_name: &'static str,
@ -148,7 +148,7 @@ pub fn arg_point2d(
arg_number: usize,
) -> Result<Address, CompileError> {
let expected = "2D point (array with length 2)";
let elements = sequence_binding(arg, "startSketchAt", "an array of length 2", arg_number)?;
let elements = sequence_binding(arg, fn_name, "an array of length 2", arg_number)?;
if elements.len() != 2 {
return Err(CompileError::ArgWrongType {
fn_name,
@ -165,12 +165,12 @@ pub fn arg_point2d(
let start_z = start + 2;
instructions.extend([
Instruction::Copy {
source: single_binding(elements[0].clone(), "startSketchAt", "number", arg_number)?,
source: single_binding(elements[0].clone(), fn_name, "number", arg_number)?,
destination: Destination::Address(start_x),
length: 1,
},
Instruction::Copy {
source: single_binding(elements[1].clone(), "startSketchAt", "number", arg_number)?,
source: single_binding(elements[1].clone(), fn_name, "number", arg_number)?,
destination: Destination::Address(start_y),
length: 1,
},

View File

@ -1,7 +1,7 @@
use kittycad_execution_plan::{
api_request::ApiRequest,
sketch_types::{self, Axes, BasePath, Plane, SketchGroup},
Destination, Instruction,
BinaryArithmetic, BinaryOperation, Destination, Instruction, Operand,
};
use kittycad_execution_plan_traits::{Address, InMemory, Primitive, Value};
use kittycad_modeling_cmds::{
@ -13,6 +13,22 @@ use uuid::Uuid;
use super::helpers::{arg_point2d, no_arg_api_call, sg_binding, single_binding, stack_api_call};
use crate::{binding_scope::EpBinding, error::CompileError, native_functions::Callable, EvalPlan};
#[derive(PartialEq)]
pub enum At {
RelativeXY,
AbsoluteXY,
RelativeX,
AbsoluteX,
RelativeY,
AbsoluteY,
}
impl At {
pub fn is_relative(&self) -> bool {
*self == At::RelativeX || *self == At::RelativeY || *self == At::RelativeXY
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Close;
@ -140,25 +156,124 @@ impl Callable for LineTo {
&self,
ctx: &mut crate::native_functions::Context<'_>,
args: Vec<EpBinding>,
) -> Result<EvalPlan, CompileError> {
LineBare::call(ctx, "lineTo", args, LineBareOptions { at: At::AbsoluteXY })
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Line;
impl Callable for Line {
fn call(
&self,
ctx: &mut crate::native_functions::Context<'_>,
args: Vec<EpBinding>,
) -> Result<EvalPlan, CompileError> {
LineBare::call(ctx, "line", args, LineBareOptions { at: At::RelativeXY })
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct XLineTo;
impl Callable for XLineTo {
fn call(
&self,
ctx: &mut crate::native_functions::Context<'_>,
args: Vec<EpBinding>,
) -> Result<EvalPlan, CompileError> {
LineBare::call(ctx, "xLineTo", args, LineBareOptions { at: At::AbsoluteX })
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct XLine;
impl Callable for XLine {
fn call(
&self,
ctx: &mut crate::native_functions::Context<'_>,
args: Vec<EpBinding>,
) -> Result<EvalPlan, CompileError> {
LineBare::call(ctx, "xLine", args, LineBareOptions { at: At::RelativeX })
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct YLineTo;
impl Callable for YLineTo {
fn call(
&self,
ctx: &mut crate::native_functions::Context<'_>,
args: Vec<EpBinding>,
) -> Result<EvalPlan, CompileError> {
LineBare::call(ctx, "yLineTo", args, LineBareOptions { at: At::AbsoluteY })
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct YLine;
impl Callable for YLine {
fn call(
&self,
ctx: &mut crate::native_functions::Context<'_>,
args: Vec<EpBinding>,
) -> Result<EvalPlan, CompileError> {
LineBare::call(ctx, "yLine", args, LineBareOptions { at: At::RelativeY })
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
/// Exposes all the possible arguments the `line` modeling command can take.
/// Reduces code for the other line functions needed.
/// We do not expose this to the developer since it does not align with
/// the documentation (there is no "lineBare").
pub struct LineBare;
/// Used to configure the call to handle different line variants.
pub struct LineBareOptions {
/// Where to start coordinates at, ex: At::RelativeXY.
at: At,
}
impl LineBare {
fn call(
ctx: &mut crate::native_functions::Context<'_>,
fn_name: &'static str,
args: Vec<EpBinding>,
opts: LineBareOptions,
) -> Result<EvalPlan, CompileError> {
let mut instructions = Vec::new();
let fn_name = "lineTo";
// Get both required params.
let required = 2;
let mut args_iter = args.into_iter();
let Some(to) = args_iter.next() else {
return Err(CompileError::NotEnoughArgs {
fn_name: fn_name.into(),
required: 2,
actual: 0,
required,
actual: args_iter.count(),
});
};
let Some(sketch_group) = args_iter.next() else {
return Err(CompileError::NotEnoughArgs {
fn_name: fn_name.into(),
required: 2,
actual: 1,
required,
actual: args_iter.count(),
});
};
let tag = match args_iter.next() {
Some(a) => a,
None => {
@ -171,26 +286,90 @@ impl Callable for LineTo {
EpBinding::Single(empty_string_addr)
}
};
// Check the type of required params.
let to = arg_point2d(to, fn_name, &mut instructions, ctx, 0)?;
// We don't check `to` here because it can take on either a
// EpBinding::Sequence or EpBinding::Single.
let sg = sg_binding(sketch_group, fn_name, "sketch group", 1)?;
let tag = single_binding(tag, fn_name, "string tag", 2)?;
let id = Uuid::new_v4();
// Start of the path segment (which is a straight line).
let length_of_3d_point = Point3d::<f64>::default().into_parts().len();
let start_of_line = ctx.next_address.offset_by(1);
// Reserve space for the line's end, and the `relative: bool` field.
ctx.next_address.offset_by(length_of_3d_point + 1);
let new_sg_index = ctx.assign_sketch_group();
instructions.extend([
// Copy based on the options.
match opts {
LineBareOptions { at: At::AbsoluteXY, .. } | LineBareOptions { at: At::RelativeXY, .. } => {
// Push the `to` 2D point onto the stack.
let EpBinding::Sequence { elements, length_at: _ } = to.clone() else {
return Err(CompileError::InvalidOperand("Must pass a list of length 2"));
};
let &[EpBinding::Single(el0), EpBinding::Single(el1)] = elements.as_slice() else {
return Err(CompileError::InvalidOperand("Must pass a sequence here."));
};
instructions.extend([
Instruction::Copy {
source: to,
length: 2,
// X
source: el0,
length: 1,
destination: Destination::StackPush,
},
// Make it a 3D point.
Instruction::StackExtend { data: vec![0.0.into()] },
Instruction::Copy {
// Y
source: el1,
length: 1,
destination: Destination::StackExtend,
},
Instruction::StackExtend { data: vec![0.0.into()] }, // Z
]);
}
LineBareOptions { at: At::AbsoluteX, .. } | LineBareOptions { at: At::RelativeX, .. } => {
let EpBinding::Single(addr) = to else {
return Err(CompileError::InvalidOperand("Must pass a single value here."));
};
instructions.extend([
Instruction::Copy {
// X
source: addr,
length: 1,
destination: Destination::StackPush,
},
Instruction::StackExtend {
data: vec![Primitive::from(0.0)],
}, // Y
Instruction::StackExtend {
data: vec![Primitive::from(0.0)],
}, // Z
]);
}
LineBareOptions { at: At::AbsoluteY, .. } | LineBareOptions { at: At::RelativeY, .. } => {
let EpBinding::Single(addr) = to else {
return Err(CompileError::InvalidOperand("Must pass a single value here."));
};
instructions.extend([
Instruction::StackPush {
data: vec![Primitive::from(0.0)],
}, // X
Instruction::Copy {
// Y
source: addr,
length: 1,
destination: Destination::StackExtend,
},
Instruction::StackExtend {
data: vec![Primitive::from(0.0)],
}, // Z
]);
}
}
instructions.extend([
// Append the new path segment to memory.
// First comes its tag.
Instruction::SetPrimitive {
@ -204,7 +383,7 @@ impl Callable for LineTo {
// Then its `relative` field.
Instruction::SetPrimitive {
address: start_of_line + 1 + length_of_3d_point,
value: false.into(),
value: opts.at.is_relative().into(),
},
// Push the path ID onto the stack.
Instruction::SketchGroupCopyFrom {
@ -231,16 +410,159 @@ impl Callable for LineTo {
data: vec![Primitive::from("ToPoint".to_owned())],
},
// `BasePath::from` point.
// Place them in the secondary stack to prepare ToPoint structure.
Instruction::SketchGroupGetLastPoint {
source: sg,
destination: Destination::StackExtend,
},
]);
// Reserve space for the segment last point
let to_point_from = ctx.next_address.offset_by(2);
instructions.extend([
// Copy to the primary stack as well to be worked with.
Instruction::SketchGroupGetLastPoint {
source: sg,
destination: Destination::Address(to_point_from),
},
]);
// `BasePath::to` point.
Instruction::Copy {
source: start_of_line + 1,
length: 2,
// The copy here depends on the incoming `to` data.
// Sometimes it's a list, sometimes it's single datum.
// And the relative/not relative matters. When relative, we need to
// copy coords from `from` into the new `to` coord that don't change.
// At least everything else can be built up from these "primitives".
if let EpBinding::Sequence { elements, length_at: _ } = to.clone() {
if let &[EpBinding::Single(el0), EpBinding::Single(el1)] = elements.as_slice() {
match opts {
// ToPoint { from: { x1, y1 }, to: { x1 + x2, y1 + y2 } }
LineBareOptions { at: At::RelativeXY, .. } => {
instructions.extend([
Instruction::BinaryArithmetic {
arithmetic: BinaryArithmetic {
operation: BinaryOperation::Add,
operand0: Operand::Reference(to_point_from + 0),
operand1: Operand::Reference(el0),
},
destination: Destination::StackExtend,
},
Instruction::BinaryArithmetic {
arithmetic: BinaryArithmetic {
operation: BinaryOperation::Add,
operand0: Operand::Reference(to_point_from + 1),
operand1: Operand::Reference(el1),
},
destination: Destination::StackExtend,
},
]);
}
// ToPoint { from: { x1, y1 }, to: { x2, y2 } }
LineBareOptions { at: At::AbsoluteXY, .. } => {
// Otherwise just directly copy the new points.
instructions.extend([
Instruction::Copy {
source: el0,
length: 1,
destination: Destination::StackExtend,
},
Instruction::Copy {
source: el1,
length: 1,
destination: Destination::StackExtend,
},
]);
}
_ => {
return Err(CompileError::InvalidOperand(
"A Sequence with At::...X or At::...Y is not valid here. Must be At::...XY.",
));
}
}
}
} else if let EpBinding::Single(addr) = to {
match opts {
// ToPoint { from: { x1, y1 }, to: { x1 + x2, y1 } }
LineBareOptions { at: At::RelativeX } => {
instructions.extend([
Instruction::BinaryArithmetic {
arithmetic: BinaryArithmetic {
operation: BinaryOperation::Add,
operand0: Operand::Reference(to_point_from + 0),
operand1: Operand::Reference(addr),
},
destination: Destination::StackExtend,
},
Instruction::Copy {
source: to_point_from + 1,
length: 1,
destination: Destination::StackExtend,
},
]);
}
// ToPoint { from: { x1, y1 }, to: { x2, y1 } }
LineBareOptions { at: At::AbsoluteX } => {
instructions.extend([
Instruction::Copy {
source: addr,
length: 1,
destination: Destination::StackExtend,
},
Instruction::Copy {
source: to_point_from + 1,
length: 1,
destination: Destination::StackExtend,
},
]);
}
// ToPoint { from: { x1, y1 }, to: { x1, y1 + y2 } }
LineBareOptions { at: At::RelativeY } => {
instructions.extend([
Instruction::Copy {
source: to_point_from + 0,
length: 1,
destination: Destination::StackExtend,
},
Instruction::BinaryArithmetic {
arithmetic: BinaryArithmetic {
operation: BinaryOperation::Add,
operand0: Operand::Reference(to_point_from + 1),
operand1: Operand::Reference(addr),
},
destination: Destination::StackExtend,
},
]);
}
// ToPoint { from: { x1, y1 }, to: { x1, y2 } }
LineBareOptions { at: At::AbsoluteY } => {
instructions.extend([
Instruction::Copy {
source: to_point_from + 0,
length: 1,
destination: Destination::StackExtend,
},
Instruction::Copy {
source: addr,
length: 1,
destination: Destination::StackExtend,
},
]);
}
_ => {
return Err(CompileError::InvalidOperand(
"A Single binding with At::...XY is not valid here.",
));
}
}
} else {
return Err(CompileError::InvalidOperand(
"Must be a sequence or single value binding.",
));
}
instructions.extend([
// `BasePath::name` string.
Instruction::Copy {
source: tag,

View File

@ -1048,14 +1048,10 @@ fn store_object_with_array_property() {
/// Write the program's plan to the KCVM debugger's normal input file.
#[allow(unused)]
fn kcvm_dbg(kcl_program: &str) {
fn kcvm_dbg(kcl_program: &str, path: &str) {
let (plan, _scope, _) = must_plan(kcl_program);
let plan_json = serde_json::to_string_pretty(&plan).unwrap();
std::fs::write(
"/Users/adamchalmers/kc-repos/modeling-api/execution-plan-debugger/test_input.json",
plan_json,
)
.unwrap();
std::fs::write(path, plan_json).unwrap();
}
#[tokio::test]
@ -1069,8 +1065,6 @@ async fn stdlib_cube_partial() {
|> close(%)
|> extrude(100.0, %)
"#;
let (_plan, _scope, last_address) = must_plan(program);
assert_eq!(last_address, Address::ZERO + 66);
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
.ast()
.unwrap();
@ -1113,23 +1107,115 @@ async fn stdlib_cube_partial() {
},
]
);
// use kittycad_modeling_cmds::{each_cmd, ok_response::OkModelingCmdResponse, ImageFormat};
// let out = client
// .unwrap()
// .run_command(
// uuid::Uuid::new_v4().into(),
// each_cmd::TakeSnapshot {
// format: ImageFormat::Png,
// },
// )
// .await
// .unwrap();
// let out = match out {
// OkModelingCmdResponse::TakeSnapshot(b) => b,
// other => panic!("wrong output: {other:?}"),
// };
// let out: Vec<u8> = out.contents.into();
// std::fs::write("image.png", out).unwrap();
use kittycad_modeling_cmds::{each_cmd, ok_response::OkModelingCmdResponse, ImageFormat};
let out = client
.unwrap()
.run_command(
uuid::Uuid::new_v4().into(),
kittycad_modeling_cmds::ModelingCmd::from(each_cmd::TakeSnapshot {
format: ImageFormat::Png,
}),
)
.await
.unwrap();
let out = match out {
OkModelingCmdResponse::TakeSnapshot(kittycad_modeling_cmds::output::TakeSnapshot { contents: b }) => b,
other => panic!("wrong output: {other:?}"),
};
use image::io::Reader as ImageReader;
let img = ImageReader::new(std::io::Cursor::new(out))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
twenty_twenty::assert_image("fixtures/cube_lineTo.png", &img, 0.9999);
}
#[tokio::test]
async fn stdlib_cube_xline_yline() {
let program = r#"
let cube = startSketchAt([0.0, 0.0], "adam")
|> xLine(210.0, %, "side0")
|> yLine(210.0, %, "side1")
|> xLine(-210.0, %, "side2")
|> yLine(-210.0, %, "side3")
|> close(%)
|> extrude(100.0, %)
"#;
kcvm_dbg(
program,
"/home/lee/Code/Zoo/modeling-api/execution-plan-debugger/cube_xyline.json",
);
let (_plan, _scope, _last_address) = must_plan(program);
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
.ast()
.unwrap();
let mut client = Some(test_client().await);
let mem = match crate::execute(ast, &mut client).await {
Ok(mem) => mem,
Err(e) => panic!("{e}"),
};
let sg = &mem.sketch_groups.last().unwrap();
assert_eq!(
sg.path_rest,
vec![
sketch_types::PathSegment::ToPoint {
base: sketch_types::BasePath {
from: Point2d { x: 0.0, y: 0.0 },
to: Point2d { x: 210.0, y: 0.0 },
name: "side0".into(),
}
},
sketch_types::PathSegment::ToPoint {
base: sketch_types::BasePath {
from: Point2d { x: 210.0, y: 0.0 },
to: Point2d { x: 210.0, y: 210.0 },
name: "side1".into(),
}
},
sketch_types::PathSegment::ToPoint {
base: sketch_types::BasePath {
from: Point2d { x: 210.0, y: 210.0 },
to: Point2d { x: 0.0, y: 210.0 },
name: "side2".into(),
}
},
sketch_types::PathSegment::ToPoint {
base: sketch_types::BasePath {
from: Point2d { x: 0.0, y: 210.0 },
to: Point2d { x: 0.0, y: 0.0 },
name: "side3".into(),
}
},
]
);
use kittycad_modeling_cmds::{each_cmd, ok_response::OkModelingCmdResponse, ImageFormat};
let out = client
.unwrap()
.run_command(
uuid::Uuid::new_v4().into(),
kittycad_modeling_cmds::ModelingCmd::from(each_cmd::TakeSnapshot {
format: ImageFormat::Png,
}),
)
.await
.unwrap();
let out = match out {
OkModelingCmdResponse::TakeSnapshot(kittycad_modeling_cmds::output::TakeSnapshot { contents: b }) => b,
other => panic!("wrong output: {other:?}"),
};
use image::io::Reader as ImageReader;
let img = ImageReader::new(std::io::Cursor::new(out))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
twenty_twenty::assert_image("fixtures/cube_xyLine.png", &img, 0.9999);
}
async fn test_client() -> Session {