Compare commits

...

4 Commits

Author SHA1 Message Date
17ddf52264 WIP: gracklify id 2024-02-28 12:19:50 -05:00
d318f4ddb2 WIP: gracklify id 2024-02-28 11:36:06 -05:00
7823bff8d7 Move startsketchAt into a new 'sketch' module 2024-02-27 08:50:21 -06:00
9ef114d1bd Grackle: implement StartSketchAt stdlib function 2024-02-26 20:03:31 -06:00
10 changed files with 415 additions and 89 deletions

View File

@ -1460,13 +1460,16 @@ name = "grackle"
version = "0.1.0"
dependencies = [
"kcl-lib",
"kittycad",
"kittycad-execution-plan",
"kittycad-execution-plan-traits",
"kittycad-modeling-cmds",
"kittycad-modeling-session",
"pretty_assertions",
"serde_json",
"thiserror",
"tokio",
"uuid",
]
[[package]]
@ -1912,7 +1915,7 @@ dependencies = [
"itertools 0.12.1",
"js-sys",
"kittycad",
"kittycad-execution-plan-macros 0.1.4 (git+https://github.com/KittyCAD/modeling-api?branch=main)",
"kittycad-execution-plan-macros",
"kittycad-execution-plan-traits",
"lazy_static",
"parse-display 0.9.0",
@ -1986,7 +1989,7 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan"
version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#08f05d91062380fe3a69f4baa1f1301532d31977"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#be68126f2dbf6c15f44fb72db5b5cb44122aed36"
dependencies = [
"bytes",
"insta",
@ -2004,19 +2007,8 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan-macros"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d31b689c944d00aadda2ef83d8422a6efff97e1be5654a61f9d95496f0c19e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.49",
]
[[package]]
name = "kittycad-execution-plan-macros"
version = "0.1.4"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#632b75a0242400fa34373d7973b9149b0e08aa3f"
version = "0.1.5"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#be68126f2dbf6c15f44fb72db5b5cb44122aed36"
dependencies = [
"proc-macro2",
"quote",
@ -2026,8 +2018,7 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan-traits"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3ec8efd57b59697eb140b63c0ffe7db44fdfe5a55f14e45513411eba2280ba5"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#be68126f2dbf6c15f44fb72db5b5cb44122aed36"
dependencies = [
"serde",
"thiserror",
@ -2036,8 +2027,8 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.1.18"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#08f05d91062380fe3a69f4baa1f1301532d31977"
version = "0.1.24"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#be68126f2dbf6c15f44fb72db5b5cb44122aed36"
dependencies = [
"anyhow",
"chrono",
@ -2047,8 +2038,9 @@ dependencies = [
"enum-iterator-derive",
"euler",
"http 0.2.9",
"kittycad-execution-plan-macros 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"kittycad-execution-plan-macros",
"kittycad-execution-plan-traits",
"kittycad-modeling-cmds-macros",
"kittycad-unit-conversion-derive",
"measurements",
"parse-display 0.8.2",
@ -2061,10 +2053,20 @@ dependencies = [
"webrtc",
]
[[package]]
name = "kittycad-modeling-cmds-macros"
version = "0.1.1"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#be68126f2dbf6c15f44fb72db5b5cb44122aed36"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.49",
]
[[package]]
name = "kittycad-modeling-session"
version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#08f05d91062380fe3a69f4baa1f1301532d31977"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#be68126f2dbf6c15f44fb72db5b5cb44122aed36"
dependencies = [
"futures",
"kittycad",

View File

@ -60,9 +60,10 @@ members = [
[workspace.dependencies]
kittycad = { version = "0.2.54", default-features = false, features = ["js", "requests"] }
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = "0.1.10"
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-modeling-cmds = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
[[test]]
name = "executor"
@ -75,4 +76,7 @@ path = "tests/modify/main.rs"
# Example: how to point modeling-api at a different repo (e.g. a branch or a local clone)
#[patch."https://github.com/KittyCAD/modeling-api"]
#kittycad-execution-plan = { path = "../../../modeling-api/execution-plan" }
#kittycad-execution-plan-macros = { path = "../../../modeling-api/execution-plan-macros" }
#kittycad-execution-plan-traits = { path = "../../../modeling-api/execution-plan-traits" }
#kittycad-modeling-cmds = { path = "../../../modeling-api/modeling-cmds" }
#kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }

View File

@ -7,11 +7,14 @@ description = "A new executor for KCL which compiles to Execution Plans"
[dependencies]
kcl-lib = { path = "../kcl" }
kittycad = { workspace = true }
kittycad-execution-plan = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
kittycad-modeling-session = { workspace = true }
thiserror = "1.0.57"
tokio = { version = "1.36.0", features = ["macros", "rt"] }
uuid = "1.7"
[dev-dependencies]
pretty_assertions = "1"

View File

@ -2,8 +2,12 @@ use std::collections::HashMap;
use kcl_lib::ast::types::{LiteralIdentifier, LiteralValue};
use super::{native_functions, Address};
use crate::{CompileError, KclFunction};
use crate::{
CompileError,
KclFunction
};
/// KCL values which can be written to KCEP memory.
/// This is recursive. For example, the bound value might be an array, which itself contains bound values.
@ -99,11 +103,11 @@ impl BindingScope {
// TODO: Actually put the stdlib prelude in here,
// things like `startSketchAt` and `line`.
ep_bindings: HashMap::from([
("id".into(), EpBinding::from(KclFunction::Id(native_functions::Id))),
("id".into(), EpBinding::from(KclFunction::Id(native_functions::id::Id))),
("add".into(), EpBinding::from(KclFunction::Add(native_functions::Add))),
(
"startSketchAt".into(),
EpBinding::from(KclFunction::StartSketchAt(native_functions::StartSketchAt)),
EpBinding::from(KclFunction::StartSketchAt(native_functions::sketch::StartSketchAt)),
),
]),
parent: None,

View File

@ -45,6 +45,12 @@ pub enum CompileError {
NoReturnStmt,
#[error("You used the %, which means \"substitute this argument for the value to the left in this |> pipeline\". But there is no such value, because you're not calling a pipeline.")]
NotInPipeline,
#[error("The function '{fn_name}' expects a parameter of type {expected} but you supplied {actual}")]
ArgWrongType {
fn_name: &'static str,
expected: &'static str,
actual: String,
},
}
#[derive(Debug, thiserror::Error)]

View File

@ -589,6 +589,8 @@ fn kcl_literal_to_kcep_literal(expr: LiteralValue) -> ept::Primitive {
}
/// Instructions that can compute some value.
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
struct EvalPlan {
/// The instructions which will compute the value.
instructions: Vec<Instruction>,
@ -617,8 +619,8 @@ impl Eq for UserDefinedFunction {}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
enum KclFunction {
Id(native_functions::Id),
StartSketchAt(native_functions::StartSketchAt),
Id(native_functions::id::Id),
StartSketchAt(native_functions::sketch::StartSketchAt),
Add(native_functions::Add),
UserDefined(UserDefinedFunction),
}

View File

@ -2,73 +2,18 @@
//! This includes some of the stdlib, e.g. `startSketchAt`.
//! But some other stdlib functions will be written in KCL.
use kcl_lib::std::sketch::PlaneData;
use kittycad_execution_plan::{BinaryArithmetic, Destination, Instruction};
use kittycad_execution_plan_traits::{Address, Value};
use kittycad_execution_plan_traits::Address;
use crate::{CompileError, EpBinding, EvalPlan};
/// The identity function. Always returns its first input.
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Id;
pub mod sketch;
pub mod id;
pub trait Callable {
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError>;
}
impl Callable for Id {
fn call(&self, _: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
if args.len() > 1 {
return Err(CompileError::TooManyArgs {
fn_name: "id".into(),
maximum: 1,
actual: args.len(),
});
}
let arg = args
.first()
.ok_or(CompileError::NotEnoughArgs {
fn_name: "id".into(),
required: 1,
actual: 0,
})?
.clone();
Ok(EvalPlan {
instructions: Vec::new(),
binding: arg,
})
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct StartSketchAt;
impl Callable for StartSketchAt {
fn call(&self, next_addr: &mut Address, _args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let mut instructions = Vec::new();
// Store the plane.
let plane = PlaneData::XY.into_parts();
instructions.push(Instruction::SetValue {
address: next_addr.offset_by(plane.len()),
value_parts: plane,
});
// TODO: Get the plane ID from global context.
// TODO: Send this command:
// ModelingCmd::SketchModeEnable {
// animated: false,
// ortho: false,
// plane_id: plane.id,
// // We pass in the normal for the plane here.
// disable_camera_with_plane: Some(plane.z_axis.clone().into()),
// },
// TODO: Send ModelingCmd::StartPath at the given point.
// TODO (maybe): Store the SketchGroup in KCEP memory.
todo!()
}
}
/// A test function that adds two numbers.
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]

View File

@ -0,0 +1,91 @@
use kittycad_execution_plan_traits::Address;
use crate::{CompileError, EpBinding, EvalPlan};
use super::Callable;
/// The identity function. Always returns its first input.
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Id;
impl Callable for Id {
fn call(&self, _next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
if args.len() > 1 {
return Err(CompileError::TooManyArgs {
fn_name: "id".into(),
maximum: 1,
actual: args.len(),
});
} else if args.len() < 1 {
return Err(CompileError::NotEnoughArgs {
fn_name: "id".into(),
required: 1,
actual: args.len(),
});
}
let arg = args.get(0).unwrap().clone();
Ok(EvalPlan {
instructions: vec![],
binding: arg,
})
}
}
#[test]
fn call_id() {
let fn_id = Id {};
let mut addr = Address::ZERO;
addr = addr.offset_by(1);
let args = vec![EpBinding::Single(addr)];
let ep = fn_id.call(&mut addr, args.clone());
assert_eq!(
ep,
Ok(EvalPlan{
instructions: vec![],
binding: args.get(0).unwrap().clone()
})
);
}
#[test]
fn call_id_too_many_args() {
let fn_id = Id {};
let mut addr = Address::ZERO;
let b1 = EpBinding::Single(addr);
addr = addr.offset_by(1);
let b2 = EpBinding::Single(addr);
addr = addr.offset_by(1);
let args = vec![b1, b2];
let ep = fn_id.call(&mut addr, args.clone());
assert_eq!(
ep,
Err(CompileError::TooManyArgs {
fn_name: "id".into(),
maximum: 1,
actual: args.len(),
})
);
}
#[test]
fn call_id_not_enough_args() {
let fn_id = Id {};
let mut addr = Address::ZERO;
let args = vec![];
let ep = fn_id.call(&mut addr, args.clone());
assert_eq!(
ep,
Err(CompileError::NotEnoughArgs {
fn_name: "id".into(),
required: 1,
actual: args.len(),
})
);
}

View File

@ -0,0 +1,202 @@
//! Native functions for sketching on the plane.
use kittycad_execution_plan::{api_request::ApiRequest, Instruction};
use kittycad_execution_plan_traits::{Address, InMemory, Value};
use kittycad_modeling_cmds::{id::ModelingCmdId, shared::Point3d, ModelingCmdEndpoint};
use uuid::Uuid;
use crate::{binding_scope::EpBinding, error::CompileError, EvalPlan};
use super::Callable;
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct StartSketchAt;
impl Callable for StartSketchAt {
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let mut instructions = Vec::new();
// First, before we send any API calls, let's validate the arguments to this function.
let mut args_iter = args.into_iter();
let Some(start) = args_iter.next() else {
return Err(CompileError::NotEnoughArgs {
fn_name: "startSketchAt".into(),
required: 1,
actual: 0,
});
};
let start_point = {
let expected = "2D point (array with length 2)";
let fn_name = "startSketchAt";
match start {
EpBinding::Single(_) => {
return Err(CompileError::ArgWrongType {
fn_name,
expected,
actual: "a single value".to_owned(),
})
}
EpBinding::Sequence { elements, .. } if elements.len() == 2 => {
// KCL stores points as an array.
// KC API stores them as Rust objects laid flat out in memory.
let start = next_addr.offset_by(2);
let start_x = start;
let start_y = start + 1;
let start_z = start + 2;
instructions.extend([
Instruction::Copy {
source: single_binding(
elements[0].clone(),
"startSketchAt (first parameter, elem 0)",
"number",
)?,
destination: start_x,
},
Instruction::Copy {
source: single_binding(
elements[1].clone(),
"startSketchAt (first parameter, elem 1)",
"number",
)?,
destination: start_y,
},
Instruction::SetPrimitive {
address: start_z,
value: 0.0.into(),
},
]);
start
}
EpBinding::Sequence { elements, .. } => {
return Err(CompileError::ArgWrongType {
fn_name,
expected,
actual: format!("array of length {}", elements.len()),
})
}
EpBinding::Map { .. } => {
return Err(CompileError::ArgWrongType {
fn_name,
expected,
actual: "object".to_owned(),
})
}
EpBinding::Function(_) => {
return Err(CompileError::ArgWrongType {
fn_name,
expected,
actual: "function".to_owned(),
})
}
}
};
// Now the function can start.
// First API call: make the plane.
let plane_id = Uuid::new_v4();
stack_api_call(
&mut instructions,
ModelingCmdEndpoint::MakePlane,
None,
plane_id.into(),
[
Some(true).into_parts(), // hide
vec![false.into()], // clobber
vec![60.0.into()], // size
Point3d { x: 0.0, y: 1.0, z: 0.0 }.into_parts(), // Y axis
Point3d { x: 1.0, y: 0.0, z: 0.0 }.into_parts(), // X axis
Point3d { x: 0.0, y: 0.0, z: 0.0 }.into_parts(), // origin of plane
],
);
// Next, enter sketch mode.
stack_api_call(
&mut instructions,
ModelingCmdEndpoint::SketchModeEnable,
None,
Uuid::new_v4().into(),
[
Some(Point3d { x: 0.0, y: 0.0, z: 1.0 }).into_parts(), // Z axis
vec![false.into()], // animated
vec![false.into()], // ortho mode
vec![plane_id.into()], // plane ID
],
);
// Then start a path
let path_id = Uuid::new_v4();
no_arg_api_call(&mut instructions, ModelingCmdEndpoint::StartPath, path_id.into());
// Move the path pen to the given point.
instructions.push(Instruction::StackPush {
data: vec![path_id.into()],
});
instructions.push(Instruction::ApiRequest(ApiRequest {
endpoint: ModelingCmdEndpoint::MovePathPen,
store_response: None,
arguments: vec![InMemory::StackPop, InMemory::Address(start_point)],
cmd_id: Uuid::new_v4().into(),
}));
// TODO: Store the SketchGroup in KCEP memory.
let sketch_group = EpBinding::Single(Address::ZERO + 999);
Ok(EvalPlan {
instructions,
binding: sketch_group,
})
}
}
/// Emit instructions for an API call with no parameters.
fn no_arg_api_call(instrs: &mut Vec<Instruction>, endpoint: ModelingCmdEndpoint, cmd_id: ModelingCmdId) {
instrs.push(Instruction::ApiRequest(ApiRequest {
endpoint,
store_response: None,
arguments: vec![],
cmd_id,
}))
}
/// Emit instructions for an API call with the given parameters.
/// The API parameters are stored in the EP memory stack.
/// So, they have to be pushed onto the stack in the right order,
/// i.e. the reverse order in which the API call's Rust struct defines the fields.
fn stack_api_call<const N: usize>(
instrs: &mut Vec<Instruction>,
endpoint: ModelingCmdEndpoint,
store_response: Option<Address>,
cmd_id: ModelingCmdId,
data: [Vec<kittycad_execution_plan_traits::Primitive>; N],
) {
let arguments = vec![InMemory::StackPop; data.len()];
instrs.extend(data.map(|data| Instruction::StackPush { data }));
instrs.push(Instruction::ApiRequest(ApiRequest {
endpoint,
store_response,
arguments,
cmd_id,
}))
}
fn single_binding(b: EpBinding, fn_name: &'static str, expected: &'static str) -> Result<Address, CompileError> {
match b {
EpBinding::Single(a) => Ok(a),
EpBinding::Sequence { .. } => Err(CompileError::ArgWrongType {
fn_name,
expected,
actual: "array".to_owned(),
}),
EpBinding::Map { .. } => Err(CompileError::ArgWrongType {
fn_name,
expected,
actual: "array".to_owned(),
}),
EpBinding::Function(_) => Err(CompileError::ArgWrongType {
fn_name,
expected,
actual: "function".to_owned(),
}),
}
}

View File

@ -1,7 +1,9 @@
use std::collections::HashMap;
use std::env;
use ep::{Destination, UnaryArithmetic};
use ept::{ListHeader, ObjectHeader};
use kittycad_modeling_session::SessionBuilder;
use pretty_assertions::assert_eq;
use super::*;
@ -1044,6 +1046,71 @@ fn store_object_with_array_property() {
)
}
#[tokio::test]
async fn stdlib_cube_partial() {
let program = r#"
let cube = startSketchAt([22.0, 33.0])
"#;
let (plan, _scope) = must_plan(program);
std::fs::write("stdlib_cube_partial.json", serde_json::to_string_pretty(&plan).unwrap()).unwrap();
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
.ast()
.unwrap();
let mem = crate::execute(ast, Some(test_client().await)).await.unwrap();
dbg!(mem);
}
async fn test_client() -> Session {
let kittycad_api_token = env::var("KITTYCAD_API_TOKEN").expect("You must set $KITTYCAD_API_TOKEN");
let kittycad_api_client = kittycad::Client::new(kittycad_api_token);
let session_builder = SessionBuilder {
client: kittycad_api_client,
fps: Some(10),
unlocked_framerate: Some(false),
video_res_height: Some(720),
video_res_width: Some(1280),
buffer_reqs: None,
await_response_timeout: None,
};
match Session::start(session_builder).await {
Err(e) => match e {
kittycad::types::error::Error::InvalidRequest(s) => panic!("Request did not meet requirements {s}"),
kittycad::types::error::Error::CommunicationError(e) => {
panic!(" A server error either due to the data, or with the connection: {e}")
}
kittycad::types::error::Error::RequestError(e) => panic!("Could not build request: {e}"),
kittycad::types::error::Error::SerdeError { error, status } => {
panic!("Serde error (HTTP {status}): {error}")
}
kittycad::types::error::Error::InvalidResponsePayload { error, response } => {
panic!("Invalid response payload. Error {error}, response {response:?}")
}
kittycad::types::error::Error::Server { body, status } => panic!("Server error (HTTP {status}): {body}"),
kittycad::types::error::Error::UnexpectedResponse(resp) => {
let status = resp.status();
let url = resp.url().to_owned();
match resp.text().await {
Ok(body) => panic!(
"Unexpected response from KittyCAD API.
URL:{url}
HTTP {status}
---Body----
{body}"
),
Err(e) => panic!(
"Unexpected response from KittyCAD API.
URL:{url}
HTTP {status}
---Body could not be read, the error is----
{e}"
),
}
}
},
Ok(x) => x,
}
}
#[ignore = "haven't done API calls or stdlib yet"]
#[test]
fn stdlib_api_calls() {