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
This commit is contained in:
@ -994,6 +994,39 @@ impl Node<MemberExpression> {
|
|||||||
|
|
||||||
// Check the property and object match -- e.g. ints for arrays, strs for objects.
|
// Check the property and object match -- e.g. ints for arrays, strs for objects.
|
||||||
match (object, property, self.computed) {
|
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) => {
|
(KclValue::Object { value: map, meta: _ }, Property::String(property), false) => {
|
||||||
if let Some(value) = map.get(&property) {
|
if let Some(value) = map.get(&property) {
|
||||||
Ok(value.to_owned())
|
Ok(value.to_owned())
|
||||||
@ -1013,7 +1046,22 @@ impl Node<MemberExpression> {
|
|||||||
vec![self.clone().into()],
|
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 t = p.type_name();
|
||||||
let article = article_for(t);
|
let article = article_for(t);
|
||||||
Err(KclError::new_semantic(KclErrorDetails::new(
|
Err(KclError::new_semantic(KclErrorDetails::new(
|
||||||
@ -2205,4 +2253,12 @@ y = x[0mm + 1]
|
|||||||
"#;
|
"#;
|
||||||
parse_execute(ast).await.unwrap_err();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -921,6 +921,12 @@ impl Point3d {
|
|||||||
units: UnitLen::Unknown,
|
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 {
|
impl From<[TyF64; 3]> for Point3d {
|
||||||
|
@ -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<Metadata>) -> 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<usize> {
|
pub(crate) fn as_usize(&self) -> Option<usize> {
|
||||||
match self {
|
match self {
|
||||||
KclValue::Number { value, .. } => crate::try_f64_to_usize(*value),
|
KclValue::Number { value, .. } => crate::try_f64_to_usize(*value),
|
||||||
|
60
rust/kcl-lib/tests/inputs/planestuff.kcl
Normal file
60
rust/kcl-lib/tests/inputs/planestuff.kcl
Normal file
@ -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)
|
Reference in New Issue
Block a user