Grackle: implement StartSketchAt stdlib function
This commit is contained in:
		
							
								
								
									
										44
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										44
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -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", | ||||
|  | ||||
| @ -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" | ||||
| @ -73,6 +74,9 @@ name = "modify" | ||||
| 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-modeling-session = { path = "../../../modeling-api/modeling-session" } | ||||
| #[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" } | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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)] | ||||
|  | ||||
| @ -2,9 +2,10 @@ | ||||
| //! 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::{api_request::ApiRequest, BinaryArithmetic, Destination, Instruction}; | ||||
| use kittycad_execution_plan_traits::{Address, InMemory, Value}; | ||||
| use kittycad_modeling_cmds::{id::ModelingCmdId, shared::Point3d, ModelingCmdEndpoint}; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| use crate::{CompileError, EpBinding, EvalPlan}; | ||||
|  | ||||
| @ -46,29 +47,172 @@ impl Callable for Id { | ||||
| pub struct StartSketchAt; | ||||
|  | ||||
| impl Callable for StartSketchAt { | ||||
|     fn call(&self, next_addr: &mut Address, _args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> { | ||||
|     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, | ||||
|         // 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()], | ||||
|         }); | ||||
|         // 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!() | ||||
|         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, | ||||
|     })) | ||||
| } | ||||
|  | ||||
| /// A test function that adds two numbers. | ||||
| #[derive(Debug, Clone)] | ||||
| #[cfg_attr(test, derive(Eq, PartialEq))] | ||||
| @ -110,3 +254,24 @@ impl Callable for Add { | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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(), | ||||
|         }), | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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() { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user