From cfbc77b62f275aae3fc3f4c9be90eeb3f81a559a Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Tue, 13 Feb 2024 10:26:09 -0800 Subject: [PATCH] Start end for sketch on face (#1406) * updates Signed-off-by: Jess Frazelle * add tests Signed-off-by: Jess Frazelle * clippy Signed-off-by: Jess Frazelle --------- Signed-off-by: Jess Frazelle --- docs/kcl/std.json | 28 +++- docs/kcl/std.md | 8 +- src/wasm-lib/kcl/src/executor.rs | 2 +- src/wasm-lib/kcl/src/fs/mod.rs | 3 +- src/wasm-lib/kcl/src/std/extrude.rs | 5 +- src/wasm-lib/kcl/src/std/mod.rs | 17 ++- src/wasm-lib/kcl/src/std/sketch.rs | 120 +++++++++++++++--- src/wasm-lib/tests/executor/main.rs | 56 ++++++++ .../executor/outputs/sketch_on_face_end.png | Bin 0 -> 35620 bytes .../executor/outputs/sketch_on_face_start.png | Bin 0 -> 37332 bytes 10 files changed, 211 insertions(+), 28 deletions(-) create mode 100644 src/wasm-lib/tests/executor/outputs/sketch_on_face_end.png create mode 100644 src/wasm-lib/tests/executor/outputs/sketch_on_face_start.png diff --git a/docs/kcl/std.json b/docs/kcl/std.json index a2b13c29d..5ff450f9a 100644 --- a/docs/kcl/std.json +++ b/docs/kcl/std.json @@ -27979,9 +27979,33 @@ }, { "name": "tag", - "type": "String", + "type": "SketchOnFaceTag", "schema": { - "type": "string", + "description": "A tag for sketch on face.", + "anyOf": [ + { + "oneOf": [ + { + "description": "The start face as in before you extruded. This could also be known as the bottom face. But we do not call it bottom because it would be the top face if you extruded it in the opposite direction or flipped the camera.", + "type": "string", + "enum": [ + "start" + ] + }, + { + "description": "The end face after you extruded. This could also be known as the top face. But we do not call it top because it would be the bottom face if you extruded it in the opposite direction or flipped the camera.", + "type": "string", + "enum": [ + "end" + ] + } + ] + }, + { + "description": "A string tag for the face you want to sketch on.", + "type": "string" + } + ], "nullable": true }, "required": true diff --git a/docs/kcl/std.md b/docs/kcl/std.md index 07175263d..6d63ed6e0 100644 --- a/docs/kcl/std.md +++ b/docs/kcl/std.md @@ -5149,7 +5149,7 @@ Start a sketch on a specific plane or face. ``` -startSketchOn(data: SketchData, tag: String) -> SketchSurface +startSketchOn(data: SketchData, tag: SketchOnFaceTag) -> SketchSurface ``` #### Arguments @@ -5239,7 +5239,11 @@ startSketchOn(data: SketchData, tag: String) -> SketchSurface }, } ``` -* `tag`: `String` +* `tag`: `SketchOnFaceTag` - A tag for sketch on face. +``` +"start" | "end" | +string +``` #### Returns diff --git a/src/wasm-lib/kcl/src/executor.rs b/src/wasm-lib/kcl/src/executor.rs index 959705810..36533c9ea 100644 --- a/src/wasm-lib/kcl/src/executor.rs +++ b/src/wasm-lib/kcl/src/executor.rs @@ -862,7 +862,7 @@ impl ExtrudeSurface { pub fn get_name(&self) -> String { match self { - ExtrudeSurface::ExtrudePlane(ep) => ep.name.clone(), + ExtrudeSurface::ExtrudePlane(ep) => ep.name.to_string(), } } diff --git a/src/wasm-lib/kcl/src/fs/mod.rs b/src/wasm-lib/kcl/src/fs/mod.rs index 6a76a0a87..b321bb523 100644 --- a/src/wasm-lib/kcl/src/fs/mod.rs +++ b/src/wasm-lib/kcl/src/fs/mod.rs @@ -8,12 +8,11 @@ pub use local::FileManager; #[cfg(target_arch = "wasm32")] #[cfg(not(test))] pub mod wasm; +use anyhow::Result; #[cfg(target_arch = "wasm32")] #[cfg(not(test))] pub use wasm::FileManager; -use anyhow::Result; - #[async_trait::async_trait(?Send)] pub trait FileSystem: Clone { /// Read a file from the local file system. diff --git a/src/wasm-lib/kcl/src/std/extrude.rs b/src/wasm-lib/kcl/src/std/extrude.rs index 71ff71d78..686da0ea3 100644 --- a/src/wasm-lib/kcl/src/std/extrude.rs +++ b/src/wasm-lib/kcl/src/std/extrude.rs @@ -123,7 +123,10 @@ async fn inner_extrude(length: f64, sketch_group: Box, args: Args) } Ok(Box::new(ExtrudeGroup { - id, + // Ok so you would think that the id would be the id of the extrude group, + // that we passed in to the function, but it's actually the id of the + // sketch group. + id: sketch_group.id, value: new_value, height: length, position: sketch_group.position, diff --git a/src/wasm-lib/kcl/src/std/mod.rs b/src/wasm-lib/kcl/src/std/mod.rs index f81e27bd0..aa13d5558 100644 --- a/src/wasm-lib/kcl/src/std/mod.rs +++ b/src/wasm-lib/kcl/src/std/mod.rs @@ -20,9 +20,9 @@ use parse_display::{Display, FromStr}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use self::kcl_stdlib::KclStdLibFn; +use self::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag}; use crate::{ - ast::types::{parse_json_number_as_f64, parse_json_value_as_string}, + ast::types::parse_json_number_as_f64, docs::StdLibFn, engine::EngineManager, errors::{KclError, KclErrorDetails}, @@ -406,7 +406,9 @@ impl Args { } } - fn get_data_and_optional_tag(&self) -> Result<(T, Option), KclError> { + fn get_data_and_optional_tag( + &self, + ) -> Result<(T, Option), KclError> { let first_value = self .args .first() @@ -426,8 +428,13 @@ impl Args { })?; if let Some(second_value) = self.args.get(1) { - let tag = parse_json_value_as_string(&second_value.get_json_value()?); - Ok((data, tag)) + let tag: SketchOnFaceTag = serde_json::from_value(second_value.get_json_value()?).map_err(|e| { + KclError::Type(KclErrorDetails { + message: format!("Failed to deserialize SketchOnFaceTag from JSON: {}", e), + source_ranges: vec![self.source_range], + }) + })?; + Ok((data, Some(tag))) } else { Ok((data, None)) } diff --git a/src/wasm-lib/kcl/src/std/sketch.rs b/src/wasm-lib/kcl/src/std/sketch.rs index 21e96bda0..e5d247571 100644 --- a/src/wasm-lib/kcl/src/std/sketch.rs +++ b/src/wasm-lib/kcl/src/std/sketch.rs @@ -4,6 +4,7 @@ use anyhow::Result; use derive_docs::stdlib; use kittycad::types::{Angle, ModelingCmd, Point3D}; use kittycad_execution_plan_macros::ExecutionPlanValue; +use parse_display::{Display, FromStr}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -813,7 +814,7 @@ impl SketchSurface { /// Start a sketch on a specific plane or face. pub async fn start_sketch_on(args: Args) -> Result { - let (data, tag): (SketchData, Option) = args.get_data_and_optional_tag()?; + let (data, tag): (SketchData, Option) = args.get_data_and_optional_tag()?; match inner_start_sketch_on(data, tag, args).await? { SketchSurface::Plane(plane) => Ok(MemoryItem::Plane(plane)), @@ -825,7 +826,11 @@ pub async fn start_sketch_on(args: Args) -> Result { #[stdlib { name = "startSketchOn", }] -async fn inner_start_sketch_on(data: SketchData, tag: Option, args: Args) -> Result { +async fn inner_start_sketch_on( + data: SketchData, + tag: Option, + args: Args, +) -> Result { match data { SketchData::Plane(plane_data) => { let plane = start_sketch_on_plane(plane_data, args).await?; @@ -838,26 +843,72 @@ async fn inner_start_sketch_on(data: SketchData, tag: Option, args: Args source_ranges: vec![args.source_range], })); }; - let face = start_sketch_on_face(extrude_group, &tag, args).await?; + let face = start_sketch_on_face(extrude_group, tag, args).await?; Ok(SketchSurface::Face(face)) } } } -async fn start_sketch_on_face(extrude_group: Box, tag: &str, args: Args) -> Result, KclError> { - let extrude_plane = extrude_group - .value - .iter() - .find_map(|extrude_surface| match extrude_surface { - ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == tag => Some(extrude_plane), - ExtrudeSurface::ExtrudePlane(_) => None, - }) - .ok_or_else(|| { +/// A tag for sketch on face. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)] +#[ts(export)] +#[serde(rename_all = "snake_case", untagged)] +#[display("{0}")] +pub enum SketchOnFaceTag { + StartOrEnd(StartOrEnd), + /// A string tag for the face you want to sketch on. + String(String), +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)] +#[ts(export)] +#[serde(rename_all = "snake_case")] +#[display(style = "snake_case")] +pub enum StartOrEnd { + /// The start face as in before you extruded. This could also be known as the bottom + /// face. But we do not call it bottom because it would be the top face if you + /// extruded it in the opposite direction or flipped the camera. + #[serde(rename = "start", alias = "START")] + Start, + /// The end face after you extruded. This could also be known as the top + /// face. But we do not call it top because it would be the bottom face if you + /// extruded it in the opposite direction or flipped the camera. + #[serde(rename = "end", alias = "END")] + End, +} + +async fn start_sketch_on_face( + extrude_group: Box, + tag: SketchOnFaceTag, + args: Args, +) -> Result, KclError> { + let extrude_plane_id = match tag { + SketchOnFaceTag::String(ref s) => extrude_group + .value + .iter() + .find_map(|extrude_surface| match extrude_surface { + ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => Some(extrude_plane.face_id), + ExtrudeSurface::ExtrudePlane(_) => None, + }) + .ok_or_else(|| { + KclError::Type(KclErrorDetails { + message: format!("Expected a face with the tag `{}`", tag), + source_ranges: vec![args.source_range], + }) + })?, + SketchOnFaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| { KclError::Type(KclErrorDetails { - message: format!("Expected a face with the tag `{}`", tag), + message: "Expected a start face to sketch on".to_string(), source_ranges: vec![args.source_range], }) - })?; + })?, + SketchOnFaceTag::StartOrEnd(StartOrEnd::End) => extrude_group.end_cap_id.ok_or_else(|| { + KclError::Type(KclErrorDetails { + message: "Expected an end face to sketch on".to_string(), + source_ranges: vec![args.source_range], + }) + })?, + }; // Enter sketch mode on the face. let id = uuid::Uuid::new_v4(); @@ -866,7 +917,7 @@ async fn start_sketch_on_face(extrude_group: Box, tag: &str, args: ModelingCmd::EnableSketchMode { animated: false, ortho: false, - entity_id: extrude_plane.face_id, + entity_id: extrude_plane_id, }, ) .await?; @@ -1645,4 +1696,43 @@ mod tests { let data: PlaneData = serde_json::from_str(&str_json).unwrap(); assert_eq!(data, PlaneData::NegXZ); } + + #[test] + fn test_deserialize_sketch_on_face_tag() { + let data = "start"; + let mut str_json = serde_json::to_string(&data).unwrap(); + assert_eq!(str_json, "\"start\""); + + str_json = "\"end\"".to_string(); + let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); + assert_eq!( + data, + crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End) + ); + + str_json = "\"thing\"".to_string(); + let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); + assert_eq!(data, crate::std::sketch::SketchOnFaceTag::String("thing".to_string())); + + str_json = "\"END\"".to_string(); + let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); + assert_eq!( + data, + crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End) + ); + + str_json = "\"start\"".to_string(); + let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); + assert_eq!( + data, + crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start) + ); + + str_json = "\"START\"".to_string(); + let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); + assert_eq!( + data, + crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start) + ); + } } diff --git a/src/wasm-lib/tests/executor/main.rs b/src/wasm-lib/tests/executor/main.rs index c898d7c3c..ae84f4d78 100644 --- a/src/wasm-lib/tests/executor/main.rs +++ b/src/wasm-lib/tests/executor/main.rs @@ -91,6 +91,62 @@ const part002 = startSketchOn(part001, "here") twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face.png", &result, 0.999); } +#[tokio::test(flavor = "multi_thread")] +async fn serial_test_sketch_on_face_start() { + let code = r#"fn cube = (pos, scale) => { + const sg = startSketchOn('XY') + |> startProfileAt(pos, %) + |> line([0, scale], %) + |> line([scale, 0], %) + |> line([0, -scale], %) + + return sg +} +const part001 = cube([0,0], 20) + |> close(%) + |> extrude(20, %) + +const part002 = startSketchOn(part001, "start") + |> startProfileAt([0, 0], %) + |> line([0, 10], %) + |> line([10, 0], %) + |> line([0, -10], %) + |> close(%) + |> extrude(5, %) +"#; + + let result = execute_and_snapshot(code).await.unwrap(); + twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_start.png", &result, 0.999); +} + +#[tokio::test(flavor = "multi_thread")] +async fn serial_test_sketch_on_face_end() { + let code = r#"fn cube = (pos, scale) => { + const sg = startSketchOn('XY') + |> startProfileAt(pos, %) + |> line([0, scale], %) + |> line([scale, 0], %) + |> line([0, -scale], %) + + return sg +} +const part001 = cube([0,0], 20) + |> close(%) + |> extrude(20, %) + +const part002 = startSketchOn(part001, "END") + |> startProfileAt([0, 0], %) + |> line([0, 10], %) + |> line([10, 0], %) + |> line([0, -10], %) + |> close(%) + |> extrude(5, %) +"#; + + let result = execute_and_snapshot(code).await.unwrap(); + twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, 0.999); +} + #[tokio::test(flavor = "multi_thread")] async fn serial_test_execute_with_function_sketch() { let code = r#"fn box = (h, l, w) => { diff --git a/src/wasm-lib/tests/executor/outputs/sketch_on_face_end.png b/src/wasm-lib/tests/executor/outputs/sketch_on_face_end.png new file mode 100644 index 0000000000000000000000000000000000000000..c51b5d31233eecc5f9851fb8ed407964a6fd7c89 GIT binary patch literal 35620 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 literal 0 HcmV?d00001 diff --git a/src/wasm-lib/tests/executor/outputs/sketch_on_face_start.png b/src/wasm-lib/tests/executor/outputs/sketch_on_face_start.png new file mode 100644 index 0000000000000000000000000000000000000000..894b3db105c7d9444002c122bd67f0b37854dd78 GIT binary patch literal 37332 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¢LTZ~^7T_#^dlo>r(H_k^#o z+xIFZ5!|KCt&{bPrF<}1@LI|TlicOsFds~w_a$i}Y2ImamP|2eN`CuHaX@(#+*IuE zm7Bj2Rq^;b{KRwBNRP08^t1RgXsRjk*X>ApNXlvcgd4CB=el}$*_;{^-&hw|MKlzJgN*~|K+2EtIKII83q*~DDl>4}k z#JK2?c=`=zEA{|xV1F`xA(r>4^6|04mHyd>?BqPn8OVHmoAdAUi~2BL+laro_tg{6 z+!r5Lyk67$i=xu0qG}W+PXF@A>0ka*Q5b#;#ErQ}u6^>zo=C&q$Q3A6oOAFe1AmAI z_J;P{we)!2rN_am^U$TNTs<30l3vnm9Q_SH*bB zfx|tYqdaLBRTYvC7C1w9{Mo#mf+VUE{9}nn@N)hU-icmk$M2x-b?$$%pS<{V+b9cwh9>&*Qi?W8nmU)Q2cRt{3(B6T*||N+1^Co1=lIAC`@WWr{LW+io}#qQIv<| z%_tB4i?bZ1O5)+q>hp~5+cWwA)RuL4v~v6w9(KFUTh)|K^^&CklLVlCm-LO?_->VUEVUM@g4PlyEn(6UCaHzp->}R*o9jBI{oubleR_ufDmFdx K;8B