Add three point circle stdlib function (#4893)

* Add parsing keyword function calls inside pipelines

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>

* Add three point circle stdlib function

* Generate new documentation

* Fix 20:20 for the circle three point test

* Convert to using keyword arguments

* Wtf yo

* Remove unused structure

* Use the new simulation tests

* Regenerate documentation

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
This commit is contained in:
49fl
2025-01-04 12:18:29 -05:00
committed by GitHub
parent 1c941112d7
commit 474acb1c68
18 changed files with 4802 additions and 44 deletions

File diff suppressed because one or more lines are too long

View File

@ -35,6 +35,7 @@ layout: manual
* [`ceil`](kcl/ceil)
* [`chamfer`](kcl/chamfer)
* [`circle`](kcl/circle)
* [`circleThreePoint`](kcl/circleThreePoint)
* [`close`](kcl/close)
* [`cm`](kcl/cm)
* [`cos`](kcl/cos)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
---
title: "CircleThreePointData"
excerpt: "Data for drawing a 3-point circle"
layout: manual
---
Data for drawing a 3-point circle
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `p1` |`[number, number]`| Point one for circle derivation. | No |
| `p2` |`[number, number]`| Point two for circle derivation. | No |
| `p3` |`[number, number]`| Point three for circle derivation. | No |

View File

@ -1656,3 +1656,24 @@ mod boolean_logical_multiple {
super::execute(TEST_NAME, false).await
}
}
mod circle_three_point {
const TEST_NAME: &str = "circle_three_point";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -69,6 +69,7 @@ lazy_static! {
Box::new(crate::std::segment::AngleToMatchLengthX),
Box::new(crate::std::segment::AngleToMatchLengthY),
Box::new(crate::std::shapes::Circle),
Box::new(crate::std::shapes::CircleThreePoint),
Box::new(crate::std::shapes::Polygon),
Box::new(crate::std::sketch::LineTo),
Box::new(crate::std::sketch::Line),

View File

@ -17,7 +17,10 @@ use crate::{
errors::{KclError, KclErrorDetails},
execution::{BasePath, ExecState, GeoMeta, KclValue, Path, Sketch, SketchSurface},
parsing::ast::types::TagNode,
std::Args,
std::{
utils::{calculate_circle_center, distance},
Args,
},
};
/// A sketch surface or a sketch.
@ -145,6 +148,64 @@ async fn inner_circle(
Ok(new_sketch)
}
/// Sketch a 3-point circle.
pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let p1 = args.get_kw_arg("p1")?;
let p2 = args.get_kw_arg("p2")?;
let p3 = args.get_kw_arg("p3")?;
let sketch_surface_or_group = args.get_unlabeled_kw_arg("sketch_surface_or_group")?;
let tag = args.get_kw_arg_opt("tag");
let sketch = inner_circle_three_point(p1, p2, p3, sketch_surface_or_group, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
value: Box::new(sketch),
})
}
/// Construct a circle derived from 3 points.
///
/// ```no_run
/// exampleSketch = startSketchOn("XY")
/// |> circleThreePoint(p1 = [10,10], p2 = [20,8], p3 = [15,5])
///
/// example = extrude(5, exampleSketch)
/// ```
#[stdlib {
name = "circleThreePoint",
keywords = true,
unlabeled_first = true,
arg_docs = {
p1 = "1st point to derive the circle.",
p2 = "2nd point to derive the circle.",
p3 = "3rd point to derive the circle.",
sketch_surface_or_group = "Plane or surface to sketch on.",
tag = "Identifier for the circle to reference elsewhere.",
}
}]
async fn inner_circle_three_point(
p1: [f64; 2],
p2: [f64; 2],
p3: [f64; 2],
sketch_surface_or_group: SketchOrSurface,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
let center = calculate_circle_center(p1, p2, p3);
inner_circle(
CircleData {
center,
// It can be the distance to any of the 3 points - they all lay on the circumference.
radius: distance(center.into(), p2.into()),
},
sketch_surface_or_group,
tag,
exec_state,
args,
)
.await
}
/// Type of the polygon
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Default)]
#[ts(export)]

View File

@ -20,8 +20,8 @@ use crate::{
parsing::ast::types::TagNode,
std::{
utils::{
arc_angles, arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
intersection_with_parallel_line, TangentialArcInfoInput,
arc_angles, arc_center_and_end, calculate_circle_center, get_tangential_arc_to_info, get_x_component,
get_y_component, intersection_with_parallel_line, TangentialArcInfoInput,
},
Args,
},
@ -2046,42 +2046,6 @@ async fn inner_tangential_arc_to_relative(
Ok(new_sketch)
}
// Calculate the center of 3 points
// To calculate the center of the 3 point circle 2 perpendicular lines are created
// These perpendicular lines will intersect at the center of the circle.
fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64; 2] {
// y2 - y1
let y_2_1 = p2[1] - p1[1];
// y3 - y2
let y_3_2 = p3[1] - p2[1];
// x2 - x1
let x_2_1 = p2[0] - p1[0];
// x3 - x2
let x_3_2 = p3[0] - p2[0];
// Slope of two perpendicular lines
let slope_a = y_2_1 / x_2_1;
let slope_b = y_3_2 / x_3_2;
// Values for line intersection
// y1 - y3
let y_1_3 = p1[1] - p3[1];
// x1 + x2
let x_1_2 = p1[0] + p2[0];
// x2 + x3
let x_2_3 = p2[0] + p3[0];
// y1 + y2
let y_1_2 = p1[1] + p2[1];
// Solve for the intersection of these two lines
let numerator = (slope_a * slope_b * y_1_3) + (slope_b * x_1_2) - (slope_a * x_2_3);
let x = numerator / (2.0 * (slope_b - slope_a));
let y = ((-1.0 / slope_a) * (x - (x_1_2 / 2.0))) + (y_1_2 / 2.0);
[x, y]
}
/// Data to draw a bezier curve.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -2259,10 +2223,7 @@ mod tests {
use pretty_assertions::assert_eq;
use crate::{
execution::TagIdentifier,
std::sketch::{calculate_circle_center, PlaneData},
};
use crate::{execution::TagIdentifier, std::sketch::PlaneData, std::utils::calculate_circle_center};
#[test]
fn test_deserialize_plane_data() {

View File

@ -8,6 +8,11 @@ use crate::{
source_range::SourceRange,
};
/// Get the distance between two points.
pub fn distance(a: Point2d, b: Point2d) -> f64 {
((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt()
}
/// Get the angle between these points
pub fn between(a: Point2d, b: Point2d) -> Angle {
let x = b.x - a.x;
@ -229,6 +234,42 @@ pub fn is_on_circumference(center: Point2d, point: Point2d, radius: f64) -> bool
(distance_squared - radius.powi(2)).abs() < 1e-9
}
// Calculate the center of 3 points
// To calculate the center of the 3 point circle 2 perpendicular lines are created
// These perpendicular lines will intersect at the center of the circle.
pub fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64; 2] {
// y2 - y1
let y_2_1 = p2[1] - p1[1];
// y3 - y2
let y_3_2 = p3[1] - p2[1];
// x2 - x1
let x_2_1 = p2[0] - p1[0];
// x3 - x2
let x_3_2 = p3[0] - p2[0];
// Slope of two perpendicular lines
let slope_a = y_2_1 / x_2_1;
let slope_b = y_3_2 / x_3_2;
// Values for line intersection
// y1 - y3
let y_1_3 = p1[1] - p3[1];
// x1 + x2
let x_1_2 = p1[0] + p2[0];
// x2 + x3
let x_2_3 = p2[0] + p3[0];
// y1 + y2
let y_1_2 = p1[1] + p2[1];
// Solve for the intersection of these two lines
let numerator = (slope_a * slope_b * y_1_3) + (slope_b * x_1_2) - (slope_a * x_2_3);
let x = numerator / (2.0 * (slope_b - slope_a));
let y = ((-1.0 / slope_a) * (x - (x_1_2 / 2.0))) + (y_1_2 / 2.0);
[x, y]
}
#[cfg(test)]
mod tests {
// Here you can bring your functions into scope

View File

@ -0,0 +1,217 @@
---
source: kcl/src/simulation_tests.rs
assertion_line: 55
description: Result of parsing circle_three_point.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 98,
"id": {
"end": 9,
"name": "sketch001",
"start": 0,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"end": 30,
"raw": "'XY'",
"start": 26,
"type": "Literal",
"type": "Literal",
"value": "XY"
}
],
"callee": {
"end": 25,
"name": "startSketchOn",
"start": 12,
"type": "Identifier"
},
"end": 31,
"start": 12,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "p1"
},
"arg": {
"elements": [
{
"end": 62,
"raw": "25",
"start": 60,
"type": "Literal",
"type": "Literal",
"value": 25.0
},
{
"end": 66,
"raw": "25",
"start": 64,
"type": "Literal",
"type": "Literal",
"value": 25.0
}
],
"end": 67,
"start": 59,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
},
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "p2"
},
"arg": {
"elements": [
{
"end": 77,
"raw": "30",
"start": 75,
"type": "Literal",
"type": "Literal",
"value": 30.0
},
{
"end": 81,
"raw": "20",
"start": 79,
"type": "Literal",
"type": "Literal",
"value": 20.0
}
],
"end": 82,
"start": 74,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
},
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "p3"
},
"arg": {
"elements": [
{
"end": 92,
"raw": "27",
"start": 90,
"type": "Literal",
"type": "Literal",
"value": 27.0
},
{
"end": 96,
"raw": "15",
"start": 94,
"type": "Literal",
"type": "Literal",
"value": 15.0
}
],
"end": 97,
"start": 89,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"callee": {
"end": 53,
"name": "circleThreePoint",
"start": 37,
"type": "Identifier"
},
"end": 98,
"start": 37,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
}
],
"end": 98,
"start": 12,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 98,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 131,
"id": {
"end": 106,
"name": "example",
"start": 99,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 119,
"raw": "10",
"start": 117,
"type": "Literal",
"type": "Literal",
"value": 10.0
},
{
"end": 130,
"name": "sketch001",
"start": 121,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 116,
"name": "extrude",
"start": 109,
"type": "Identifier"
},
"end": 131,
"start": 109,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 99,
"type": "VariableDeclarator"
},
"end": 131,
"kind": "const",
"start": 99,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 132,
"start": 0
}
}

View File

@ -0,0 +1,3 @@
sketch001 = startSketchOn('XY')
|> circleThreePoint(p1 = [25, 25], p2 = [30, 20], p3 = [27, 15])
example = extrude(10, sketch001)

View File

@ -0,0 +1,53 @@
---
source: kcl/src/simulation_tests.rs
assertion_line: 108
description: Operations executed circle_three_point.kcl
snapshot_kind: text
---
[
{
"labeledArgs": {
"data": {
"sourceRange": [
26,
30,
0
]
}
},
"name": "startSketchOn",
"sourceRange": [
12,
31,
0
],
"type": "StdLibCall",
"unlabeledArg": null
},
{
"labeledArgs": {
"length": {
"sourceRange": [
117,
119,
0
]
},
"sketch_set": {
"sourceRange": [
121,
130,
0
]
}
},
"name": "extrude",
"sourceRange": [
109,
131,
0
],
"type": "StdLibCall",
"unlabeledArg": null
}
]

View File

@ -0,0 +1,242 @@
---
source: kcl/src/simulation_tests.rs
assertion_line: 99
description: Program memory after executing circle_three_point.kcl
snapshot_kind: text
---
{
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
},
"example": {
"type": "Solid",
"type": "Solid",
"id": "[uuid]",
"value": [
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
37,
98,
0
],
"tag": null,
"type": "extrudeArc"
}
],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
37,
98,
0
]
},
"ccw": true,
"center": [
24.749999999999996,
19.749999999999996
],
"from": [
30.0059,
19.75
],
"radius": 5.255949010407163,
"tag": null,
"to": [
30.0059,
19.75
],
"type": "Circle"
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"__meta": []
},
"start": {
"from": [
30.00594901040716,
19.749999999999996
],
"to": [
30.00594901040716,
19.749999999999996
],
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
37,
98,
0
]
}
},
"__meta": [
{
"sourceRange": [
37,
98,
0
]
}
]
},
"height": 10.0,
"startCapId": "[uuid]",
"endCapId": "[uuid]",
"__meta": [
{
"sourceRange": [
37,
98,
0
]
}
]
},
"sketch001": {
"type": "Sketch",
"value": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
37,
98,
0
]
},
"ccw": true,
"center": [
24.749999999999996,
19.749999999999996
],
"from": [
30.0059,
19.75
],
"radius": 5.255949010407163,
"tag": null,
"to": [
30.0059,
19.75
],
"type": "Circle"
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"__meta": []
},
"start": {
"from": [
30.00594901040716,
19.749999999999996
],
"to": [
30.00594901040716,
19.749999999999996
],
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
37,
98,
0
]
}
},
"__meta": [
{
"sourceRange": [
37,
98,
0
]
}
]
}
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB