From 4f4c44e7c76bdeba7d1185e3e092dd7aa1cf74cc Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Wed, 2 Jul 2025 11:24:26 -0500 Subject: [PATCH] KCL: Getter for axes of planes (#7662) ## Goal Currently, there's no way in KCL to get fields of a plane, e.g. the underlying X axis, Y axis or origin. This would be useful for geometry calculations in KCL. It would help KCL users write transformations between planes for rotating geometry. For example, this enables ```kcl export fn crossProduct(@vectors) { a = vectors[0] b = vectors[1] x = a[1] * b[2] - (a[2] * b[1]) y = a[2] * b[0] - (a[0] * b[2]) z = a[0] * b[1] - (a[1] * b[0]) return [x, y, z] } export fn normalOf(@plane) { return crossProduct([plane.xAxis, plane.yAxis]) } ``` ## Implementation My goal was just to enable a simple getter for planes, like `myPlane.xAxis` and yAxis and origins. That's nearly what happened, except I discovered that there's two ways to represent a plane: either `KclValue::Plane` or `KclValue::Object` with the right fields. No matter which format your plane is represented as, it should behave consistently when you get its properties. Those properties should be returned as `[number; 3]` because that is how KCL represents points. Unfortunately we actually require planes-as-objects to be defined with axes like `myPlane = { xAxis = { x = 1, y = 0, z = 0 }, ...}`, but that's a mistake in my opinion. So if you do use that representation of a plane, it should still return a [number; 3]. This required some futzing around so that we let you access object fields .x and .y as [0] and [1], which is weird, but whatever, I think it's good. This PR is tested via planestuff.kcl which has a Rust unit test. Part of the hole efforts, see https://github.com/KittyCAD/modeling-app/discussions/7543 --- rust/kcl-lib/src/execution/exec_ast.rs | 58 ++++++++++++++++++++++- rust/kcl-lib/src/execution/geometry.rs | 6 +++ rust/kcl-lib/src/execution/kcl_value.rs | 25 ++++++++++ rust/kcl-lib/tests/inputs/planestuff.kcl | 60 ++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 rust/kcl-lib/tests/inputs/planestuff.kcl diff --git a/rust/kcl-lib/src/execution/exec_ast.rs b/rust/kcl-lib/src/execution/exec_ast.rs index f1ee7cc47..0a8b46fb1 100644 --- a/rust/kcl-lib/src/execution/exec_ast.rs +++ b/rust/kcl-lib/src/execution/exec_ast.rs @@ -994,6 +994,39 @@ impl Node { // Check the property and object match -- e.g. ints for arrays, strs for objects. match (object, property, self.computed) { + (KclValue::Plane { value: plane }, Property::String(property), false) => match property.as_str() { + "yAxis" => { + let (p, u) = plane.info.y_axis.as_3_dims(); + Ok(KclValue::array_from_point3d( + p, + NumericType::Known(crate::exec::UnitType::Length(u)), + vec![meta], + )) + } + "xAxis" => { + let (p, u) = plane.info.x_axis.as_3_dims(); + Ok(KclValue::array_from_point3d( + p, + NumericType::Known(crate::exec::UnitType::Length(u)), + vec![meta], + )) + } + "origin" => { + let (p, u) = plane.info.origin.as_3_dims(); + Ok(KclValue::array_from_point3d( + p, + NumericType::Known(crate::exec::UnitType::Length(u)), + vec![meta], + )) + } + other => Err(KclError::new_undefined_value( + KclErrorDetails::new( + format!("Property '{other}' not found in plane"), + vec![self.clone().into()], + ), + None, + )), + }, (KclValue::Object { value: map, meta: _ }, Property::String(property), false) => { if let Some(value) = map.get(&property) { Ok(value.to_owned()) @@ -1013,7 +1046,22 @@ impl Node { vec![self.clone().into()], ))) } - (KclValue::Object { .. }, p, _) => { + (KclValue::Object { value: map, .. }, p @ Property::UInt(i), _) => { + if i == 0 + && let Some(value) = map.get("x") + { + return Ok(value.to_owned()); + } + if i == 1 + && let Some(value) = map.get("y") + { + return Ok(value.to_owned()); + } + if i == 2 + && let Some(value) = map.get("z") + { + return Ok(value.to_owned()); + } let t = p.type_name(); let article = article_for(t); Err(KclError::new_semantic(KclErrorDetails::new( @@ -2205,4 +2253,12 @@ y = x[0mm + 1] "#; parse_execute(ast).await.unwrap_err(); } + + #[tokio::test(flavor = "multi_thread")] + async fn getting_property_of_plane() { + // let ast = include_str!("../../tests/inputs/planestuff.kcl"); + let ast = std::fs::read_to_string("tests/inputs/planestuff.kcl").unwrap(); + + parse_execute(&ast).await.unwrap(); + } } diff --git a/rust/kcl-lib/src/execution/geometry.rs b/rust/kcl-lib/src/execution/geometry.rs index 658da8bf2..f4fcc996c 100644 --- a/rust/kcl-lib/src/execution/geometry.rs +++ b/rust/kcl-lib/src/execution/geometry.rs @@ -921,6 +921,12 @@ impl Point3d { units: UnitLen::Unknown, } } + + pub fn as_3_dims(&self) -> ([f64; 3], UnitLen) { + let p = [self.x, self.y, self.z]; + let u = self.units; + (p, u) + } } impl From<[TyF64; 3]> for Point3d { diff --git a/rust/kcl-lib/src/execution/kcl_value.rs b/rust/kcl-lib/src/execution/kcl_value.rs index 9f9a07451..846a6fe73 100644 --- a/rust/kcl-lib/src/execution/kcl_value.rs +++ b/rust/kcl-lib/src/execution/kcl_value.rs @@ -458,6 +458,31 @@ impl KclValue { } } + /// Put the point into a KCL point. + pub fn array_from_point3d(p: [f64; 3], ty: NumericType, meta: Vec) -> Self { + let [x, y, z] = p; + Self::HomArray { + value: vec![ + Self::Number { + value: x, + meta: meta.clone(), + ty, + }, + Self::Number { + value: y, + meta: meta.clone(), + ty, + }, + Self::Number { + value: z, + meta: meta.clone(), + ty, + }, + ], + ty: ty.into(), + } + } + pub(crate) fn as_usize(&self) -> Option { match self { KclValue::Number { value, .. } => crate::try_f64_to_usize(*value), diff --git a/rust/kcl-lib/tests/inputs/planestuff.kcl b/rust/kcl-lib/tests/inputs/planestuff.kcl new file mode 100644 index 000000000..fd0c0946c --- /dev/null +++ b/rust/kcl-lib/tests/inputs/planestuff.kcl @@ -0,0 +1,60 @@ +// There are 3 ways to define a plane in KCL, according to https://zoo.dev/docs/kcl-std/types/std-types-Plane +// - A default plane +// - Modifying a default plane e.g. via offsetPlane +// - Defining your own struct +// This file tests they all work equivalently. + +// Define a plane using struct representation. +myPlane = { + origin = { x = 0, y = 0, z = 0 }, + xAxis = { x = 1, y = 0, z = 0 }, + yAxis = { x = 0, y = 1, z = 0 }, +} + +// Prove we can get its axes and origin. +ax = myPlane.xAxis +assert(ax[0], isEqualTo = 1) +assert(ax[1], isEqualTo = 0) +assert(ax[2], isEqualTo = 0) +ay = myPlane.yAxis +assert(ay[0], isEqualTo = 0) +assert(ay[1], isEqualTo = 1) +assert(ay[2], isEqualTo = 0) +aorigin = myPlane.origin +assert(aorigin[0], isEqualTo = 0) +assert(aorigin[1], isEqualTo = 0) +assert(aorigin[2], isEqualTo = 0) + +// Define a plane using standard planes. +myOtherPlane = XY + +// Prove we can get its axes and origin. +axOther = myOtherPlane.xAxis +assert(axOther[0], isEqualTo = 1) +assert(axOther[1], isEqualTo = 0) +assert(axOther[2], isEqualTo = 0) +ayOther = myOtherPlane.yAxis +assert(ayOther[0], isEqualTo = 0) +assert(ayOther[1], isEqualTo = 1) +assert(ayOther[2], isEqualTo = 0) +aoriginOther = myOtherPlane.origin +assert(aoriginOther[0], isEqualTo = 0) +assert(aoriginOther[1], isEqualTo = 0) +assert(aoriginOther[2], isEqualTo = 0) + +// Define a plane using a plane-modifying function like offsetPlane. +myAlternatePlane = offsetPlane(XY, offset = 0) + +// Prove we can get its axes and origin. +axAlternate = myAlternatePlane.xAxis +assert(axAlternate[0], isEqualTo = 1) +assert(axAlternate[1], isEqualTo = 0) +assert(axAlternate[2], isEqualTo = 0) +ayAlternate = myAlternatePlane.yAxis +assert(ayAlternate[0], isEqualTo = 0) +assert(ayAlternate[1], isEqualTo = 1) +assert(ayAlternate[2], isEqualTo = 0) +aoriginAlternate = myAlternatePlane.origin +assert(aoriginAlternate[0], isEqualTo = 0) +assert(aoriginAlternate[1], isEqualTo = 0) +assert(aoriginAlternate[2], isEqualTo = 0)