From cfbc77b62f275aae3fc3f4c9be90eeb3f81a559a Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Tue, 13 Feb 2024 10:26:09 -0800 Subject: [PATCH 1/7] 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 Date: Wed, 14 Feb 2024 05:35:05 +1100 Subject: [PATCH 2/7] cost part001 = startSketchOn(..) should be undone . . . (#1404) * undo sketch if no lines have been created * fix sketch axis bug * fix wrong event origin bug * race condition on animation ending * remove logs * codespell --- .codespellrc | 2 +- src/App.tsx | 25 -------- src/clientSideScene/clientSideScene.ts | 3 +- src/clientSideScene/setup.tsx | 74 ++++++++++++++-------- src/components/ModelingMachineProvider.tsx | 12 ++++ src/machines/modelingMachine.ts | 57 +++++++++-------- 6 files changed, 94 insertions(+), 79 deletions(-) diff --git a/.codespellrc b/.codespellrc index 1325ca343..df795912f 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,3 +1,3 @@ [codespell] -ignore-words-list: crate,everytime,inout,co-ordinate +ignore-words-list: crate,everytime,inout,co-ordinate,ot skip: **/target,node_modules,build,**/Cargo.lock diff --git a/src/App.tsx b/src/App.tsx index c699f5b7e..e05378328 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -119,31 +119,6 @@ export function App() { }, cmd_id: newCmdId, }) - } else { - const interactionGuards = cameraMouseDragGuards[cameraControls] - let interaction: CameraDragInteractionType_type - - const eWithButton = { ...e, button: buttonDownInStream } - - if (interactionGuards.pan.callback(eWithButton)) { - interaction = 'pan' - } else if (interactionGuards.rotate.callback(eWithButton)) { - interaction = 'rotate' - } else if (interactionGuards.zoom.dragCallback(eWithButton)) { - interaction = 'zoom' - } else { - return - } - - debounceSocketSend({ - type: 'modeling_cmd_req', - cmd: { - type: 'camera_drag_move', - interaction, - window: { x, y }, - }, - cmd_id: newCmdId, - }) } } diff --git a/src/clientSideScene/clientSideScene.ts b/src/clientSideScene/clientSideScene.ts index f82189f6c..b291e76f2 100644 --- a/src/clientSideScene/clientSideScene.ts +++ b/src/clientSideScene/clientSideScene.ts @@ -241,12 +241,13 @@ class ClientSideScene { engineCommandManager, programMemoryOverride, }) - this.sceneProgramMemory = programMemory const sketchGroup = sketchGroupFromPathToNode({ pathToNode: sketchPathToNode, ast: kclManager.ast, programMemory, }) + if (!Array.isArray(sketchGroup?.value)) return + this.sceneProgramMemory = programMemory const group = new Group() group.userData = { type: SKETCH_GROUP_SEGMENTS, diff --git a/src/clientSideScene/setup.tsx b/src/clientSideScene/setup.tsx index 64e95c0f9..5ab174d44 100644 --- a/src/clientSideScene/setup.tsx +++ b/src/clientSideScene/setup.tsx @@ -71,12 +71,14 @@ interface ThreeCamValues { isPerspective: boolean } +const lastCmdDelay = 50 + let lastCmd: any = null let lastCmdTime: number = Date.now() let lastCmdTimeoutId: number | null = null const sendLastReliableChannel = () => { - if (lastCmd && Date.now() - lastCmdTime >= 300) { + if (lastCmd && Date.now() - lastCmdTime >= lastCmdDelay) { engineCommandManager.sendSceneCommand(lastCmd, true) lastCmdTime = Date.now() } @@ -98,16 +100,57 @@ const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => { if (lastCmdTimeoutId !== null) { clearTimeout(lastCmdTimeoutId) } - lastCmdTimeoutId = setTimeout(sendLastReliableChannel, 300) as any as number + lastCmdTimeoutId = setTimeout( + sendLastReliableChannel, + lastCmdDelay + ) as any as number }, 1000 / 30) +let lastPerspectiveCmd: any = null +let lastPerspectiveCmdTime: number = Date.now() +let lastPerspectiveCmdTimeoutId: number | null = null + +const sendLastPerspectiveReliableChannel = () => { + if ( + lastPerspectiveCmd && + Date.now() - lastPerspectiveCmdTime >= lastCmdDelay + ) { + engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true) + lastPerspectiveCmdTime = Date.now() + } +} + const throttledUpdateEngineFov = throttle( (vals: { position: Vector3 quaternion: Quaternion zoom: number fov: number - }) => updateEngineFov(vals), + }) => { + const cmd = { + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_perspective_settings', + ...convertThreeCamValuesToEngineCam({ + ...vals, + isPerspective: true, + }), + fov_y: vals.fov, + ...calculateNearFarFromFOV(vals.fov), + }, + } as any + engineCommandManager.sendSceneCommand(cmd) + lastPerspectiveCmd = cmd + lastPerspectiveCmdTime = Date.now() + if (lastPerspectiveCmdTimeoutId !== null) { + clearTimeout(lastPerspectiveCmdTimeoutId) + } + lastPerspectiveCmdTimeoutId = setTimeout( + sendLastPerspectiveReliableChannel, + lastCmdDelay + ) as any as number + }, 1000 / 15 ) @@ -481,7 +524,7 @@ class SetupSingleton { const targetFov = 4 const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN - let frameWaitOnFinish = 5 + let frameWaitOnFinish = 10 const animateFovChange = () => { if (this.camera instanceof PerspectiveCamera) { @@ -1242,29 +1285,6 @@ function calculateNearFarFromFOV(fov: number) { return { z_near: 0.1, z_far } } -function updateEngineFov(args: { - position: Vector3 - quaternion: Quaternion - zoom: number - fov: number -}) { - engineCommandManager.sendSceneCommand( - { - type: 'modeling_cmd_req', - cmd_id: uuidv4(), - cmd: { - type: 'default_camera_perspective_settings', - ...convertThreeCamValuesToEngineCam({ - ...args, - isPerspective: true, - }), - fov_y: args.fov, - ...calculateNearFarFromFOV(args.fov), - }, - } as any /* TODO - this command isn't in the spec yet, remove any when it is */ - ) -} - export function isQuaternionVertical(q: Quaternion) { const v = new Vector3(0, 0, 1).applyQuaternion(q) // no x or y components means it's vertical diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 32f03713f..65d39a8a1 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -36,6 +36,7 @@ import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConf import { setupSingleton } from 'clientSideScene/setup' import { getSketchQuaternion } from 'clientSideScene/clientSideScene' import { startSketchOnDefault } from 'lang/modifyAst' +import { Program } from 'lang/wasm' type MachineContext = { state: StateFrom @@ -189,6 +190,17 @@ export const ModelingMachineProvider = ({ }, }, services: { + 'AST-undo-startSketchOn': async ({ sketchPathToNode }) => { + if (!sketchPathToNode) return + const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast)) + const varDecIndex = sketchPathToNode[1][0] + // remove body item at varDecIndex + newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) + await kclManager.executeAstMock(newAst, { updates: 'code' }) + setupSingleton.setCallbacks({ + onClick: () => {}, + }) + }, 'animate-to-face': async (_, { data: { plane, normal } }) => { const { modifiedAst, pathToNode } = startSketchOnDefault( kclManager.ast, diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 97b7910fb..6a12256d6 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -116,7 +116,7 @@ export type MoveDesc = { line: number; snippet: string } export const modelingMachine = createMachine( { - /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMARhoA6GgE4A7COEBWGoIBMswQBoQAT0TDBANn1j9NAByTlw08tODTAFlMBfJ5rQYc+AgGUw7AASwWGDknNy0DEggLGxhPFECCMIK8mLSyvL2gvKmwvr29vqSmjoIBvKSYgoFwsr2MpLyLm7oWHhQYtgQmGA+foHBoVy4EbwxHMO8ifryqTNFyvqmNLX2NPYlulZGCjSGohlF+oLNIO5t+J3dvQCiuOxgAE6BANZ+5AAWo1HjcVOI0mE0mMkhkwns0ms6wymwQ0nWYlM8MkVmkFnB1lO508HS6PQIdwez1gb3YnyowkizFYE24-zK1jE8kEKP00mk9jqBmEsMk+Sq6zWCkM5mUJ1cZ1aOKu+JufHYjwArhhvtTYpMEohbKYxJYOTlpIpcspYclTDtFJDdrMaDksVL2mJvKTPsQyJRMM73l96GMaX9NQhlHVKszbVlhBZ5ApTUa0go6gd9BkmhLsY6vWSPk6XR8AJLXPoBII9IbhX0-f0a0CJOpIiQVQXpNGFU3ZSomQSCPaiAoGfT2jwZ3M570F-HIEhvAZQAC2YHu-gAbk9OOQSJhVdEq3TA2LMsZbZJOWj9Bj9KaOeJzPlZhlAct7IOLh1M59R1nx71J9OgnOFwEHyoI82AAF7cOwG5br81b8IgYp2BIIjwjQwaOOYprHJU4KWCI2SZPIwbPtKb7ZqRX7ENwsAKiQeD+EBIHgfcG7+BA2DUe6YDQTu8Q1vByFiIUmTrMe8JrPIsKqGKxjRpI3ZWNkwjEcO3ofp8FFEFRNF0SujxrixbEcRQXEVmqtK8XBQZdsoYiCIax5ssyyh7JJJjCFUzlnvYih5Mm0jKZcpFqfmhaabg1GPLRuD+AAggAQt4-gABrcequ58VZjiIiYZ40Osgj2OCsIqIyswqFIhpKPCA5pg6gUjuRoVaZFdHxYlACaqXmfSYpZYYhHyNIfnsqYxWFUYKiRrkXYiIVAWvg1uYac1UX+GQUA9F1AYZXUeyInsFgGJISh5dIkm2PYiI2jQHK5JG-m1UO9WqY1+JhRFq09Pg7A+lS25pRZtZrOIkJItCBRZEskkObZMwOENCwFfNwXBct4XadFTBPFjuBseQiqYCQzyGZBxlbbBtbKCiYgWLthp6JI1gaNoQg4bZeQqBy6EFA9LRPQtL1LU16MtdFDFgRBG6YFoa04FAIymf93V7ioRg2Aoj6DYRo0s0kDiCIitPst5kYmE+j0vijr29O9GPLqu2Drpg0uy9g8vk+llm9TZpizDIhQ2NGZ265GzJVOawYODddhZMjQXW5RIurbAuAkEw-jsKgyUe4D-FDcY5rHWsx0GDdklHYiwZSMc3aLBYceLWOwsfXRKdpxnWedYrMGe7WdlGPUx61A4tMXrrdSGlUJuzPk2wKA3gtN29K10WAACOiosV9UA-TnPXZEYcmQrdQJ2Zkkm2jqqFslTkLVzVfOW-HQvL0ndFMETUvUN3PH78dgneXSGyW0zJijj0WJdZMjQUSAmjLaB+kp+ZWxfjbFe0VHhgFnKgFc-hyBoPYLAPeKtahIVmNGFkyYdalEsDkYwehsgrEGo4cUj8SKN0-IWAASmAAAtGAPgIRFQPCITtcaTIbQZAKksdkppnKVDkoYGQLJahdgXlmVGhYbgb2wOnAAMngMAHdUCbh-gDHqfJ5E2GOFkJEjQgSmnDEhRo0ZHyGCKGo98CctGKh0RndaAFsAsSJuQDuIjLIqBEPtJEiwhqgkGqaTklRyhKBtJCLIqZWEqXUTFAA7rRQCwEJbMUwKxdipNKD+DwAAM1QAQCA3AwCdFwEuVAbwxAwHYDw8WTFIKYB4dU1AYTEhdjZNlRo6xfZyLAdQjkwIChRhsAozIHjsy5PyfRQpPSDJlM4pU3ANSCBPEeMBMQTBCbsBqY8Wc7S-BdM2ZLPpAyhlCAMDqZklVbwcmSBsceyJjB2ROjdWBzgLZsNUmsjg9s9KO22UZCpAy6kNKaS0tpHSeG6X0o8-ZgzTHKwyl2Se8gTDHUyGiWaPzqFnnchCPIuRfaGiKObTJz1sl5MhRimFJSSa7IRUck5ZySAXOAtctFHKnb9Oxc8so-ddQ2F9pyWxyZJJEsqENZyk1ahLCJSssQEKAhtWSnsg59TcCNLwCixpaKSAACNYA8L4BKmpUquzLHDooJRMx4Qxl1iVVVrzCgKHhHUDJiCn4jj1bFBKhreWPGOY8U55zLkituTau1Dqnm4u2uEuw14VXH2vkUCSPquQ0xtNYawKIA46ojQa9qRrakmrNc01plqU22p4VoR1OK-o91ztKtEaRQZwMKvUMepQuyNAkI0HN3Z9R6GrWy-VUa60xrjQmwVSabmdNTR2rtzqc2CUtBEtJNcxrUzcVIe6g13Ggqye+CN60ej1sRaa5FLat08MfbwjNPbf6BhdSDOoYoqrOWbOdBwTI67dlMMsRYIb0wsvvYuta+An2rv5Ym4VH6v17szRTF5jJajpDFAhFYBVzoFDSFIUEoIOQWCZaGsFrL1nbx+s+xtb7UW3NYx8XDv6zH-rsjqX2mRRTmhUAUYqKwbIzH9vCc0og5ILpYwuHeHxn18vjQKoVVyP08b436AT+K7Khl6k5ZYFQKXwRkOIDI5hIzORWLIZTkKsaPBxnjAmRNSlwsMQijj5r31orcx5x2XnHg8O5cZAzlYjPZsWLZXIhUlGOGhMVBMNM2Q3giaPFzAQQsLk84TYmOzjIadjRhjdWHgvY0K2F4rkXSuUBi2ZLNwzVaJfBPdAqj4TQ+t7JlyEfIcvBgQQhgW6j9GmqMZgIsAxSxxGdVJREik9AKX9rCZMl00TBjsEovKyzb2IezFNwxmdjEEm0enUmMB7iBJKcE0JeHe5CFsMCa9oITCoSc0W0oehbRIVQkCLILIZA6tOzNsQeZcAcAIPuvIaR3mGlvhUc+usaO6gjqoc09QHDKHBwYyH0PYcUn43i7NhFBJWGjmeYSzIHEyCZKIctZhi4ZAJ9N87mAxC4GFRuObJYQiLee32-CNk7AQOA5GIFppkjAmPLYIlyYbT1yOxN98EOudiAAHJZwAAqoDwAQggMUIAQECJBPS-gWBG+dekHUJVDTeSFOaXkjOlDJkWMsCJ0YdXE-YHDkXPUbryLMGiJEp8RCxjPBIfIICT1nnx2rlG-u4eUkM+T2syRUh4Ro1zFRmFIydfZDkI0x0xt1XV9mAAKv4u7QTHghMzgLwYwuydtaEEUA2tRwRF1wpaaPlRCo2HqLIcwswdW1-wAEhvTfaneN8Zr1AxjnUzBz9JvKXucptgBydPUIPaMrP8Lz-wVSSCUD6At1iYAz8E3y4TU1UrK1IQszRwoSwrMIGnUyQogoVBEu1mRjIGwFnEFXaA7mtwf16AC2bTaWANAIeB4Uzh4TP0oClTyHBEPDsENGjG9mZlKGcRpjDy+y6wDSAJhwQPAMzkgLIF6F8GLFb2GHQK7FSGZCJUsBuipkaDHS1EnlkGciPU4McHIJALAPwAgP4TKXAJJG9BfSbQtTEHgMFV4WQJkKzHQKgVIUVyUHSTklhA5BsmPEOi8hyBqglF5wwHgCiHGwzw7yDGTCnVkGzyUFUC7EknSDSFpiOlHTDHg0rxlDAFsPwwQBHSulwM1T8kKE-1qCkA8nSG8g9yKEkBWSCJeyDCUHcmOlsQjzUGSGKhMF1FHlQlrj5D8KQWfiXkCNi0z3ggDgLjRDMHMCEkMFjCL1kDIxTGyBg2SOTyCgjW6QeR83KT82xVSL7SpkiRx2jmsEcAsHcKLyHnaJZH7AcDyyhUxSGJ5VGOqLsKpm8iZFsGZ3yAcCI2VXZGME8mSGlwVyT2ZSr11WQwNSSnrTGPMSJSZD2DQghE5FG2KgyEqFtEmiYXWDITWNrReJ2OCKplmFLQTCKA5j1GKhZB9ksEZn4JUEWDWK-QhNayhOgQbDwl9lEBmB5HHlsAHmckDmsm+15kYzvVWWQx4xxKVl2O-2sAvTkgxFsGhn5DsBOKvDsDYLWIK1xnq28yi3hW2NxLSKpmcl1FNhSSBDMF+1ZliJEEoRg2WEjBYTpOOzECX2MVeP-V8kEj5FS1ykZhkFhHJKZAMFUDyFKOvQ5zO2X2539yNPxUsEulHXNMU3MGDj+xWFVWGkhgKhqGdMh15yuQ3A9PCSJXEGOnBgcz7B4KSFQgBIsGTG7DskWHWAjK11138ANyNysOlNF3aPjFnjXyrj0FlwByyEKBHkGhukOzuJTxh3YFjKz2kj2CJJREUHIVjDMHjEIkKmvhTEnzr04Fnw7i7M7yylDicJZB2xchDhM2yndQrXqGriPxP1QKqLLPpGOB2H9mWC5h5ioUQEaEMNBD0EWAWDkREMoPEOoIFVNTnKSGDDmTyh-IOiAz6wILDmODDD8lUCPCfLEKgAkL4CkPELUM+A-NqEUCoykFmHqGB3wIBARAqkKHZFvMWHFBcCAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBDOFJYWVlQ2UlY3UrQsQFUQVy0Q00w3FLGVNTBtcQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iZCdKiWZpcQKEyHTKmTLrYrCGb5DRaGHKBGiYzKRoXZqeNodLoEB5PV6wD7sb5UQyRZisMbcQEIQRLGiSdRmfIqUxLCpw5QqcrQ4SGTTWUx5bGXPE3Ql3PjsZ4AVwwv1psXGCSEMjkkhkNFE-MNBxo4vEcM22wqOQxikMKwUktxrUk3nJ32IZEomFdnx+9BGdIBmoQdoUykkxmEyIhELMsjhobKoYUdsOIq2jo8zp9FK+LrdXwAkrcegEgl0BuF-X9AxrQIlDHspPzRKYbAsttYzfojCYZBYaEs5uCaCZuZmrm0c99877i4TkCQPn0oABbMCPfwANxenHIJEwquitYZwaspiT1U0c00qQKPZDGgOKVHcgk+SUGgn0uned-8+6RdlyCNcNwCL5UGebAAC9uHYA8j3+Ot+CMJFJFUMN9TtfI0RkBNxDtcoNHkSx+WMCoZG-bMC1nXMAOIbhYAVEg8H8CCoNgx4D38CBsCYz0wEQk94nrVDdgsJsTU-C91DhNJuQsCpRFHYiLwOKjrl-WjvnoohGOY1id2ePduN4-iKEE6s1XpESUJDGQlkkcUZE2RYnzUPY5M5cprBFDQEWEBzdg0qcaP-Es9NwJjnhY3B-AAQQAIW8fwAA0hPVU9RPszZJByMRG1Oc80jk8UNCcg1FFfVtajOJos00sKC10-SYtYpKUoATQymzGSsXLrDsZQ5EMA0DTwh9DnUUElMWNQoy-c4pWo31tKLCLWti-wyCgLoeqDbKDhyaQlCveQU2EUw5OUxEtnkdE0ROYQQrWtaWqigy4q6fB2D9Glj0y2yGxKGYVANE0Dn2UQ5IWbY5jbQxLujFRnqWp1GtW8LCUi6KtqYF58dwXjyEVTASFeMz4Is-bkIbbVthsIrrEsTDlDklNyuFKpTARaxkTEF6tKx7occ+tjIJguCD0wPRtpwKAhisgHerPBz+wtLQJGIjJ+QTDFw2OLZ-MCsR+TqnEGtCzHmo2j62rioyTMwGW5ewBWaayuyrEuvLZEccVFjMDEE2GkEth5qolmORxzeWjHcze23cdY2BcBIJh-HYVA0o9oGjAIqQHJHBQeeqMw5OyfttQNeZVC2QLKLRy3XuFhi7a21P08z7PuqVpDPeB59MjIhFxGFbI5MbGZdkcOQLyjZQckFpq5yTsWwAAR0VbjvqgX7c7644pAxY2ef1OQx4TWpWRyExR31ZQDVMZfrdX7HNtYphyel6g++EvrZAzFSEXUapwnxRgTKcIBpQFgnEyGoUQL8E6t1FvbfwzwwCrlQDufw5AP6PFgAfVWWh0JWGIsNTQo0dAPhFARCwixbDZBKGoYaSCZytwAEpgEEGAPgIRFRPCIYdOoUgDiGFOKkdIYhg4PnUP2dIqQ0S1yUAsNhf4bayi3tgDOAAZPAYBu6oEPH-QGfVUSCnqLYMQ1CiiBXDMiAi2o1CKPHE3ScLcNH3C0RnKmMBHjYG4uTcg3chF2SGtscEBoCKP0UCIhMxgyg3jrrE5Ei81GSHigAdxYuBCWnF4KYB4nxKmlB-B4AAGaoAIBAbgYB2i4C3KgD4kgYDsEEOxSWXFMCCAqagUJiQ0jZDyhoReJpahPVqHJJ85hNguX2MoWwlV0lZJyeLDiUtCmUwEmU3AlSCAvGeJBSQTAybsEqc8VcLS-DtLyRsnpuy+kmJVtlQZII7TIlTMRFyPM5Lgn7NkQco46g4UMMs7JHBty7mwPuTZxTtm9OqbU+pjTmmtMEI7aFB57mVP6YgfY-YDi2hMGicR+QvJKDZGIOQdhSiBTBasjFMKinmVKQig5RyTkkDOZBS5aLGVYt6bihA+K2T6jEPPI2Xk9gRkfqoNIcj2T0ohR1NKOy9k1NwHUvAKK6lopIAAI1gIIPg2LHn-X7nnYV0TZjDVfBfLYpVhoRgyE+Reshhq1CVQEFVqU1VVPZc8Y5pzzm8uuQao1JrBVPIOmEgiMwcjgiUK2fUWhSqLGkFsR+BFshRmfm4n8NEVnKuSv4TqfrEWauRU03VYbDWCD0KaoV8yM3yAqLYKBxE4SHDRHlbmyktiHGCvmlaCci3epLWWtlzxDmBs5dyi5Vy2nhvrY26NtM8VxosDhMeDl+Suq7W5CwCz5jV0JY3eq7itJju2vgLo5aNVaoadWxdggdpdFXea-+wZ9ig0sAcPIrYA7XQNE5Qdf6KhhgRF6m9u0DFTpnUGrlIaX1vu4VGz9pjv0EThtkfqVg1Dz2uojCM-6shyHPmiaDu9fr3qRdq59aLqNfA-QGTDLyCIghyPJcBew7ww1yoOWlKhEZWCoxuPeXxy0BsQ-O0NbSmMsZrGx2N6bjAmC2Isa8E0igOWlSelE-l7oGmg-jZ4hNiak3JsykpcGHkVsfTql9pnzPQss88QQWyLKKesjGgZasKpKGsMNBy1RoYPjmSCMejZ5iAYA7HdGVtR3goCM5jcFmyYUzhRZKT06OXBp5U5gmaXXMZY81lyg3nla+bxf58UgWRnaijmFnTAVpDChGY-E4cX0l6M1YYzApY+gVjiE2vU5gnx6hFJkdMJVws8wjHVk01hMLVB6-o-rRJvGZx2mBAJhSgkhLXQPPFCJyqjTMKOdQ3M2YPh5vGkGjgsiuIvQW1avWDFZyMZIQsuAOAEFGxIaQ3aITpn8pfB8JL0Il0uhzfUjhQXDvjjOd7-Xvu-fYP96krHnlhJctsE4NhFDKT9ioOE8MnKNgcHUbkpo1t9c+5gSQuAeUHkG+WEII2juWuW-j1suxhTQ7DGTmEGbSULL-XaPNL2R3I-WwzyQAA5bOAAFVAeB2CwAIPFCAEBAjwWMv4Fg6um3zHKjCMabYXKDg0HyBZvsI5ZEUKddJP2-tCoSfYzYWQVDnxNDbmhYI8pKIcoC3TCPpdI7zK7jHVIMM44bFhNkMIYQjNkKORsCZjiJM2OkBYunThYkR4lmcAAVHb-jAnPGCVnNn-ROdx+q8K4a5gFlKFNEt44menxsjcnqSwxEMxF9emX-Au3K-V6qXcLbKOGcm+RL7TImxxSDm04gNs4ZeZpBItbyjQ+tKKiJtnfixlfwAHlcD2arc0+K3gS+CAPzUwQx-2Bn8Vg39dIYoQVQWhiem+o4RGyzALRhguRkJPhqL+DM7+DlIkCUA9DDY8RgAwGkwpZkyapCqCCFQnTE4YiIzEQERXQQ46htjcjRZHAVCowR5tBkDYCrhcqtDdyG5oHdAPpX51I0F0FPCCBZyCAwGUBCoVCXiL4ArAKwhEERJWDMLfJRjqAvQcH0H4CMGcqaq17DaDBNqBQkbUqtgYQQIPgzwRh1DDRqb8g1ByG-acEMFZz+C8LFIMFki+iX70bNLyFcE8EOG5gCGPykKcgnDahpAIhwhPiRZ5AQiLx2ANaaB04faoBfZK7+Cq7q6a6kAWTGLv7HbCpja9piCHA5CIwmBwinDqziKKATbupVAuDnDM4YDwBRBxxQDY6N6CAg7SByCxIYSaA2JCDiQQjTI8x6gORIyIJD4EhgCNEf7MgkKVQ8zgjETXjmgpjlBTTnaSGWBS4WyXoFjjEZHcgMxe7Eq+5thdr9gLKVCHBRaOD8jpLCzbGWoHAnC6iLwphjyhZ1DxLJAHBjypDVAXi-HQYdL5KmTla2aVK3F9RhgMwZClzCg1CpC-LJDAhQjzBWLh4bGvZJYMpQpMqeasoPJglnhyIyrp4iLN6GBeTKRbpPgcwxYYjQY+p+r4mHQlxSBqCOIF52CjiEE6ZFR5SVSYRzSdh0kToMlKbx5GAlz9ihF5G9Gr7FCDggjVwUGthyLrH1GvTXqoYik+Yf4ijijoRQgJpVB5CnDXRqBsjyoVCqBzBb5iY-SSa9KMlewlw3zqaPwpgF7zAwwCiXTiKXFDFXF76FrJaG5FZEwlZWY4kgmoCOkNibAb7cjKS3jtglByS2izC+mtijSVCF5UGvQz6xGYAxl4r2AWC1SbA2DqBLAAGLGnB3Y1AlC7ColqlaT5lfbR5FnCqjRlCZAlDllVAQl8gfHzDpCp4p4LLRGo7M4XIHgdlijxqjzHDOKyCylZBlDtjiKlAuSmETny7xGJEEKznzCIinCyCNgZDQg3a2IIhsh-IUTuqKDnpoky5R7o4dnkQt7azMzghmAZ40IihJgigpjGjpgOiBmrQj5+KcDj7dyzmAG8yGguSVBcm9h5CPFhg8hZC-7DG5n76H567kwv4Fjn5vkLDxp6gLCEqSEAFbDoSXYqAxa3zYVPmR6QHZx8FjGilNF7BhzpDiKqB1CXTDR8g6hPj2D9paB+zmG0EKFQBKHMEkVSA8zi42DyD4aXmIDDThgmDqBzAzxTSUHMXUEWEyWMG2FMT2FbGcU6mOC6gIiYRjzcg0pdpzCCj2AQhRiBQXiLQuBAA */ id: 'Modeling', tsTypes: {} as import('./modelingMachine.typegen').Typegen0, @@ -292,7 +292,7 @@ export const modelingMachine = createMachine( }, }, - entry: ['equip select'], + entry: 'setup client side sketch segments', }, 'Await horizontal distance info': { @@ -381,8 +381,8 @@ export const modelingMachine = createMachine( 'Line tool': { exit: [ - 'tear down client sketch', - 'setup client side sketch segments', + // 'tear down client sketch', + // 'setup client side sketch segments', ], on: { @@ -427,6 +427,8 @@ export const modelingMachine = createMachine( target: 'normal', actions: 'set up draft line without teardown', }, + + Cancel: '#Modeling.Sketch.undo startSketchOn', }, }, }, @@ -445,11 +447,6 @@ export const modelingMachine = createMachine( }, 'Tangential arc to': { - exit: [ - 'tear down client sketch', - 'setup client side sketch segments', - ], - entry: 'set up draft arc', on: { @@ -461,6 +458,14 @@ export const modelingMachine = createMachine( 'Equip Line tool': 'Line tool', }, }, + + 'undo startSketchOn': { + invoke: { + src: 'AST-undo-startSketchOn', + id: 'AST-undo-startSketchOn', + onDone: '#Modeling.idle', + }, + }, }, initial: 'Init', @@ -476,7 +481,7 @@ export const modelingMachine = createMachine( 'remove sketch grid', ], - entry: ['add axis n grid', 'setup client side sketch segments'], + entry: ['add axis n grid', 'conditionally equip line tool'], }, 'Sketch no face': { @@ -619,17 +624,7 @@ export const modelingMachine = createMachine( selectionRanges ) }), - // TODO figure out why data isn't typed with sketchPathToNode and sketchNormalBackUp 'set new sketch metadata': assign((_, { data }) => data), - 'equip select': () => - engineCommandManager.sendSceneCommand({ - type: 'modeling_cmd_req', - cmd_id: uuidv4(), - cmd: { - type: 'set_tool', - tool: 'select', - }, - }), // TODO implement source ranges for all of these constraints // need to make the async like the modal constraints 'Make selection horizontal': ({ selectionRanges, sketchPathToNode }) => { @@ -747,20 +742,32 @@ export const modelingMachine = createMachine( focusPath: pathToExtrudeArg, }) }, + 'conditionally equip line tool': (_, { type }) => { + if (type === 'done.invoke.animate-to-face') { + setupSingleton.modelingSend('Equip Line tool') + } + }, 'setup client side sketch segments': ({ sketchPathToNode }, { type }) => { - if (type !== 'done.invoke.animate-to-face') { + if (Object.keys(clientSideScene.activeSegments).length > 0) { + clientSideScene.tearDownSketch({ removeAxis: false }).then(() => { + clientSideScene.setupSketch({ + sketchPathToNode: sketchPathToNode || [], + }) + }) + } else { clientSideScene.setupSketch({ sketchPathToNode: sketchPathToNode || [], }) - } else { - setupSingleton.modelingSend('Equip Line tool') } }, 'animate after sketch': () => { clientSideScene.animateAfterSketch() }, - 'tear down client sketch': () => - clientSideScene.tearDownSketch({ removeAxis: false }), + 'tear down client sketch': () => { + if (clientSideScene.activeSegments) { + clientSideScene.tearDownSketch({ removeAxis: false }) + } + }, 'remove sketch grid': () => clientSideScene.removeSketchGrid(), 'set up draft line': ({ sketchPathToNode }) => { clientSideScene.setUpDraftLine(sketchPathToNode || []) From 00ede7ec1a616034b8915572ae3dfb10bf039dfe Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Wed, 14 Feb 2024 06:19:52 +1100 Subject: [PATCH 3/7] clean up (#1407) --- src/App.tsx | 5 +---- src/lang/std/fileSystemManager.ts | 6 +----- src/lib/testHelpers.ts | 1 - src/machines/modelingMachine.ts | 2 -- src/routes/SignIn.tsx | 1 - 5 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e05378328..344f1edc4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,8 +24,6 @@ import { type IndexLoaderData } from 'lib/types' import { paths } from 'lib/paths' import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { onboardingPaths } from 'routes/Onboarding/paths' -import { cameraMouseDragGuards } from 'lib/cameraControls' -import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models' import { CodeMenu } from 'components/CodeMenu' import { TextEditor } from 'components/TextEditor' import { Themes, getSystemTheme } from 'lib/theme' @@ -56,8 +54,7 @@ export function App() { })) const { settings } = useGlobalStateContext() - const { showDebugPanel, onboardingStatus, cameraControls, theme } = - settings?.context || {} + const { showDebugPanel, onboardingStatus, theme } = settings?.context || {} const { state, send } = useModelingContext() const editorTheme = theme === Themes.System ? getSystemTheme() : theme diff --git a/src/lang/std/fileSystemManager.ts b/src/lang/std/fileSystemManager.ts index d407ee2fb..defeba8e8 100644 --- a/src/lang/std/fileSystemManager.ts +++ b/src/lang/std/fileSystemManager.ts @@ -1,8 +1,4 @@ -import { - readBinaryFile, - exists as tauriExists, - BaseDirectory, -} from '@tauri-apps/api/fs' +import { readBinaryFile, exists as tauriExists } from '@tauri-apps/api/fs' import { isTauri } from 'lib/isTauri' import { join } from '@tauri-apps/api/path' diff --git a/src/lib/testHelpers.ts b/src/lib/testHelpers.ts index 4076984d3..12af3d177 100644 --- a/src/lib/testHelpers.ts +++ b/src/lib/testHelpers.ts @@ -4,7 +4,6 @@ import { EngineCommand, } from '../lang/std/engineConnection' import { Models } from '@kittycad/lib' -import { v4 as uuidv4 } from 'uuid' type WebSocketResponse = Models['OkWebSocketResponseData_type'] diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 6a12256d6..00436ba92 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -1,6 +1,5 @@ import { PathToNode, VariableDeclarator } from 'lang/wasm' import { engineCommandManager } from 'lang/std/engineConnection' -import { isReducedMotion } from 'lib/utils' import { Axis, Selection, @@ -8,7 +7,6 @@ import { Selections, } from 'lib/selections' import { assign, createMachine } from 'xstate' -import { v4 as uuidv4 } from 'uuid' import { isCursorInSketchCommandRange } from 'lang/util' import { getNodePathFromSourceRange } from 'lang/queryAst' import { kclManager } from 'lang/KclSingleton' diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx index 0a628f6ef..e7ab03485 100644 --- a/src/routes/SignIn.tsx +++ b/src/routes/SignIn.tsx @@ -22,7 +22,6 @@ const SignIn = () => { }, } = useGlobalStateContext() - const appliedTheme = theme === Themes.System ? getSystemTheme() : theme const signInTauri = async () => { // We want to invoke our command to login via device auth. try { From c699611f5b93e211c971c6e44b80714777955bde Mon Sep 17 00:00:00 2001 From: Serena Gandhi <60444726+gserena01@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:20:49 -0600 Subject: [PATCH 4/7] Pull Circular patterns through to App (#1405) * debugging steps * add testing * Update src/wasm-lib/tests/executor/main.rs * generate docs and fmt Signed-off-by: Jess Frazelle --------- Signed-off-by: Jess Frazelle Co-authored-by: gserena Co-authored-by: Jess Frazelle Co-authored-by: Jess Frazelle --- docs/kcl/std.json | 1767 +++++++++++++++++ docs/kcl/std.md | 189 ++ src/wasm-lib/kcl/src/std/mod.rs | 1 + src/wasm-lib/kcl/src/std/patterns.rs | 98 + src/wasm-lib/tests/executor/main.rs | 82 + .../patterns_circular_3d_tilted_axis.png | Bin 0 -> 71592 bytes .../outputs/patterns_circular_basic_2d.png | Bin 0 -> 73531 bytes .../outputs/patterns_circular_basic_3d.png | Bin 0 -> 76160 bytes 8 files changed, 2137 insertions(+) create mode 100644 src/wasm-lib/tests/executor/outputs/patterns_circular_3d_tilted_axis.png create mode 100644 src/wasm-lib/tests/executor/outputs/patterns_circular_basic_2d.png create mode 100644 src/wasm-lib/tests/executor/outputs/patterns_circular_basic_3d.png diff --git a/docs/kcl/std.json b/docs/kcl/std.json index 5ff450f9a..23f593e1c 100644 --- a/docs/kcl/std.json +++ b/docs/kcl/std.json @@ -21033,6 +21033,1773 @@ "unpublished": false, "deprecated": false }, + { + "name": "patternCircular", + "summary": "A Circular pattern.", + "description": "", + "tags": [], + "args": [ + { + "name": "data", + "type": "CircularPatternData", + "schema": { + "description": "Data for a circular pattern.", + "type": "object", + "required": [ + "arcDegrees", + "axis", + "center", + "repetitions", + "rotateDuplicates" + ], + "properties": { + "arcDegrees": { + "description": "The arc angle (in degrees) to place the repetitions. Must be greater than 0.", + "type": "number", + "format": "double" + }, + "axis": { + "description": "The axis around which to make the pattern. This is a 3D vector.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 3, + "minItems": 3 + }, + "center": { + "description": "The center about which to make th pattern. This is a 3D vector.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 3, + "minItems": 3 + }, + "repetitions": { + "description": "The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once.", + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "rotateDuplicates": { + "description": "Whether or not to rotate the duplicates as they are copied.", + "type": "boolean" + } + } + }, + "required": true + }, + { + "name": "geometry", + "type": "Geometry", + "schema": { + "description": "A geometry.", + "oneOf": [ + { + "description": "A sketch group is a collection of paths.", + "type": "object", + "required": [ + "__meta", + "id", + "position", + "rotation", + "start", + "type", + "value", + "xAxis", + "yAxis", + "zAxis" + ], + "properties": { + "__meta": { + "description": "Metadata.", + "type": "array", + "items": { + "description": "Metadata.", + "type": "object", + "required": [ + "sourceRange" + ], + "properties": { + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + } + }, + "entityId": { + "description": "The plane id or face id of the sketch group.", + "type": "string", + "format": "uuid", + "nullable": true + }, + "id": { + "description": "The id of the sketch group.", + "type": "string", + "format": "uuid" + }, + "position": { + "description": "The position of the sketch group.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 3, + "minItems": 3 + }, + "rotation": { + "description": "The rotation of the sketch group base plane.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 4, + "minItems": 4 + }, + "start": { + "description": "The starting path.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "type": { + "type": "string", + "enum": [ + "SketchGroup" + ] + }, + "value": { + "description": "The paths in the sketch group.", + "type": "array", + "items": { + "description": "A path.", + "oneOf": [ + { + "description": "A path that goes to a point.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to", + "type" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "ToPoint" + ] + } + } + }, + { + "description": "A arc that is tangential to the last path segment that goes to a point", + "type": "object", + "required": [ + "__geoMeta", + "ccw", + "center", + "from", + "name", + "to", + "type" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "ccw": { + "description": "arc's direction", + "type": "boolean" + }, + "center": { + "description": "the arc's center", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "TangentialArcTo" + ] + } + } + }, + { + "description": "A path that is horizontal.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to", + "type", + "x" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "Horizontal" + ] + }, + "x": { + "description": "The x coordinate.", + "type": "number", + "format": "double" + } + } + }, + { + "description": "An angled line to.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to", + "type" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "AngledLineTo" + ] + }, + "x": { + "description": "The x coordinate.", + "type": "number", + "format": "double", + "nullable": true + }, + "y": { + "description": "The y coordinate.", + "type": "number", + "format": "double", + "nullable": true + } + } + }, + { + "description": "A base path.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to", + "type" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "Base" + ] + } + } + } + ] + } + }, + "xAxis": { + "description": "The x-axis of the sketch group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + }, + "yAxis": { + "description": "The y-axis of the sketch group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + }, + "zAxis": { + "description": "The z-axis of the sketch group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + } + } + }, + { + "description": "An extrude group is a collection of extrude surfaces.", + "type": "object", + "required": [ + "__meta", + "height", + "id", + "position", + "rotation", + "type", + "value", + "xAxis", + "yAxis", + "zAxis" + ], + "properties": { + "__meta": { + "description": "Metadata.", + "type": "array", + "items": { + "description": "Metadata.", + "type": "object", + "required": [ + "sourceRange" + ], + "properties": { + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + } + }, + "endCapId": { + "description": "The id of the extrusion end cap", + "type": "string", + "format": "uuid", + "nullable": true + }, + "height": { + "description": "The height of the extrude group.", + "type": "number", + "format": "double" + }, + "id": { + "description": "The id of the extrude group.", + "type": "string", + "format": "uuid" + }, + "position": { + "description": "The position of the extrude group.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 3, + "minItems": 3 + }, + "rotation": { + "description": "The rotation of the extrude group.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 4, + "minItems": 4 + }, + "startCapId": { + "description": "The id of the extrusion start cap", + "type": "string", + "format": "uuid", + "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "ExtrudeGroup" + ] + }, + "value": { + "description": "The extrude surfaces.", + "type": "array", + "items": { + "description": "An extrude surface.", + "oneOf": [ + { + "description": "An extrude plane.", + "type": "object", + "required": [ + "faceId", + "id", + "name", + "position", + "rotation", + "sourceRange", + "type" + ], + "properties": { + "faceId": { + "description": "The face id for the extrude plane.", + "type": "string", + "format": "uuid" + }, + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "The name.", + "type": "string" + }, + "position": { + "description": "The position.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 3, + "minItems": 3 + }, + "rotation": { + "description": "The rotation.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 4, + "minItems": 4 + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "extrudePlane" + ] + } + } + } + ] + } + }, + "xAxis": { + "description": "The x-axis of the extrude group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + }, + "yAxis": { + "description": "The y-axis of the extrude group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + }, + "zAxis": { + "description": "The z-axis of the extrude group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + } + } + } + ] + }, + "required": true + } + ], + "returnValue": { + "name": "", + "type": "Geometries", + "schema": { + "description": "A set of geometry.", + "oneOf": [ + { + "type": [ + "object", + "array" + ], + "items": { + "description": "A sketch group is a collection of paths.", + "type": "object", + "required": [ + "__meta", + "id", + "position", + "rotation", + "start", + "value", + "xAxis", + "yAxis", + "zAxis" + ], + "properties": { + "__meta": { + "description": "Metadata.", + "type": "array", + "items": { + "description": "Metadata.", + "type": "object", + "required": [ + "sourceRange" + ], + "properties": { + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + } + }, + "entityId": { + "description": "The plane id or face id of the sketch group.", + "type": "string", + "format": "uuid", + "nullable": true + }, + "id": { + "description": "The id of the sketch group.", + "type": "string", + "format": "uuid" + }, + "position": { + "description": "The position of the sketch group.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 3, + "minItems": 3 + }, + "rotation": { + "description": "The rotation of the sketch group base plane.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 4, + "minItems": 4 + }, + "start": { + "description": "The starting path.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "value": { + "description": "The paths in the sketch group.", + "type": "array", + "items": { + "description": "A path.", + "oneOf": [ + { + "description": "A path that goes to a point.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to", + "type" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "ToPoint" + ] + } + } + }, + { + "description": "A arc that is tangential to the last path segment that goes to a point", + "type": "object", + "required": [ + "__geoMeta", + "ccw", + "center", + "from", + "name", + "to", + "type" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "ccw": { + "description": "arc's direction", + "type": "boolean" + }, + "center": { + "description": "the arc's center", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "TangentialArcTo" + ] + } + } + }, + { + "description": "A path that is horizontal.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to", + "type", + "x" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "Horizontal" + ] + }, + "x": { + "description": "The x coordinate.", + "type": "number", + "format": "double" + } + } + }, + { + "description": "An angled line to.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to", + "type" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "AngledLineTo" + ] + }, + "x": { + "description": "The x coordinate.", + "type": "number", + "format": "double", + "nullable": true + }, + "y": { + "description": "The y coordinate.", + "type": "number", + "format": "double", + "nullable": true + } + } + }, + { + "description": "A base path.", + "type": "object", + "required": [ + "__geoMeta", + "from", + "name", + "to", + "type" + ], + "properties": { + "__geoMeta": { + "description": "Metadata.", + "type": "object", + "required": [ + "id", + "sourceRange" + ], + "properties": { + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + }, + "from": { + "description": "The from point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "name": { + "description": "The name of the path.", + "type": "string" + }, + "to": { + "description": "The to point.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "Base" + ] + } + } + } + ] + } + }, + "xAxis": { + "description": "The x-axis of the sketch group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + }, + "yAxis": { + "description": "The y-axis of the sketch group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + }, + "zAxis": { + "description": "The z-axis of the sketch group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + } + } + }, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "SketchGroups" + ] + } + } + }, + { + "type": [ + "object", + "array" + ], + "items": { + "description": "An extrude group is a collection of extrude surfaces.", + "type": "object", + "required": [ + "__meta", + "height", + "id", + "position", + "rotation", + "value", + "xAxis", + "yAxis", + "zAxis" + ], + "properties": { + "__meta": { + "description": "Metadata.", + "type": "array", + "items": { + "description": "Metadata.", + "type": "object", + "required": [ + "sourceRange" + ], + "properties": { + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + } + } + } + }, + "endCapId": { + "description": "The id of the extrusion end cap", + "type": "string", + "format": "uuid", + "nullable": true + }, + "height": { + "description": "The height of the extrude group.", + "type": "number", + "format": "double" + }, + "id": { + "description": "The id of the extrude group.", + "type": "string", + "format": "uuid" + }, + "position": { + "description": "The position of the extrude group.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 3, + "minItems": 3 + }, + "rotation": { + "description": "The rotation of the extrude group.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 4, + "minItems": 4 + }, + "startCapId": { + "description": "The id of the extrusion start cap", + "type": "string", + "format": "uuid", + "nullable": true + }, + "value": { + "description": "The extrude surfaces.", + "type": "array", + "items": { + "description": "An extrude surface.", + "oneOf": [ + { + "description": "An extrude plane.", + "type": "object", + "required": [ + "faceId", + "id", + "name", + "position", + "rotation", + "sourceRange", + "type" + ], + "properties": { + "faceId": { + "description": "The face id for the extrude plane.", + "type": "string", + "format": "uuid" + }, + "id": { + "description": "The id of the geometry.", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "The name.", + "type": "string" + }, + "position": { + "description": "The position.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 3, + "minItems": 3 + }, + "rotation": { + "description": "The rotation.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "maxItems": 4, + "minItems": 4 + }, + "sourceRange": { + "description": "The source range.", + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 2, + "minItems": 2 + }, + "type": { + "type": "string", + "enum": [ + "extrudePlane" + ] + } + } + } + ] + } + }, + "xAxis": { + "description": "The x-axis of the extrude group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + }, + "yAxis": { + "description": "The y-axis of the extrude group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + }, + "zAxis": { + "description": "The z-axis of the extrude group base plane in the 3D space", + "type": "object", + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "z": { + "type": "number", + "format": "double" + } + } + } + } + }, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ExtrudeGroups" + ] + } + } + } + ] + }, + "required": true + }, + "unpublished": false, + "deprecated": false + }, { "name": "patternLinear", "summary": "A linear pattern.", diff --git a/docs/kcl/std.md b/docs/kcl/std.md index 6d63ed6e0..02e4f24f1 100644 --- a/docs/kcl/std.md +++ b/docs/kcl/std.md @@ -41,6 +41,7 @@ * [`log2`](#log2) * [`max`](#max) * [`min`](#min) + * [`patternCircular`](#patternCircular) * [`patternLinear`](#patternLinear) * [`pi`](#pi) * [`pow`](#pow) @@ -3998,6 +3999,194 @@ min(args: [number]) -> number +### patternCircular + +A Circular pattern. + + + +``` +patternCircular(data: CircularPatternData, geometry: Geometry) -> Geometries +``` + +#### Arguments + +* `data`: `CircularPatternData` - Data for a circular pattern. +``` +{ + // The arc angle (in degrees) to place the repetitions. Must be greater than 0. + arcDegrees: number, + // The axis around which to make the pattern. This is a 3D vector. + axis: [number, number, number], + // The center about which to make th pattern. This is a 3D vector. + center: [number, number, number], + // The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. + repetitions: number, + // Whether or not to rotate the duplicates as they are copied. + rotateDuplicates: string, +} +``` +* `geometry`: `Geometry` - A geometry. +``` +{ + // The plane id or face id of the sketch group. + entityId: uuid, + // The id of the sketch group. + id: uuid, + // The position of the sketch group. + position: [number, number, number], + // The rotation of the sketch group base plane. + rotation: [number, number, number, number], + // The starting path. + start: { + // The from point. + from: [number, number], + // The name of the path. + name: string, + // The to point. + to: [number, number], +}, + type: "SketchGroup", + // The paths in the sketch group. + value: [{ + // The from point. + from: [number, number], + // The name of the path. + name: string, + // The to point. + to: [number, number], + type: "ToPoint", +} | +{ + // arc's direction + ccw: string, + // the arc's center + center: [number, number], + // The from point. + from: [number, number], + // The name of the path. + name: string, + // The to point. + to: [number, number], + type: "TangentialArcTo", +} | +{ + // The from point. + from: [number, number], + // The name of the path. + name: string, + // The to point. + to: [number, number], + type: "Horizontal", + // The x coordinate. + x: number, +} | +{ + // The from point. + from: [number, number], + // The name of the path. + name: string, + // The to point. + to: [number, number], + type: "AngledLineTo", + // The x coordinate. + x: number, + // The y coordinate. + y: number, +} | +{ + // The from point. + from: [number, number], + // The name of the path. + name: string, + // The to point. + to: [number, number], + type: "Base", +}], + // The x-axis of the sketch group base plane in the 3D space + xAxis: { + x: number, + y: number, + z: number, +}, + // The y-axis of the sketch group base plane in the 3D space + yAxis: { + x: number, + y: number, + z: number, +}, + // The z-axis of the sketch group base plane in the 3D space + zAxis: { + x: number, + y: number, + z: number, +}, +} | +{ + // The id of the extrusion end cap + endCapId: uuid, + // The height of the extrude group. + height: number, + // The id of the extrude group. + id: uuid, + // The position of the extrude group. + position: [number, number, number], + // The rotation of the extrude group. + rotation: [number, number, number, number], + // The id of the extrusion start cap + startCapId: uuid, + type: "ExtrudeGroup", + // The extrude surfaces. + value: [{ + // The face id for the extrude plane. + faceId: uuid, + // The id of the geometry. + id: uuid, + // The name. + name: string, + // The position. + position: [number, number, number], + // The rotation. + rotation: [number, number, number, number], + // The source range. + sourceRange: [number, number], + type: "extrudePlane", +}], + // The x-axis of the extrude group base plane in the 3D space + xAxis: { + x: number, + y: number, + z: number, +}, + // The y-axis of the extrude group base plane in the 3D space + yAxis: { + x: number, + y: number, + z: number, +}, + // The z-axis of the extrude group base plane in the 3D space + zAxis: { + x: number, + y: number, + z: number, +}, +} +``` + +#### Returns + +* `Geometries` - A set of geometry. +``` +{ + type: "SketchGroups", +} | +{ + type: "ExtrudeGroups", +} +``` + + + ### patternLinear A linear pattern. diff --git a/src/wasm-lib/kcl/src/std/mod.rs b/src/wasm-lib/kcl/src/std/mod.rs index aa13d5558..115c222da 100644 --- a/src/wasm-lib/kcl/src/std/mod.rs +++ b/src/wasm-lib/kcl/src/std/mod.rs @@ -73,6 +73,7 @@ lazy_static! { Box::new(crate::std::sketch::BezierCurve), Box::new(crate::std::sketch::Hole), Box::new(crate::std::patterns::PatternLinear), + Box::new(crate::std::patterns::PatternCircular), Box::new(crate::std::import::Import), Box::new(crate::std::math::Cos), Box::new(crate::std::math::Sin), diff --git a/src/wasm-lib/kcl/src/std/patterns.rs b/src/wasm-lib/kcl/src/std/patterns.rs index f47a391c4..546ed470d 100644 --- a/src/wasm-lib/kcl/src/std/patterns.rs +++ b/src/wasm-lib/kcl/src/std/patterns.rs @@ -27,6 +27,25 @@ pub struct LinearPatternData { pub axis: [f64; 3], } +/// Data for a circular pattern. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct CircularPatternData { + /// The number of repetitions. Must be greater than 0. + /// This excludes the original entity. For example, if `repetitions` is 1, + /// the original entity will be copied once. + pub repetitions: usize, + /// The axis around which to make the pattern. This is a 3D vector. + pub axis: [f64; 3], + /// The center about which to make th pattern. This is a 3D vector. + pub center: [f64; 3], + /// The arc angle (in degrees) to place the repetitions. Must be greater than 0. + pub arc_degrees: f64, + /// Whether or not to rotate the duplicates as they are copied. + pub rotate_duplicates: bool, +} + /// A linear pattern. pub async fn pattern_linear(args: Args) -> Result { let (data, geometry): (LinearPatternData, Geometry) = args.get_data_and_geometry()?; @@ -47,6 +66,26 @@ pub async fn pattern_linear(args: Args) -> Result { } } +/// A circular pattern. +pub async fn pattern_circular(args: Args) -> Result { + let (data, geometry): (CircularPatternData, Geometry) = args.get_data_and_geometry()?; + + if data.axis == [0.0, 0.0, 0.0] { + return Err(KclError::Semantic(KclErrorDetails { + message: + "The axis of the circular pattern cannot be the zero vector. Otherwise they will just duplicate in place." + .to_string(), + source_ranges: vec![args.source_range], + })); + } + + let new_geometries = inner_pattern_circular(data, geometry, args).await?; + match new_geometries { + Geometries::SketchGroups(sketch_groups) => Ok(MemoryItem::SketchGroups { value: sketch_groups }), + Geometries::ExtrudeGroups(extrude_groups) => Ok(MemoryItem::ExtrudeGroups { value: extrude_groups }), + } +} + /// A linear pattern. #[stdlib { name = "patternLinear", @@ -99,3 +138,62 @@ async fn inner_pattern_linear(data: LinearPatternData, geometry: Geometry, args: Ok(geometries) } + +/// A Circular pattern. +#[stdlib { + name = "patternCircular", +}] +async fn inner_pattern_circular( + data: CircularPatternData, + geometry: Geometry, + args: Args, +) -> Result { + let id = uuid::Uuid::new_v4(); + + let resp = args + .send_modeling_cmd( + id, + ModelingCmd::EntityCircularPattern { + axis: data.axis.into(), + entity_id: geometry.id(), + center: data.center.into(), + num_repetitions: data.repetitions as u32, + arc_degrees: data.arc_degrees, + rotate_duplicates: data.rotate_duplicates, + }, + ) + .await?; + + let kittycad::types::OkWebSocketResponseData::Modeling { + modeling_response: kittycad::types::OkModelingCmdResponse::EntityCircularPattern { data: pattern_info }, + } = &resp + else { + return Err(KclError::Engine(KclErrorDetails { + message: format!("EntityCircularPattern response was not as expected: {:?}", resp), + source_ranges: vec![args.source_range], + })); + }; + + let geometries = match geometry { + Geometry::SketchGroup(sketch_group) => { + let mut geometries = vec![sketch_group.clone()]; + for id in pattern_info.entity_ids.iter() { + let mut new_sketch_group = sketch_group.clone(); + new_sketch_group.id = *id; + geometries.push(new_sketch_group); + } + Geometries::SketchGroups(geometries) + } + Geometry::ExtrudeGroup(extrude_group) => { + let mut geometries = vec![extrude_group.clone()]; + for id in pattern_info.entity_ids.iter() { + let mut new_extrude_group = extrude_group.clone(); + new_extrude_group.id = *id; + geometries.push(new_extrude_group); + } + Geometries::ExtrudeGroups(geometries) + } + }; + + Ok(geometries) +} diff --git a/src/wasm-lib/tests/executor/main.rs b/src/wasm-lib/tests/executor/main.rs index ae84f4d78..cb2a54c00 100644 --- a/src/wasm-lib/tests/executor/main.rs +++ b/src/wasm-lib/tests/executor/main.rs @@ -768,6 +768,88 @@ const rectangle = startSketchOn('XY') twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_holes.png", &result, 0.999); } +#[tokio::test(flavor = "multi_thread")] +async fn serial_test_patterns_circular_basic_2d() { + let code = r#"fn circle = (pos, radius) => { + const sg = startSketchOn('XY') + |> startProfileAt([pos[0] + radius, pos[1]], %) + |> arc({ + angle_end: 360, + angle_start: 0, + radius: radius + }, %) + |> close(%) + return sg +} + +const part = circle([0,0], 2) + |> patternCircular({axis: [0,0,1], center: [20, 20, 20], repetitions: 12, arcDegrees: 210, rotateDuplicates: true}, %) +"#; + + let result = execute_and_snapshot(code).await.unwrap(); + twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_2d.png", &result, 0.999); +} + +#[tokio::test(flavor = "multi_thread")] +async fn serial_test_patterns_circular_basic_3d() { + let code = r#"fn circle = (pos, radius) => { + const sg = startSketchOn('XY') + |> startProfileAt([pos[0] + radius, pos[1]], %) + |> arc({ + angle_end: 360, + angle_start: 0, + radius: radius + }, %) + |> close(%) + return sg +} + +const part = startSketchOn('XY') + |> startProfileAt([0, 0], %) + |> line([0,1], %) + |> line([1, 0], %) + |> line([0, -1], %) + |> close(%) + |> extrude(1, %) + |> patternCircular({axis: [0,1,0], center: [-20, -20, -20], repetitions: 40, arcDegrees: 360, rotateDuplicates: false}, %) +"#; + + let result = execute_and_snapshot(code).await.unwrap(); + twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_3d.png", &result, 0.999); +} + +#[tokio::test(flavor = "multi_thread")] +async fn serial_test_patterns_circular_3d_tilted_axis() { + let code = r#"fn circle = (pos, radius) => { + const sg = startSketchOn('XY') + |> startProfileAt([pos[0] + radius, pos[1]], %) + |> arc({ + angle_end: 360, + angle_start: 0, + radius: radius + }, %) + |> close(%) + return sg +} + +const part = startSketchOn('XY') + |> startProfileAt([0, 0], %) + |> line([0,1], %) + |> line([1, 0], %) + |> line([0, -1], %) + |> close(%) + |> extrude(1, %) + |> patternCircular({axis: [1,1,-1], center: [10, 0, 10], repetitions: 10, arcDegrees: 360, rotateDuplicates: true}, %) +"#; + + let result = execute_and_snapshot(code).await.unwrap(); + twenty_twenty::assert_image( + "tests/executor/outputs/patterns_circular_3d_tilted_axis.png", + &result, + 0.999, + ); +} + #[tokio::test(flavor = "multi_thread")] async fn serial_test_import_file_doesnt_exist() { let code = r#"const model = import("thing.obj")"#; diff --git a/src/wasm-lib/tests/executor/outputs/patterns_circular_3d_tilted_axis.png b/src/wasm-lib/tests/executor/outputs/patterns_circular_3d_tilted_axis.png new file mode 100644 index 0000000000000000000000000000000000000000..beced47eede45ae4f519b02b6e2acd474216c792 GIT binary patch literal 71592 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 literal 0 HcmV?d00001 diff --git a/src/wasm-lib/tests/executor/outputs/patterns_circular_basic_2d.png b/src/wasm-lib/tests/executor/outputs/patterns_circular_basic_2d.png new file mode 100644 index 0000000000000000000000000000000000000000..ef8b773ba98965e654a59d70eb54e6e829302aeb GIT binary patch literal 73531 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

v2+^E(k)x@~ntBS8Qa?3!bnyLf#=QUBbQ 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 Xk=r-zsP}>YJ@xprf6RGo;a~qBD5uWZ literal 0 HcmV?d00001 From e1af4b4219ded8f5c2f62c9d6a02276f7cee7c7d Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Tue, 13 Feb 2024 12:20:32 -0800 Subject: [PATCH 5/7] add known issues file (#1408) * updates Signed-off-by: Jess Frazelle * fixes Signed-off-by: Jess Frazelle * Update KNOWN-ISSUES.md --------- Signed-off-by: Jess Frazelle --- docs/kcl/KNOWN-ISSUES.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/kcl/KNOWN-ISSUES.md diff --git a/docs/kcl/KNOWN-ISSUES.md b/docs/kcl/KNOWN-ISSUES.md new file mode 100644 index 000000000..3eed1d383 --- /dev/null +++ b/docs/kcl/KNOWN-ISSUES.md @@ -0,0 +1,12 @@ +# Known Issues + +The following are bugs that are not in modeling-app or kcl itself. These bugs +once fixed in engine will just start working here with no language changes. + +- **Sketch on Face**: If your sketch is outside the edges of the face (on which you + are sketching) you will get multiple models returned instead of one single + model for that sketch and its underlying 3D object. + +- **Patterns**: If you try and pass a pattern to `hole` currently only the first + item in the pattern is being subtracted. This is an engine bug that is being + worked on. From 19925d22c1607a2a08e1ecd4aa53f03690b71781 Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Wed, 14 Feb 2024 08:03:20 +1100 Subject: [PATCH 6/7] rename scene classes for clarity (#1409) * rename for clarity * typo * make coverage happ+ somewhat pointless since we don't use coverage because its not complete with both vitest and playwright * local storage issue * fmt * fix --- src/clientSideScene/ClientSideSceneComp.tsx | 252 +++++++++++++++++ .../{clientSideScene.ts => sceneEntities.ts} | 63 +++-- .../{setup.test.ts => sceneInfra.test.ts} | 2 +- .../{setup.tsx => sceneInfra.ts} | 260 +----------------- src/clientSideScene/segments.ts | 4 +- src/components/CamToggle.tsx | 18 +- src/components/DebugPanel.tsx | 2 +- src/components/ModelingMachineProvider.tsx | 10 +- src/components/Stream.tsx | 2 +- src/components/TextEditor.tsx | 4 +- src/lang/KclSingleton.tsx | 20 +- src/lang/modifyAst.ts | 2 +- src/lang/std/engineConnection.ts | 4 +- src/lib/selections.ts | 8 +- src/machines/modelingMachine.ts | 74 ++--- 15 files changed, 376 insertions(+), 349 deletions(-) create mode 100644 src/clientSideScene/ClientSideSceneComp.tsx rename src/clientSideScene/{clientSideScene.ts => sceneEntities.ts} (95%) rename src/clientSideScene/{setup.test.ts => sceneInfra.test.ts} (95%) rename src/clientSideScene/{setup.tsx => sceneInfra.ts} (81%) diff --git a/src/clientSideScene/ClientSideSceneComp.tsx b/src/clientSideScene/ClientSideSceneComp.tsx new file mode 100644 index 000000000..7325336aa --- /dev/null +++ b/src/clientSideScene/ClientSideSceneComp.tsx @@ -0,0 +1,252 @@ +import { useRef, useEffect, useState } from 'react' +import { useModelingContext } from 'hooks/useModelingContext' + +import { cameraMouseDragGuards } from 'lib/cameraControls' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' +import { useStore } from 'useStore' +import { + DEBUG_SHOW_BOTH_SCENES, + ReactCameraProperties, + sceneInfra, +} from './sceneInfra' +import { throttle } from 'lib/utils' + +function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { + const [isCamMoving, setIsCamMoving] = useState(false) + const [isTween, setIsTween] = useState(false) + + const { state } = useModelingContext() + + useEffect(() => { + sceneInfra.setIsCamMovingCallback((isMoving, isTween) => { + setIsCamMoving(isMoving) + setIsTween(isTween) + }) + }, []) + + if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving) + return { hideClient: false, hideServer: false } + let hideServer = state.matches('Sketch') || state.matches('Sketch no face') + if (isTween) { + hideServer = false + } + + return { hideClient: !hideServer, hideServer } +} + +export const ClientSideScene = ({ + cameraControls, +}: { + cameraControls: ReturnType< + typeof useGlobalStateContext + >['settings']['context']['cameraControls'] +}) => { + const canvasRef = useRef(null) + const { state, send } = useModelingContext() + const { hideClient, hideServer } = useShouldHideScene() + const { setHighlightRange } = useStore((s) => ({ + setHighlightRange: s.setHighlightRange, + highlightRange: s.highlightRange, + })) + + // Listen for changes to the camera controls setting + // and update the client-side scene's controls accordingly. + useEffect(() => { + sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls]) + }, [cameraControls]) + useEffect(() => { + sceneInfra.updateOtherSelectionColors( + state?.context?.selectionRanges?.otherSelections || [] + ) + }, [state?.context?.selectionRanges?.otherSelections]) + + useEffect(() => { + if (!canvasRef.current) return + const canvas = canvasRef.current + canvas.appendChild(sceneInfra.renderer.domElement) + sceneInfra.animate() + sceneInfra.setHighlightCallback(setHighlightRange) + canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false) + canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false) + canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false) + sceneInfra.setSend(send) + return () => { + canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove) + canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown) + canvas?.removeEventListener('mouseup', sceneInfra.onMouseUp) + } + }, []) + + return ( +

+ ) +} + +const throttled = throttle((a: ReactCameraProperties) => { + if (a.type === 'perspective' && a.fov) { + sceneInfra.dollyZoom(a.fov) + } +}, 1000 / 15) + +export const CamDebugSettings = () => { + const [camSettings, setCamSettings] = useState({ + type: 'perspective', + fov: 12, + position: [0, 0, 0], + quaternion: [0, 0, 0, 1], + }) + const [fov, setFov] = useState(12) + + useEffect(() => { + sceneInfra.setReactCameraPropertiesCallback(setCamSettings) + }, [sceneInfra]) + useEffect(() => { + if (camSettings.type === 'perspective' && camSettings.fov) { + setFov(camSettings.fov) + } + }, [(camSettings as any)?.fov]) + + return ( +
+

cam settings

+ perspective cam + { + if (camSettings.type === 'perspective') { + sceneInfra.useOrthographicCamera() + } else { + sceneInfra.usePerspectiveCamera() + } + }} + /> + {camSettings.type === 'perspective' && ( + { + setFov(parseFloat(e.target.value)) + + throttled({ + ...camSettings, + fov: parseFloat(e.target.value), + }) + }} + className="w-full cursor-pointer pointer-events-auto" + /> + )} + {camSettings.type === 'perspective' && ( +
+ fov + { + sceneInfra.setCam({ + ...camSettings, + fov: parseFloat(e.target.value), + }) + }} + /> +
+ )} + {camSettings.type === 'orthographic' && ( + <> +
+ fov + { + sceneInfra.setCam({ + ...camSettings, + zoom: parseFloat(e.target.value), + }) + }} + /> +
+ + )} +
+ Position +
    +
  • + x: + { + sceneInfra.setCam({ + ...camSettings, + position: [ + parseFloat(e.target.value), + camSettings.position[1], + camSettings.position[2], + ], + }) + }} + /> +
  • +
  • + y: + { + sceneInfra.setCam({ + ...camSettings, + position: [ + camSettings.position[0], + parseFloat(e.target.value), + camSettings.position[2], + ], + }) + }} + /> +
  • +
  • + z: + { + sceneInfra.setCam({ + ...camSettings, + position: [ + camSettings.position[0], + camSettings.position[1], + parseFloat(e.target.value), + ], + }) + }} + /> +
  • +
+
+
+ ) +} diff --git a/src/clientSideScene/clientSideScene.ts b/src/clientSideScene/sceneEntities.ts similarity index 95% rename from src/clientSideScene/clientSideScene.ts rename to src/clientSideScene/sceneEntities.ts index b291e76f2..e971a07f7 100644 --- a/src/clientSideScene/clientSideScene.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -25,14 +25,14 @@ import { INTERSECTION_PLANE_LAYER, isQuaternionVertical, RAYCASTABLE_PLANE, - setupSingleton, + sceneInfra, SKETCH_GROUP_SEGMENTS, SKETCH_LAYER, X_AXIS, XZ_PLANE, Y_AXIS, YZ_PLANE, -} from './setup' +} from './sceneInfra' import { CallExpression, getTangentialArcToInfo, @@ -85,7 +85,10 @@ export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body' export const TANGENTIAL_ARC_TO__SEGMENT_DASH = 'tangential-arc-to-segment-body-dashed' -class ClientSideScene { +// This singleton Class is responsible for all of the things the user sees and interacts with. +// That mostly mean sketch elements. +// Cameras, controls, raycasters, etc are handled by sceneInfra +class SceneEntities { scene: Scene sceneProgramMemory: ProgramMemory = { root: {}, return: null } activeSegments: { [key: string]: Group } = {} @@ -93,18 +96,18 @@ class ClientSideScene { axisGroup: Group | null = null currentSketchQuaternion: Quaternion | null = null constructor() { - this.scene = setupSingleton?.scene - setupSingleton?.setOnCamChange(this.onCamChange) + this.scene = sceneInfra?.scene + sceneInfra?.setOnCamChange(this.onCamChange) } onCamChange = () => { - const orthoFactor = orthoScale(setupSingleton.camera) + const orthoFactor = orthoScale(sceneInfra.camera) Object.values(this.activeSegments).forEach((segment) => { const factor = - setupSingleton.camera instanceof OrthographicCamera + sceneInfra.camera instanceof OrthographicCamera ? orthoFactor - : perspScale(setupSingleton.camera, segment) + : perspScale(sceneInfra.camera, segment) if ( segment.userData.from && segment.userData.to && @@ -135,9 +138,9 @@ class ClientSideScene { }) if (this.axisGroup) { const factor = - setupSingleton.camera instanceof OrthographicCamera + sceneInfra.camera instanceof OrthographicCamera ? orthoFactor - : perspScale(setupSingleton.camera, this.axisGroup) + : perspScale(sceneInfra.camera, this.axisGroup) const x = this.axisGroup.getObjectByName(X_AXIS) x?.scale.set(1, factor, 1) const y = this.axisGroup.getObjectByName(Y_AXIS) @@ -259,11 +262,11 @@ class ClientSideScene { sketchGroup.position[1], sketchGroup.position[2] ) - const orthoFactor = orthoScale(setupSingleton.camera) + const orthoFactor = orthoScale(sceneInfra.camera) const factor = - setupSingleton.camera instanceof OrthographicCamera + sceneInfra.camera instanceof OrthographicCamera ? orthoFactor - : perspScale(setupSingleton.camera, dummy) + : perspScale(sceneInfra.camera, dummy) sketchGroup.value.forEach((segment, index) => { let segPathToNode = getNodePathFromSourceRange( draftSegment ? truncatedAst : kclManager.ast, @@ -310,7 +313,7 @@ class ClientSideScene { this.scene.add(group) if (!draftSegment) { - setupSingleton.setCallbacks({ + sceneInfra.setCallbacks({ onDrag: (args) => { this.onDragSegment({ ...args, @@ -320,7 +323,7 @@ class ClientSideScene { onMove: () => {}, onClick: (args) => { if (!args || !args.object) { - setupSingleton.modelingSend({ + sceneInfra.modelingSend({ type: 'Set selection', data: { selectionType: 'singleCodeCursor', @@ -331,7 +334,7 @@ class ClientSideScene { const { object } = args const event = getEventForSegmentSelection(object) if (!event) return - setupSingleton.modelingSend(event) + sceneInfra.modelingSend(event) }, onMouseEnter: ({ object }) => { // TODO change the color of the segment to yellow? @@ -351,15 +354,15 @@ class ClientSideScene { parent.userData.pathToNode, 'CallExpression' ).node - setupSingleton.highlightCallback([node.start, node.end]) + sceneInfra.highlightCallback([node.start, node.end]) const yellow = 0xffff00 colorSegment(object, yellow) return } - setupSingleton.highlightCallback([0, 0]) + sceneInfra.highlightCallback([0, 0]) }, onMouseLeave: ({ object }) => { - setupSingleton.highlightCallback([0, 0]) + sceneInfra.highlightCallback([0, 0]) const parent = getParentGroup(object) const isSelected = parent?.userData?.isSelected colorSegment(object, isSelected ? 0x0000ff : 0xffffff) @@ -372,7 +375,7 @@ class ClientSideScene { }, }) } else { - setupSingleton.setCallbacks({ + sceneInfra.setCallbacks({ onDrag: () => {}, onClick: async (args) => { if (!args) return @@ -427,7 +430,7 @@ class ClientSideScene { }, }) } - setupSingleton.controls.enableRotate = false + sceneInfra.controls.enableRotate = false } updateAstAndRejigSketch = async ( sketchPathToNode: PathToNode, @@ -530,7 +533,7 @@ class ClientSideScene { this.sceneProgramMemory = programMemory const sketchGroup = programMemory.root[variableDeclarationName] .value as Path[] - const orthoFactor = orthoScale(setupSingleton.camera) + const orthoFactor = orthoScale(sceneInfra.camera) sketchGroup.forEach((segment, index) => { const segPathToNode = getNodePathFromSourceRange( modifiedAst, @@ -546,9 +549,9 @@ class ClientSideScene { // const prevSegment = sketchGroup.slice(index - 1)[0] const type = group?.userData?.type const factor = - setupSingleton.camera instanceof OrthographicCamera + sceneInfra.camera instanceof OrthographicCamera ? orthoFactor - : perspScale(setupSingleton.camera, group) + : perspScale(sceneInfra.camera, group) if (type === TANGENTIAL_ARC_TO_SEGMENT) { this.updateTangentialArcToSegment({ prevSegment: sketchGroup[index - 1], @@ -705,9 +708,9 @@ class ClientSideScene { } async animateAfterSketch() { if (isReducedMotion()) { - setupSingleton.usePerspectiveCamera() + sceneInfra.usePerspectiveCamera() } else { - await setupSingleton.animateToPerspective() + await sceneInfra.animateToPerspective() } } removeSketchGrid() { @@ -740,7 +743,7 @@ class ClientSideScene { reject() } } - setupSingleton.controls.enableRotate = true + sceneInfra.controls.enableRotate = true this.activeSegments = {} // maybe should reset onMove etc handlers if (shouldResolve) resolve(true) @@ -759,7 +762,7 @@ class ClientSideScene { }) } setupDefaultPlaneHover() { - setupSingleton.setCallbacks({ + sceneInfra.setCallbacks({ onMouseEnter: ({ object }) => { if (object.parent.userData.type !== DEFAULT_PLANES) return const type: DefaultPlane = object.userData.type @@ -784,7 +787,7 @@ class ClientSideScene { planeString = posNorm ? 'XZ' : '-XZ' normal = posNorm ? [0, 1, 0] : [0, -1, 0] } - setupSingleton.modelingSend({ + sceneInfra.modelingSend({ type: 'Select default plane', data: { plane: planeString, @@ -798,7 +801,7 @@ class ClientSideScene { export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ' -export const clientSideScene = new ClientSideScene() +export const sceneEntitiesManager = new SceneEntities() // calculations/pure-functions/easy to test so no excuse not to diff --git a/src/clientSideScene/setup.test.ts b/src/clientSideScene/sceneInfra.test.ts similarity index 95% rename from src/clientSideScene/setup.test.ts rename to src/clientSideScene/sceneInfra.test.ts index dbd1d98a9..90a710069 100644 --- a/src/clientSideScene/setup.test.ts +++ b/src/clientSideScene/sceneInfra.test.ts @@ -1,5 +1,5 @@ import { Quaternion } from 'three' -import { isQuaternionVertical } from './setup' +import { isQuaternionVertical } from './sceneInfra' describe('isQuaternionVertical', () => { it('should identify vertical quaternions', () => { diff --git a/src/clientSideScene/setup.tsx b/src/clientSideScene/sceneInfra.ts similarity index 81% rename from src/clientSideScene/setup.tsx rename to src/clientSideScene/sceneInfra.ts index 5ab174d44..3b9410237 100644 --- a/src/clientSideScene/setup.tsx +++ b/src/clientSideScene/sceneInfra.ts @@ -24,7 +24,6 @@ import { Object3DEventMap, } from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' -import { useRef, useEffect, useState } from 'react' import { engineCommandManager } from 'lang/std/engineConnection' import { v4 as uuidv4 } from 'uuid' import { isReducedMotion, roundOff, throttle } from 'lib/utils' @@ -33,9 +32,7 @@ import { useModelingContext } from 'hooks/useModelingContext' import { deg2Rad } from 'lib/utils2d' import * as TWEEN from '@tweenjs/tween.js' import { MouseGuard, cameraMouseDragGuards } from 'lib/cameraControls' -import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { SourceRange } from 'lang/wasm' -import { useStore } from 'useStore' import { Axis } from 'lib/selections' import { createGridHelper } from './helpers' @@ -181,7 +178,7 @@ interface onMoveCallbackArgs { intersection: Intersection> } -type ReactCameraProperties = +export type ReactCameraProperties = | { type: 'perspective' fov?: number @@ -195,8 +192,11 @@ type ReactCameraProperties = quaternion: [number, number, number, number] } -class SetupSingleton { - static instance: SetupSingleton +// This singleton class is responsible for all of the under the hood setup for the client side scene. +// That is the cameras and switching between them, raycasters for click mouse events and their abstractions (onClick etc), setting up controls. +// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts +class SceneInfra { + static instance: SceneInfra scene: Scene camera: PerspectiveCamera | OrthographicCamera renderer: WebGLRenderer @@ -333,7 +333,7 @@ class SetupSingleton { const light = new AmbientLight(0x505050) // soft white light this.scene.add(light) - SetupSingleton.instance = this + SceneInfra.instance = this } private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void = () => {} @@ -713,7 +713,7 @@ class SetupSingleton { } | null => { this.planeRaycaster.setFromCamera( this.currentMouseVector, - setupSingleton.camera + sceneInfra.camera ) const planeIntersects = this.planeRaycaster.intersectObjects( this.scene.children, @@ -976,7 +976,7 @@ class SetupSingleton { if (planesGroup) this.scene.remove(planesGroup) } updateOtherSelectionColors = (otherSelections: Axis[]) => { - const axisGroup = setupSingleton.scene.children.find( + const axisGroup = sceneInfra.scene.children.find( ({ userData }) => userData?.type === AXIS_GROUP ) const axisMap: { [key: string]: Axis } = { @@ -998,247 +998,7 @@ class SetupSingleton { } } -export const setupSingleton = new SetupSingleton() - -function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { - const [isCamMoving, setIsCamMoving] = useState(false) - const [isTween, setIsTween] = useState(false) - - const { state } = useModelingContext() - - useEffect(() => { - setupSingleton.setIsCamMovingCallback((isMoving, isTween) => { - setIsCamMoving(isMoving) - setIsTween(isTween) - }) - }, []) - - if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving) - return { hideClient: false, hideServer: false } - let hideServer = state.matches('Sketch') || state.matches('Sketch no face') - if (isTween) { - hideServer = false - } - - return { hideClient: !hideServer, hideServer } -} - -export const ClientSideScene = ({ - cameraControls, -}: { - cameraControls: ReturnType< - typeof useGlobalStateContext - >['settings']['context']['cameraControls'] -}) => { - const canvasRef = useRef(null) - const { state, send } = useModelingContext() - const { hideClient, hideServer } = useShouldHideScene() - const { setHighlightRange } = useStore((s) => ({ - setHighlightRange: s.setHighlightRange, - highlightRange: s.highlightRange, - })) - - // Listen for changes to the camera controls setting - // and update the client-side scene's controls accordingly. - useEffect(() => { - setupSingleton.setInteractionGuards(cameraMouseDragGuards[cameraControls]) - }, [cameraControls]) - useEffect(() => { - setupSingleton.updateOtherSelectionColors( - state?.context?.selectionRanges?.otherSelections || [] - ) - }, [state?.context?.selectionRanges?.otherSelections]) - - useEffect(() => { - if (!canvasRef.current) return - const canvas = canvasRef.current - canvas.appendChild(setupSingleton.renderer.domElement) - setupSingleton.animate() - setupSingleton.setHighlightCallback(setHighlightRange) - canvas.addEventListener('mousemove', setupSingleton.onMouseMove, false) - canvas.addEventListener('mousedown', setupSingleton.onMouseDown, false) - canvas.addEventListener('mouseup', setupSingleton.onMouseUp, false) - setupSingleton.setSend(send) - return () => { - canvas?.removeEventListener('mousemove', setupSingleton.onMouseMove) - canvas?.removeEventListener('mousedown', setupSingleton.onMouseDown) - canvas?.removeEventListener('mouseup', setupSingleton.onMouseUp) - } - }, []) - - return ( -
- ) -} - -const throttled = throttle((a: ReactCameraProperties) => { - if (a.type === 'perspective' && a.fov) { - setupSingleton.dollyZoom(a.fov) - } -}, 1000 / 15) - -export const CamDebugSettings = () => { - const [camSettings, setCamSettings] = useState({ - type: 'perspective', - fov: 12, - position: [0, 0, 0], - quaternion: [0, 0, 0, 1], - }) - const [fov, setFov] = useState(12) - - useEffect(() => { - setupSingleton.setReactCameraPropertiesCallback(setCamSettings) - }, [setupSingleton]) - useEffect(() => { - if (camSettings.type === 'perspective' && camSettings.fov) { - setFov(camSettings.fov) - } - }, [(camSettings as any)?.fov]) - - return ( -
-

cam settings

- perspective cam - { - if (camSettings.type === 'perspective') { - setupSingleton.useOrthographicCamera() - } else { - setupSingleton.usePerspectiveCamera() - } - }} - /> - {camSettings.type === 'perspective' && ( - { - setFov(parseFloat(e.target.value)) - - throttled({ - ...camSettings, - fov: parseFloat(e.target.value), - }) - }} - className="w-full cursor-pointer pointer-events-auto" - /> - )} - {camSettings.type === 'perspective' && ( -
- fov - { - setupSingleton.setCam({ - ...camSettings, - fov: parseFloat(e.target.value), - }) - }} - /> -
- )} - {camSettings.type === 'orthographic' && ( - <> -
- fov - { - setupSingleton.setCam({ - ...camSettings, - zoom: parseFloat(e.target.value), - }) - }} - /> -
- - )} -
- Position -
    -
  • - x: - { - setupSingleton.setCam({ - ...camSettings, - position: [ - parseFloat(e.target.value), - camSettings.position[1], - camSettings.position[2], - ], - }) - }} - /> -
  • -
  • - y: - { - setupSingleton.setCam({ - ...camSettings, - position: [ - camSettings.position[0], - parseFloat(e.target.value), - camSettings.position[2], - ], - }) - }} - /> -
  • -
  • - z: - { - setupSingleton.setCam({ - ...camSettings, - position: [ - camSettings.position[0], - camSettings.position[1], - parseFloat(e.target.value), - ], - }) - }} - /> -
  • -
-
-
- ) -} +export const sceneInfra = new SceneInfra() function convertThreeCamValuesToEngineCam({ position, diff --git a/src/clientSideScene/segments.ts b/src/clientSideScene/segments.ts index 1a279ccd8..4623c7076 100644 --- a/src/clientSideScene/segments.ts +++ b/src/clientSideScene/segments.ts @@ -25,9 +25,9 @@ import { TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT_BODY, TANGENTIAL_ARC_TO__SEGMENT_DASH, -} from './clientSideScene' +} from './sceneEntities' import { getTangentPointFromPreviousArc } from 'lib/utils2d' -import { ARROWHEAD } from './setup' +import { ARROWHEAD } from './sceneInfra' export function straightSegment({ from, diff --git a/src/components/CamToggle.tsx b/src/components/CamToggle.tsx index f55cd0c8b..97bc49389 100644 --- a/src/components/CamToggle.tsx +++ b/src/components/CamToggle.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from 'react' -import { setupSingleton } from '../clientSideScene/setup' +import { sceneInfra } from '../clientSideScene/sceneInfra' import { engineCommandManager } from 'lang/std/engineConnection' import { throttle, isReducedMotion } from 'lib/utils' const updateDollyZoom = throttle( - (newFov: number) => setupSingleton.dollyZoom(newFov), + (newFov: number) => sceneInfra.dollyZoom(newFov), 1000 / 15 ) @@ -15,19 +15,19 @@ export const CamToggle = () => { useEffect(() => { engineCommandManager.waitForReady.then(async () => { - setupSingleton.dollyZoom(fov) + sceneInfra.dollyZoom(fov) }) }, []) const toggleCamera = () => { if (isPerspective) { isReducedMotion() - ? setupSingleton.useOrthographicCamera() - : setupSingleton.animateToOrthographic() + ? sceneInfra.useOrthographicCamera() + : sceneInfra.animateToOrthographic() } else { isReducedMotion() - ? setupSingleton.usePerspectiveCamera() - : setupSingleton.animateToPerspective() + ? sceneInfra.usePerspectiveCamera() + : sceneInfra.animateToPerspective() } setIsPerspective(!isPerspective) } @@ -60,9 +60,9 @@ export const CamToggle = () => {