Move the wasm lib, and cleanup rust directory and all references (#5585)

* git mv src/wasm-lib rust

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* mv wasm-lib to workspace

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* mv kcl-lib

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* mv derive docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* resolve file paths

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* move more shit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix more paths

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make yarn build:wasm work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix scripts

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixups

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* better references

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix cargo ci

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix reference

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix more ci

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cargo sort

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix script

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix a dep

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* sort

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove unused deps

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Revert "remove unused deps"

This reverts commit fbabdb062e275fd5cbc1476f8480a1afee15d972.

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* deps;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2025-03-01 13:59:01 -08:00
committed by GitHub
parent 0a2bf4b55f
commit c3bdc6f106
1443 changed files with 509 additions and 4274 deletions

View File

@ -0,0 +1,327 @@
//! Standard library appearance.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc, shared::Color};
use regex::Regex;
use rgba_simple::Hex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use validator::Validate;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Solid, SolidSet},
std::Args,
};
lazy_static::lazy_static! {
static ref HEX_REGEX: Regex = Regex::new(r"^#[0-9a-fA-F]{6}$").unwrap();
}
/// Data for appearance.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct AppearanceData {
/// Color of the new material, a hex string like "#ff0000".
#[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
pub color: String,
/// Metalness of the new material, a percentage like 95.7.
#[validate(range(min = 0.0, max = 100.0))]
pub metalness: Option<f64>,
/// Roughness of the new material, a percentage like 95.7.
#[validate(range(min = 0.0, max = 100.0))]
pub roughness: Option<f64>,
// TODO(jess): we can also ambient occlusion here I just don't know what it is.
}
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
let color: String = args.get_kw_arg("color")?;
let metalness: Option<f64> = args.get_kw_arg_opt("metalness")?;
let roughness: Option<f64> = args.get_kw_arg_opt("roughness")?;
let data = AppearanceData {
color,
metalness,
roughness,
};
// Validate the data.
data.validate().map_err(|err| {
KclError::Semantic(KclErrorDetails {
message: format!("Invalid appearance data: {}", err),
source_ranges: vec![args.source_range],
})
})?;
// Make sure the color if set is valid.
if !HEX_REGEX.is_match(&data.color) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Invalid hex color (`{}`), try something like `#fff000`", data.color),
source_ranges: vec![args.source_range],
}));
}
let result = inner_appearance(solid_set, data.color, data.metalness, data.roughness, args).await?;
Ok(result.into())
}
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
///
/// This will work on any solid, including extruded solids, revolved solids, and shelled solids.
/// ```no_run
/// // Add color to an extruded solid.
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> line(endAbsolute = [10, 0])
/// |> line(endAbsolute = [0, 10])
/// |> line(endAbsolute = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// // There are other options besides 'color', but they're optional.
/// |> appearance(color='#ff0000')
/// ```
///
/// ```no_run
/// // Add color to a revolved solid.
/// sketch001 = startSketchOn('XY')
/// |> circle( center = [15, 0], radius = 5 )
/// |> revolve({ angle = 360, axis = 'y' }, %)
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// )
/// ```
///
/// ```no_run
/// // Add color to different solids.
/// fn cube(center) {
/// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %)
/// |> line(endAbsolute = [center[0] + 10, center[1] - 10])
/// |> line(endAbsolute = [center[0] + 10, center[1] + 10])
/// |> line(endAbsolute = [center[0] - 10, center[1] + 10])
/// |> close()
/// |> extrude(length = 10)
/// }
///
/// example0 = cube([0, 0])
/// example1 = cube([20, 0])
/// example2 = cube([40, 0])
///
/// appearance([example0, example1], color='#ff0000', metalness=50, roughness=50)
/// appearance(example2, color='#00ff00', metalness=50, roughness=50)
/// ```
///
/// ```no_run
/// // You can set the appearance before or after you shell it will yield the same result.
/// // This example shows setting the appearance _after_ the shell.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
///
/// shell(
/// firstSketch,
/// faces = ['end'],
/// thickness = 0.25,
/// )
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// )
/// ```
///
/// ```no_run
/// // You can set the appearance before or after you shell it will yield the same result.
/// // This example shows setting the appearance _before_ the shell.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// )
///
/// shell(
/// firstSketch,
/// faces = ['end'],
/// thickness = 0.25,
/// )
/// ```
///
/// ```no_run
/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
/// // This example shows _before_ the pattern.
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 2])
/// |> line(end = [3, 1])
/// |> line(end = [0, -4])
/// |> close()
///
/// example = extrude(exampleSketch, length = 1)
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// )
/// |> patternLinear3d(
/// axis = [1, 0, 1],
/// instances = 7,
/// distance = 6
/// )
/// ```
///
/// ```no_run
/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
/// // This example shows _after_ the pattern.
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 2])
/// |> line(end = [3, 1])
/// |> line(end = [0, -4])
/// |> close()
///
/// example = extrude(exampleSketch, length = 1)
/// |> patternLinear3d(
/// axis = [1, 0, 1],
/// instances = 7,
/// distance = 6
/// )
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// )
/// ```
///
/// ```no_run
/// // Color the result of a 2D pattern that was extruded.
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([.5, 25], %)
/// |> line(end = [0, 5])
/// |> line(end = [-1, 0])
/// |> line(end = [0, -5])
/// |> close()
/// |> patternCircular2d(
/// center = [0, 0],
/// instances = 13,
/// arcDegrees = 360,
/// rotateDuplicates = true
/// )
///
/// example = extrude(exampleSketch, length = 1)
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// )
/// ```
///
/// ```no_run
/// // Color the result of a sweep.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn('XZ')
/// |> startProfileAt([0.05, 0.05], %)
/// |> line(end = [0, 7])
/// |> tangentialArc({
/// offset: 90,
/// radius: 5
/// }, %)
/// |> line(end = [-3, 0])
/// |> tangentialArc({
/// offset: -90,
/// radius: 5
/// }, %)
/// |> line(end = [0, 7])
///
/// pipeHole = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 1.5,
/// )
///
/// sweepSketch = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 2,
/// )
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> appearance(
/// color = "#ff0000",
/// metalness = 50,
/// roughness = 50
/// )
/// ```
#[stdlib {
name = "appearance",
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "The solid(s) whose appearance is being set" },
color = { docs = "Color of the new material, a hex string like '#ff0000'"},
metalness = { docs = "Metalness of the new material, a percentage like 95.7." },
roughness = { docs = "Roughness of the new material, a percentage like 95.7." },
}
}]
async fn inner_appearance(
solid_set: SolidSet,
color: String,
metalness: Option<f64>,
roughness: Option<f64>,
args: Args,
) -> Result<SolidSet, KclError> {
let solids: Vec<Box<Solid>> = solid_set.into();
for solid in &solids {
// Set the material properties.
let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
KclError::Semantic(KclErrorDetails {
message: format!("Invalid hex color (`{color}`): {err}"),
source_ranges: vec![args.source_range],
})
})?;
let color = Color {
r: rgb.red,
g: rgb.green,
b: rgb.blue,
a: 100.0,
};
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
object_id: solid.id,
color,
metalness: metalness.unwrap_or_default() as f32 / 100.0,
roughness: roughness.unwrap_or_default() as f32 / 100.0,
ambient_occlusion: 0.0,
}),
)
.await?;
// Idk if we want to actually modify the memory for the colors, but I'm not right now since
// I can't think of a use case for it.
}
Ok(SolidSet::from(solids))
}

1584
rust/kcl-lib/src/std/args.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,304 @@
use kcl_derive_docs::stdlib;
use super::{
args::{Arg, FromArgs},
Args,
};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
kcl_value::{FunctionSource, KclValue},
ExecState,
},
source_range::SourceRange,
ExecutorContext,
};
/// Apply a function to each element of an array.
pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (array, f): (Vec<KclValue>, &FunctionSource) = FromArgs::from_args(&args, 0)?;
let meta = vec![args.source_range.into()];
let new_array = inner_map(array, f, exec_state, &args).await?;
Ok(KclValue::Array { value: new_array, meta })
}
/// Apply a function to every element of a list.
///
/// Given a list like `[a, b, c]`, and a function like `f`, returns
/// `[f(a), f(b), f(c)]`
/// ```no_run
/// r = 10 // radius
/// fn drawCircle(id) {
/// return startSketchOn("XY")
/// |> circle( center= [id * 2 * r, 0], radius= r)
/// }
///
/// // Call `drawCircle`, passing in each element of the array.
/// // The outputs from each `drawCircle` form a new array,
/// // which is the return value from `map`.
/// circles = map(
/// [1..3],
/// drawCircle
/// )
/// ```
/// ```no_run
/// r = 10 // radius
/// // Call `map`, using an anonymous function instead of a named one.
/// circles = map(
/// [1..3],
/// fn(id) {
/// return startSketchOn("XY")
/// |> circle( center= [id * 2 * r, 0], radius= r)
/// }
/// )
/// ```
#[stdlib {
name = "map",
}]
async fn inner_map<'a>(
array: Vec<KclValue>,
map_fn: &'a FunctionSource,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<Vec<KclValue>, KclError> {
let mut new_array = Vec::with_capacity(array.len());
for elem in array {
let new_elem = call_map_closure(elem, map_fn, args.source_range, exec_state, &args.ctx).await?;
new_array.push(new_elem);
}
Ok(new_array)
}
async fn call_map_closure(
input: KclValue,
map_fn: &FunctionSource,
source_range: SourceRange,
exec_state: &mut ExecState,
ctxt: &ExecutorContext,
) -> Result<KclValue, KclError> {
let output = map_fn
.call(exec_state, ctxt, vec![Arg::synthetic(input)], source_range)
.await?;
let source_ranges = vec![source_range];
let output = output.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: "Map function must return a value".to_string(),
source_ranges,
})
})?;
Ok(output)
}
/// For each item in an array, update a value.
pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (array, start, f): (Vec<KclValue>, KclValue, &FunctionSource) = FromArgs::from_args(&args, 0)?;
inner_reduce(array, start, f, exec_state, &args).await
}
/// Take a starting value. Then, for each element of an array, calculate the next value,
/// using the previous value and the element.
/// ```no_run
/// // This function adds two numbers.
/// fn add(a, b) { return a + b }
///
/// // This function adds an array of numbers.
/// // It uses the `reduce` function, to call the `add` function on every
/// // element of the `arr` parameter. The starting value is 0.
/// fn sum(arr) { return reduce(arr, 0, add) }
///
/// /*
/// The above is basically like this pseudo-code:
/// fn sum(arr):
/// sumSoFar = 0
/// for i in arr:
/// sumSoFar = add(sumSoFar, i)
/// return sumSoFar
/// */
///
/// // We use `assertEqual` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
/// ```
/// ```no_run
/// // This example works just like the previous example above, but it uses
/// // an anonymous `add` function as its parameter, instead of declaring a
/// // named function outside.
/// arr = [1, 2, 3]
/// sum = reduce(arr, 0, (i, result_so_far) => { return i + result_so_far })
///
/// // We use `assertEqual` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
/// ```
/// ```no_run
/// // Declare a function that sketches a decagon.
/// fn decagon(radius) {
/// // Each side of the decagon is turned this many degrees from the previous angle.
/// stepAngle = (1/10) * TAU
///
/// // Start the decagon sketch at this point.
/// startOfDecagonSketch = startSketchOn('XY')
/// |> startProfileAt([(cos(0)*radius), (sin(0) * radius)], %)
///
/// // Use a `reduce` to draw the remaining decagon sides.
/// // For each number in the array 1..10, run the given function,
/// // which takes a partially-sketched decagon and adds one more edge to it.
/// fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
/// // Draw one edge of the decagon.
/// x = cos(stepAngle * i) * radius
/// y = sin(stepAngle * i) * radius
/// return line(partialDecagon, end = [x, y])
/// })
///
/// return fullDecagon
///
/// }
///
/// /*
/// The `decagon` above is basically like this pseudo-code:
/// fn decagon(radius):
/// stepAngle = (1/10) * TAU
/// plane = startSketchOn('XY')
/// startOfDecagonSketch = startProfileAt([(cos(0)*radius), (sin(0) * radius)], plane)
///
/// // Here's the reduce part.
/// partialDecagon = startOfDecagonSketch
/// for i in [1..10]:
/// x = cos(stepAngle * i) * radius
/// y = sin(stepAngle * i) * radius
/// partialDecagon = line(partialDecagon, end = [x, y])
/// fullDecagon = partialDecagon // it's now full
/// return fullDecagon
/// */
///
/// // Use the `decagon` function declared above, to sketch a decagon with radius 5.
/// decagon(5.0) |> close()
/// ```
#[stdlib {
name = "reduce",
}]
async fn inner_reduce<'a>(
array: Vec<KclValue>,
start: KclValue,
reduce_fn: &'a FunctionSource,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<KclValue, KclError> {
let mut reduced = start;
for elem in array {
reduced = call_reduce_closure(elem, reduced, reduce_fn, args.source_range, exec_state, &args.ctx).await?;
}
Ok(reduced)
}
async fn call_reduce_closure(
elem: KclValue,
start: KclValue,
reduce_fn: &FunctionSource,
source_range: SourceRange,
exec_state: &mut ExecState,
ctxt: &ExecutorContext,
) -> Result<KclValue, KclError> {
// Call the reduce fn for this repetition.
let reduce_fn_args = vec![Arg::synthetic(elem), Arg::synthetic(start)];
let transform_fn_return = reduce_fn.call(exec_state, ctxt, reduce_fn_args, source_range).await?;
// Unpack the returned transform object.
let source_ranges = vec![source_range];
let out = transform_fn_return.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: "Reducer function must return a value".to_string(),
source_ranges: source_ranges.clone(),
})
})?;
Ok(out)
}
/// Append an element to the end of an array.
///
/// Returns a new array with the element appended.
///
/// ```no_run
/// arr = [1, 2, 3]
/// new_arr = push(arr, 4)
/// assertEqual(new_arr[3], 4, 0.00001, "4 was added to the end of the array")
/// ```
#[stdlib {
name = "push",
}]
async fn inner_push(mut array: Vec<KclValue>, elem: KclValue, args: &Args) -> Result<KclValue, KclError> {
// Unwrap the KclValues to JValues for manipulation
array.push(elem);
Ok(KclValue::Array {
value: array,
meta: vec![args.source_range.into()],
})
}
pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
// Extract the array and the element from the arguments
let (val, elem): (KclValue, KclValue) = FromArgs::from_args(&args, 0)?;
let meta = vec![args.source_range];
let KclValue::Array { value: array, meta: _ } = val else {
let actual_type = val.human_friendly_type();
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: meta,
message: format!("You can't push to a value of type {actual_type}, only an array"),
}));
};
inner_push(array, elem, &args).await
}
/// Remove the last element from an array.
///
/// Returns a new array with the last element removed.
///
/// ```no_run
/// arr = [1, 2, 3, 4]
/// new_arr = pop(arr)
/// assertEqual(new_arr[0], 1, 0.00001, "1 is the first element of the array")
/// assertEqual(new_arr[1], 2, 0.00001, "2 is the second element of the array")
/// assertEqual(new_arr[2], 3, 0.00001, "3 is the third element of the array")
/// ```
#[stdlib {
name = "pop",
keywords = true,
unlabeled_first = true,
args = {
array = { docs = "The array to pop from. Must not be empty."},
}
}]
async fn inner_pop(array: Vec<KclValue>, args: &Args) -> Result<KclValue, KclError> {
if array.is_empty() {
return Err(KclError::Semantic(KclErrorDetails {
message: "Cannot pop from an empty array".to_string(),
source_ranges: vec![args.source_range],
}));
}
// Create a new array with all elements except the last one
let new_array = array[..array.len() - 1].to_vec();
Ok(KclValue::Array {
value: new_array,
meta: vec![args.source_range.into()],
})
}
pub async fn pop(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
// Extract the array from the arguments
let val = args.get_unlabeled_kw_arg("array")?;
let meta = vec![args.source_range];
let KclValue::Array { value: array, meta: _ } = val else {
let actual_type = val.human_friendly_type();
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: meta,
message: format!("You can't pop from a value of type {actual_type}, only an array"),
}));
};
inner_pop(array, &args).await
}

View File

@ -0,0 +1,153 @@
//! Standard library assert functions.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue},
std::Args,
};
async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError> {
if !value {
return Err(KclError::Type(KclErrorDetails {
message: format!("assert failed: {}", message),
source_ranges: vec![args.source_range],
}));
}
Ok(())
}
/// Check that the provided value is true, or raise a [KclError]
/// with the provided description.
pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, description): (bool, String) = args.get_data()?;
inner_assert(data, &description, &args).await?;
Ok(KclValue::none())
}
/// Check a value at runtime, and raise an error if the argument provided
/// is false.
///
/// ```no_run
/// myVar = true
/// assert(myVar, "should always be true")
/// ```
#[stdlib {
name = "assert",
}]
async fn inner_assert(data: bool, message: &str, args: &Args) -> Result<(), KclError> {
_assert(data, message, args).await
}
pub async fn assert_lt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_lt(left, right, &description, &args).await?;
Ok(KclValue::none())
}
/// Check that a numerical value is less than to another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// assertLessThan(1, 2, "1 is less than 2")
/// ```
#[stdlib {
name = "assertLessThan",
}]
async fn inner_assert_lt(left: f64, right: f64, message: &str, args: &Args) -> Result<(), KclError> {
_assert(left < right, message, args).await
}
pub async fn assert_gt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_gt(left, right, &description, &args).await?;
Ok(KclValue::none())
}
/// Check that a numerical value equals another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// n = 1.0285
/// o = 1.0286
/// assertEqual(n, o, 0.01, "n is within the given tolerance for o")
/// ```
#[stdlib {
name = "assertEqual",
}]
async fn inner_assert_equal(left: f64, right: f64, epsilon: f64, message: &str, args: &Args) -> Result<(), KclError> {
if epsilon <= 0.0 {
Err(KclError::Type(KclErrorDetails {
message: "assertEqual epsilon must be greater than zero".to_owned(),
source_ranges: vec![args.source_range],
}))
} else if (right - left).abs() < epsilon {
Ok(())
} else {
Err(KclError::Type(KclErrorDetails {
message: format!("assert failed because {left} != {right}: {message}"),
source_ranges: vec![args.source_range],
}))
}
}
pub async fn assert_equal(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, epsilon, description): (f64, f64, f64, String) = args.get_data()?;
inner_assert_equal(left, right, epsilon, &description, &args).await?;
Ok(KclValue::none())
}
/// Check that a numerical value is greater than another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// assertGreaterThan(2, 1, "2 is greater than 1")
/// ```
#[stdlib {
name = "assertGreaterThan",
}]
async fn inner_assert_gt(left: f64, right: f64, message: &str, args: &Args) -> Result<(), KclError> {
_assert(left > right, message, args).await
}
pub async fn assert_lte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_lte(left, right, &description, &args).await?;
Ok(KclValue::none())
}
/// Check that a numerical value is less than or equal to another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// assertLessThanOrEq(1, 2, "1 is less than 2")
/// assertLessThanOrEq(1, 1, "1 is equal to 1")
/// ```
#[stdlib {
name = "assertLessThanOrEq",
}]
async fn inner_assert_lte(left: f64, right: f64, message: &str, args: &Args) -> Result<(), KclError> {
_assert(left <= right, message, args).await
}
pub async fn assert_gte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_gte(left, right, &description, &args).await?;
Ok(KclValue::none())
}
/// Check that a numerical value is greater than or equal to another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// assertGreaterThanOrEq(2, 1, "2 is greater than 1")
/// assertGreaterThanOrEq(1, 1, "1 is equal to 1")
/// ```
#[stdlib {
name = "assertGreaterThanOrEq",
}]
async fn inner_assert_gte(left: f64, right: f64, message: &str, args: &Args) -> Result<(), KclError> {
_assert(left >= right, message, args).await
}

View File

@ -0,0 +1,233 @@
//! Types for referencing an axis or edge.
use anyhow::Result;
use kcmc::length_unit::LengthUnit;
use kittycad_modeling_cmds::{self as kcmc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{errors::KclError, std::fillet::EdgeReference};
/// A 2D axis or tagged edge.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(untagged)]
pub enum Axis2dOrEdgeReference {
/// 2D axis and origin.
Axis(AxisAndOrigin2d),
/// Tagged edge.
Edge(EdgeReference),
}
/// A 2D axis and origin.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum AxisAndOrigin2d {
/// X-axis.
#[serde(rename = "X", alias = "x")]
X,
/// Y-axis.
#[serde(rename = "Y", alias = "y")]
Y,
/// Flip the X-axis.
#[serde(rename = "-X", alias = "-x")]
NegX,
/// Flip the Y-axis.
#[serde(rename = "-Y", alias = "-y")]
NegY,
Custom {
/// The axis.
axis: [f64; 2],
/// The origin.
origin: [f64; 2],
},
}
impl AxisAndOrigin2d {
/// Get the axis and origin.
pub fn axis_and_origin(&self) -> Result<(kcmc::shared::Point3d<f64>, kcmc::shared::Point3d<LengthUnit>), KclError> {
let (axis, origin) = match self {
AxisAndOrigin2d::X => ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin2d::Y => ([0.0, 1.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin2d::NegX => ([-1.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin2d::NegY => ([0.0, -1.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin2d::Custom { axis, origin } => ([axis[0], axis[1], 0.0], [origin[0], origin[1], 0.0]),
};
Ok((
kcmc::shared::Point3d {
x: axis[0],
y: axis[1],
z: axis[2],
},
kcmc::shared::Point3d {
x: LengthUnit(origin[0]),
y: LengthUnit(origin[1]),
z: LengthUnit(origin[2]),
},
))
}
}
/// A 3D axis or tagged edge.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(untagged)]
pub enum Axis3dOrEdgeReference {
/// 3D axis and origin.
Axis(AxisAndOrigin3d),
/// Tagged edge.
Edge(EdgeReference),
}
/// A 3D axis and origin.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum AxisAndOrigin3d {
/// X-axis.
#[serde(rename = "X", alias = "x")]
X,
/// Y-axis.
#[serde(rename = "Y", alias = "y")]
Y,
/// Z-axis.
#[serde(rename = "Z", alias = "z")]
Z,
/// Flip the X-axis.
#[serde(rename = "-X", alias = "-x")]
NegX,
/// Flip the Y-axis.
#[serde(rename = "-Y", alias = "-y")]
NegY,
/// Flip the Z-axis.
#[serde(rename = "-Z", alias = "-z")]
NegZ,
Custom {
/// The axis.
axis: [f64; 3],
/// The origin.
origin: [f64; 3],
},
}
impl AxisAndOrigin3d {
/// Get the axis and origin.
pub fn axis_and_origin(&self) -> Result<(kcmc::shared::Point3d<f64>, kcmc::shared::Point3d<LengthUnit>), KclError> {
let (axis, origin) = match self {
AxisAndOrigin3d::X => ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::Y => ([0.0, 1.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::Z => ([0.0, 0.0, 1.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::NegX => ([-1.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::NegY => ([0.0, -1.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::NegZ => ([0.0, 0.0, -1.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::Custom { axis, origin } => {
([axis[0], axis[1], axis[2]], [origin[0], origin[1], origin[2]])
}
};
Ok((
kcmc::shared::Point3d {
x: axis[0],
y: axis[1],
z: axis[2],
},
kcmc::shared::Point3d {
x: LengthUnit(origin[0]),
y: LengthUnit(origin[1]),
z: LengthUnit(origin[2]),
},
))
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::std::axis_or_reference::{
Axis2dOrEdgeReference, Axis3dOrEdgeReference, AxisAndOrigin2d, AxisAndOrigin3d,
};
#[test]
fn test_deserialize_revolve_axis_2d() {
let data = Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::X);
let mut str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, "\"X\"");
str_json = "\"Y\"".to_string();
let data: Axis2dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::Y));
str_json = "\"-Y\"".to_string();
let data: Axis2dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::NegY));
str_json = "\"-x\"".to_string();
let data: Axis2dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::NegX));
let data = Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::Custom {
axis: [0.0, -1.0],
origin: [1.0, 0.0],
});
str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, r#"{"custom":{"axis":[0.0,-1.0],"origin":[1.0,0.0]}}"#);
str_json = r#"{"custom": {"axis": [0,-1], "origin": [1,2.0]}}"#.to_string();
let data: Axis2dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(
data,
Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::Custom {
axis: [0.0, -1.0],
origin: [1.0, 2.0]
})
);
}
#[test]
fn test_deserialize_revolve_axis_3d() {
let data = Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::X);
let mut str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, "\"X\"");
str_json = "\"Y\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::Y));
str_json = "\"Z\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::Z));
str_json = "\"-Y\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::NegY));
str_json = "\"-x\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::NegX));
str_json = "\"-z\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::NegZ));
let data = Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::Custom {
axis: [0.0, -1.0, 0.0],
origin: [1.0, 0.0, 0.0],
});
str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, r#"{"custom":{"axis":[0.0,-1.0,0.0],"origin":[1.0,0.0,0.0]}}"#);
str_json = r#"{"custom": {"axis": [0,-1,0], "origin": [1,2.0,0]}}"#.to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(
data,
Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::Custom {
axis: [0.0, -1.0, 0.0],
origin: [1.0, 2.0, 0.0]
})
);
}
}

View File

@ -0,0 +1,172 @@
//! Standard library chamfers.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use super::utils::unique_count;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid},
parsing::ast::types::TagNode,
std::{fillet::EdgeReference, Args},
};
pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
/// Create chamfers on tagged paths.
pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?;
let length = args.get_kw_arg("length")?;
let tags = args.get_kw_arg("tags")?;
let tag = args.get_kw_arg_opt("tag")?;
let value = inner_chamfer(solid, length, tags, tag, exec_state, args).await?;
Ok(KclValue::Solid { value })
}
/// Cut a straight transitional edge along a tagged path.
///
/// Chamfer is similar in function and use to a fillet, except
/// a fillet will blend the transition along an edge, rather than cut
/// a sharp, straight transitional edge.
///
/// ```no_run
/// // Chamfer a mounting plate.
/// width = 20
/// length = 10
/// thickness = 1
/// chamferLength = 2
///
/// mountingPlateSketch = startSketchOn("XY")
/// |> startProfileAt([-width/2, -length/2], %)
/// |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
/// |> line(endAbsolute = [width/2, length/2], tag = $edge2)
/// |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
/// |> close(tag = $edge4)
///
/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
/// |> chamfer(
/// length = chamferLength,
/// tags = [
/// getNextAdjacentEdge(edge1),
/// getNextAdjacentEdge(edge2),
/// getNextAdjacentEdge(edge3),
/// getNextAdjacentEdge(edge4)
/// ],
/// )
/// ```
///
/// ```no_run
/// // Sketch on the face of a chamfer.
/// fn cube(pos, scale) {
/// sg = startSketchOn('XY')
/// |> startProfileAt(pos, %)
/// |> line(end = [0, scale])
/// |> line(end = [scale, 0])
/// |> line(end = [0, -scale])
///
/// return sg
/// }
///
/// part001 = cube([0,0], 20)
/// |> close(tag = $line1)
/// |> extrude(length = 20)
/// // We tag the chamfer to reference it later.
/// |> chamfer(
/// length = 10,
/// tags = [getOppositeEdge(line1)],
/// tag = $chamfer1,
/// )
///
/// sketch001 = startSketchOn(part001, chamfer1)
/// |> startProfileAt([10, 10], %)
/// |> line(end = [2, 0])
/// |> line(end = [0, 2])
/// |> line(end = [-2, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
/// |> extrude(length = 10)
/// ```
#[stdlib {
name = "chamfer",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
solid = { docs = "The solid whose edges should be chamfered" },
length = { docs = "The length of the chamfer" },
tags = { docs = "The paths you want to chamfer" },
tag = { docs = "Create a new tag which refers to this chamfer"},
}
}]
async fn inner_chamfer(
solid: Box<Solid>,
length: f64,
tags: Vec<EdgeReference>,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
// Check if tags contains any duplicate values.
let unique_tags = unique_count(tags.clone());
if unique_tags != tags.len() {
return Err(KclError::Type(KclErrorDetails {
message: "Duplicate tags are not allowed.".to_string(),
source_ranges: vec![args.source_range],
}));
}
// If you try and tag multiple edges with a tagged chamfer, we want to return an
// error to the user that they can only tag one edge at a time.
if tag.is_some() && tags.len() > 1 {
return Err(KclError::Type(KclErrorDetails {
message: "You can only tag one edge at a time with a tagged chamfer. Either delete the tag for the chamfer fn if you don't need it OR separate into individual chamfer functions for each tag.".to_string(),
source_ranges: vec![args.source_range],
}));
}
let mut solid = solid.clone();
for edge_tag in tags {
let edge_id = match edge_tag {
EdgeReference::Uuid(uuid) => uuid,
EdgeReference::Tag(edge_tag) => args.get_tag_engine_info(exec_state, &edge_tag)?.id,
};
let id = exec_state.next_uuid();
args.batch_end_cmd(
id,
ModelingCmd::from(mcmd::Solid3dFilletEdge {
edge_id,
object_id: solid.id,
radius: LengthUnit(length),
tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
cut_type: CutType::Chamfer,
// We make this a none so that we can remove it in the future.
face_id: None,
}),
)
.await?;
solid.edge_cuts.push(EdgeCut::Chamfer {
id,
edge_id,
length,
tag: Box::new(tag.clone()),
});
if let Some(ref tag) = tag {
solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
face_id: id,
tag: Some(tag.clone()),
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
}));
}
}
Ok(solid)
}

View File

@ -0,0 +1,41 @@
//! Conversions between types.
use kcl_derive_docs::stdlib;
use crate::{
errors::KclError,
execution::{ExecState, KclValue},
std::Args,
};
/// Converts a number to integer.
pub async fn int(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number_with_type()?;
let converted = inner_int(num.n)?;
Ok(args.make_user_val_from_f64_with_type(num.map(converted)))
}
/// Convert a number to an integer.
///
/// DEPRECATED use floor(), ceil(), or round().
///
/// ```no_run
/// n = int(ceil(5/2))
/// assertEqual(n, 3, 0.0001, "5/2 = 2.5, rounded up makes 3")
/// // Draw n cylinders.
/// startSketchOn('XZ')
/// |> circle(center = [0, 0], radius = 2 )
/// |> extrude(length = 5)
/// |> patternTransform(instances = n, transform = fn(id) {
/// return { translate = [4 * id, 0, 0] }
/// })
/// ```
#[stdlib {
name = "int",
tags = ["convert"],
deprecated = true,
}]
fn inner_int(num: f64) -> Result<f64, KclError> {
Ok(num)
}

View File

@ -0,0 +1,343 @@
//! Functions related to extruding.
use std::collections::HashMap;
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{
each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, output::ExtrusionFaceInfo,
shared::ExtrusionFaceCapType, websocket::OkWebSocketResponseData, ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSet, SketchSurface, Solid,
SolidSet,
},
std::Args,
};
/// Extrudes by a given amount.
pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_set = args.get_unlabeled_kw_arg("sketch_set")?;
let length = args.get_kw_arg("length")?;
let result = inner_extrude(sketch_set, length, exec_state, args).await?;
Ok(result.into())
}
/// Extend a 2-dimensional sketch through a third dimension in order to
/// create new 3-dimensional volume, or if extruded into an existing volume,
/// cut into an existing solid.
///
/// ```no_run
/// example = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> arc({
/// angleStart = 120,
/// angleEnd = 0,
/// radius = 5,
/// }, %)
/// |> line(end = [5, 0])
/// |> line(end = [0, 10])
/// |> bezierCurve({
/// control1 = [-10, 0],
/// control2 = [2, 10],
/// to = [-5, 10],
/// }, %)
/// |> line(end = [-5, -2])
/// |> close()
/// |> extrude(length = 10)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([-10, 0], %)
/// |> arc({
/// angleStart = 120,
/// angleEnd = -60,
/// radius = 5,
/// }, %)
/// |> line(end = [10, 0])
/// |> line(end = [5, 0])
/// |> bezierCurve({
/// control1 = [-3, 0],
/// control2 = [2, 10],
/// to = [-5, 10],
/// }, %)
/// |> line(end = [-4, 10])
/// |> line(end = [-5, -2])
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "extrude",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
sketch_set = { docs = "Which sketches should be extruded"},
length = { docs = "How far to extrude the given sketches"},
}
}]
async fn inner_extrude(
sketch_set: SketchSet,
length: f64,
exec_state: &mut ExecState,
args: Args,
) -> Result<SolidSet, KclError> {
let id = exec_state.next_uuid();
// Extrude the element(s).
let sketches: Vec<Sketch> = sketch_set.into();
let mut solids = Vec::new();
for sketch in &sketches {
// Before we extrude, we need to enable the sketch mode.
// We do this here in case extrude is called out of order.
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::EnableSketchMode {
animated: false,
ortho: false,
entity_id: sketch.on.id(),
adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch.on {
// We pass in the normal for the plane here.
Some(plane.z_axis.into())
} else {
None
},
}),
)
.await?;
// TODO: We're reusing the same UUID for multiple commands. This seems
// like the artifact graph would never be able to find all the
// responses.
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Extrude {
target: sketch.id.into(),
distance: LengthUnit(length),
faces: Default::default(),
}),
)
.await?;
// Disable the sketch mode.
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
)
.await?;
solids.push(do_post_extrude(sketch.clone(), id.into(), length, exec_state, args.clone()).await?);
}
Ok(solids.into())
}
pub(crate) async fn do_post_extrude(
sketch: Sketch,
solid_id: ArtifactId,
length: f64,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
// Bring the object to the front of the scene.
// See: https://github.com/KittyCAD/modeling-app/issues/806
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
)
.await?;
// The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
// So, let's just use the first one.
let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
return Err(KclError::Type(KclErrorDetails {
message: "Expected a non-empty sketch".to_string(),
source_ranges: vec![args.source_range],
}));
};
let mut sketch = sketch.clone();
// If we were sketching on a face, we need the original face id.
if let SketchSurface::Face(ref face) = sketch.on {
sketch.id = face.solid.sketch.id;
}
let solid3d_info = args
.send_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
edge_id: any_edge_id,
object_id: sketch.id,
}),
)
.await?;
let face_infos = if let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
} = solid3d_info
{
data.faces
} else {
vec![]
};
for (curve_id, face_id) in face_infos
.iter()
.filter(|face_info| face_info.cap == ExtrusionFaceCapType::None)
.filter_map(|face_info| {
if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
Some((curve_id, face_id))
} else {
None
}
})
{
// Batch these commands, because the Rust code doesn't actually care about the outcome.
// So, there's no need to await them.
// Instead, the Typescript codebases (which handles WebSocket sends when compiled via Wasm)
// uses this to build the artifact graph, which the UI needs.
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
edge_id: curve_id,
object_id: sketch.id,
face_id,
}),
)
.await?;
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
edge_id: curve_id,
object_id: sketch.id,
face_id,
}),
)
.await?;
}
let Faces {
sides: face_id_map,
start_cap_id,
end_cap_id,
} = analyze_faces(exec_state, &args, face_infos).await;
// Iterate over the sketch.value array and add face_id to GeoMeta
let no_engine_commands = args.ctx.no_engine_commands().await;
let new_value = sketch
.paths
.iter()
.flat_map(|path| {
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
match path {
Path::Arc { .. }
| Path::TangentialArc { .. }
| Path::TangentialArcTo { .. }
| Path::Circle { .. }
| Path::CircleThreePoint { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata,
},
});
Some(extrude_surface)
}
Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata,
},
});
Some(extrude_surface)
}
}
} else if no_engine_commands {
// Only pre-populate the extrude surface if we are in mock mode.
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
// pushing this values with a fake face_id to make extrudes mock-execute safe
face_id: exec_state.next_uuid(),
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata,
},
});
Some(extrude_surface)
} else {
None
}
})
.collect();
Ok(Box::new(Solid {
// Ok so you would think that the id would be the id of the solid,
// that we passed in to the function, but it's actually the id of the
// sketch.
id: sketch.id,
artifact_id: solid_id,
value: new_value,
meta: sketch.meta.clone(),
units: sketch.units,
sketch,
height: length,
start_cap_id,
end_cap_id,
edge_cuts: vec![],
}))
}
#[derive(Default)]
struct Faces {
/// Maps curve ID to face ID for each side.
sides: HashMap<Uuid, Option<Uuid>>,
/// Top face ID.
end_cap_id: Option<Uuid>,
/// Bottom face ID.
start_cap_id: Option<Uuid>,
}
async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
let mut faces = Faces {
sides: HashMap::with_capacity(face_infos.len()),
..Default::default()
};
if args.ctx.no_engine_commands().await {
// Create fake IDs for start and end caps, to make extrudes mock-execute safe
faces.start_cap_id = Some(exec_state.next_uuid());
faces.end_cap_id = Some(exec_state.next_uuid());
}
for face_info in face_infos {
match face_info.cap {
ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
ExtrusionFaceCapType::Both => {
faces.end_cap_id = face_info.face_id;
faces.start_cap_id = face_info.face_id;
}
ExtrusionFaceCapType::None => {
if let Some(curve_id) = face_info.curve_id {
faces.sides.insert(curve_id, face_info.face_id);
}
}
}
}
faces
}

View File

@ -0,0 +1,434 @@
//! Standard library fillets.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{
each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::CutType,
websocket::OkWebSocketResponseData, ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::utils::unique_count;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier},
parsing::ast::types::TagNode,
settings::types::UnitLength,
std::Args,
};
/// A tag or a uuid of an edge.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Hash)]
#[ts(export)]
#[serde(untagged)]
pub enum EdgeReference {
/// A uuid of an edge.
Uuid(uuid::Uuid),
/// A tag of an edge.
Tag(Box<TagIdentifier>),
}
impl EdgeReference {
pub fn get_engine_id(&self, exec_state: &mut ExecState, args: &Args) -> Result<uuid::Uuid, KclError> {
match self {
EdgeReference::Uuid(uuid) => Ok(*uuid),
EdgeReference::Tag(tag) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
}
}
}
/// Create fillets on tagged paths.
pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?;
let radius = args.get_kw_arg("radius")?;
let tolerance = args.get_kw_arg_opt("tolerance")?;
let tags = args.get_kw_arg("tags")?;
let tag = args.get_kw_arg_opt("tag")?;
let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
Ok(KclValue::Solid { value })
}
/// Blend a transitional edge along a tagged path, smoothing the sharp edge.
///
/// Fillet is similar in function and use to a chamfer, except
/// a chamfer will cut a sharp transition along an edge while fillet
/// will smoothly blend the transition.
///
/// ```no_run
/// width = 20
/// length = 10
/// thickness = 1
/// filletRadius = 2
///
/// mountingPlateSketch = startSketchOn("XY")
/// |> startProfileAt([-width/2, -length/2], %)
/// |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
/// |> line(endAbsolute = [width/2, length/2], tag = $edge2)
/// |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
/// |> close(tag = $edge4)
///
/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
/// |> fillet(
/// radius = filletRadius,
/// tags = [
/// getNextAdjacentEdge(edge1),
/// getNextAdjacentEdge(edge2),
/// getNextAdjacentEdge(edge3),
/// getNextAdjacentEdge(edge4)
/// ],
/// )
/// ```
///
/// ```no_run
/// width = 20
/// length = 10
/// thickness = 1
/// filletRadius = 1
///
/// mountingPlateSketch = startSketchOn("XY")
/// |> startProfileAt([-width/2, -length/2], %)
/// |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
/// |> line(endAbsolute = [width/2, length/2], tag = $edge2)
/// |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
/// |> close(tag = $edge4)
///
/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
/// |> fillet(
/// radius = filletRadius,
/// tolerance = 0.000001,
/// tags = [
/// getNextAdjacentEdge(edge1),
/// getNextAdjacentEdge(edge2),
/// getNextAdjacentEdge(edge3),
/// getNextAdjacentEdge(edge4)
/// ],
/// )
/// ```
#[stdlib {
name = "fillet",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
solid = { docs = "The solid whose edges should be filletted" },
radius = { docs = "The radius of the fillet" },
tags = { docs = "The paths you want to fillet" },
tolerance = { docs = "The tolerance for this fillet" },
tag = { docs = "Create a new tag which refers to this fillet"},
}
}]
async fn inner_fillet(
solid: Box<Solid>,
radius: f64,
tags: Vec<EdgeReference>,
tolerance: Option<f64>,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
// Check if tags contains any duplicate values.
let unique_tags = unique_count(tags.clone());
if unique_tags != tags.len() {
return Err(KclError::Type(KclErrorDetails {
message: "Duplicate tags are not allowed.".to_string(),
source_ranges: vec![args.source_range],
}));
}
let mut solid = solid.clone();
for edge_tag in tags {
let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
let id = exec_state.next_uuid();
args.batch_end_cmd(
id,
ModelingCmd::from(mcmd::Solid3dFilletEdge {
edge_id,
object_id: solid.id,
radius: LengthUnit(radius),
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
cut_type: CutType::Fillet,
// We make this a none so that we can remove it in the future.
face_id: None,
}),
)
.await?;
solid.edge_cuts.push(EdgeCut::Fillet {
id,
edge_id,
radius,
tag: Box::new(tag.clone()),
});
if let Some(ref tag) = tag {
solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
face_id: id,
tag: Some(tag.clone()),
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
}));
}
}
Ok(solid)
}
/// Get the opposite edge to the edge given.
pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_data()?;
let edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?;
Ok(KclValue::Uuid {
value: edge,
meta: vec![args.source_range.into()],
})
}
/// Get the opposite edge to the edge given.
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> angledLine({
/// angle = 60,
/// length = 10,
/// }, %)
/// |> angledLine({
/// angle = 120,
/// length = 10,
/// }, %)
/// |> line(end = [-10, 0])
/// |> angledLine({
/// angle = 240,
/// length = 10,
/// }, %, $referenceEdge)
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// |> fillet(
/// radius = 3,
/// tags = [getOppositeEdge(referenceEdge)],
/// )
/// ```
#[stdlib {
name = "getOppositeEdge",
}]
async fn inner_get_opposite_edge(tag: TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<Uuid, KclError> {
if args.ctx.no_engine_commands().await {
return Ok(exec_state.next_uuid());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
let resp = args
.send_modeling_cmd(
id,
ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
edge_id: tagged_path.id,
object_id: tagged_path.sketch,
face_id,
}),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {:?}", resp),
source_ranges: vec![args.source_range],
}));
};
Ok(opposite_edge.edge)
}
/// Get the next adjacent edge to the edge given.
pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_data()?;
let edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?;
Ok(KclValue::Uuid {
value: edge,
meta: vec![args.source_range.into()],
})
}
/// Get the next adjacent edge to the edge given.
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> angledLine({
/// angle = 60,
/// length = 10,
/// }, %)
/// |> angledLine({
/// angle = 120,
/// length = 10,
/// }, %)
/// |> line(end = [-10, 0])
/// |> angledLine({
/// angle = 240,
/// length = 10,
/// }, %, $referenceEdge)
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// |> fillet(
/// radius = 3,
/// tags = [getNextAdjacentEdge(referenceEdge)],
/// )
/// ```
#[stdlib {
name = "getNextAdjacentEdge",
}]
async fn inner_get_next_adjacent_edge(
tag: TagIdentifier,
exec_state: &mut ExecState,
args: Args,
) -> Result<Uuid, KclError> {
if args.ctx.no_engine_commands().await {
return Ok(exec_state.next_uuid());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
let resp = args
.send_modeling_cmd(
id,
ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
edge_id: tagged_path.id,
object_id: tagged_path.sketch,
face_id,
}),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!(
"mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {:?}",
resp
),
source_ranges: vec![args.source_range],
}));
};
adjacent_edge.edge.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found next adjacent to tag: `{}`", tag.value),
source_ranges: vec![args.source_range],
})
})
}
/// Get the previous adjacent edge to the edge given.
pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_data()?;
let edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?;
Ok(KclValue::Uuid {
value: edge,
meta: vec![args.source_range.into()],
})
}
/// Get the previous adjacent edge to the edge given.
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> angledLine({
/// angle = 60,
/// length = 10,
/// }, %)
/// |> angledLine({
/// angle = 120,
/// length = 10,
/// }, %)
/// |> line(end = [-10, 0])
/// |> angledLine({
/// angle = 240,
/// length = 10,
/// }, %, $referenceEdge)
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// |> fillet(
/// radius = 3,
/// tags = [getPreviousAdjacentEdge(referenceEdge)],
/// )
/// ```
#[stdlib {
name = "getPreviousAdjacentEdge",
}]
async fn inner_get_previous_adjacent_edge(
tag: TagIdentifier,
exec_state: &mut ExecState,
args: Args,
) -> Result<Uuid, KclError> {
if args.ctx.no_engine_commands().await {
return Ok(exec_state.next_uuid());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
let resp = args
.send_modeling_cmd(
id,
ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge {
edge_id: tagged_path.id,
object_id: tagged_path.sketch,
face_id,
}),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!(
"mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {:?}",
resp
),
source_ranges: vec![args.source_range],
}));
};
adjacent_edge.edge.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found previous adjacent to tag: `{}`", tag.value),
source_ranges: vec![args.source_range],
})
})
}
pub(crate) fn default_tolerance(units: &UnitLength) -> f64 {
match units {
UnitLength::Mm => 0.0000001,
UnitLength::Cm => 0.0000001,
UnitLength::In => 0.0000001,
UnitLength::Ft => 0.0001,
UnitLength::Yd => 0.001,
UnitLength::M => 0.001,
}
}

View File

@ -0,0 +1,241 @@
//! Standard library helices.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
execution::{ExecState, Helix as HelixValue, KclValue, Solid},
std::{axis_or_reference::Axis3dOrEdgeReference, Args},
};
/// Create a helix.
pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let angle_start = args.get_kw_arg("angleStart")?;
let revolutions = args.get_kw_arg("revolutions")?;
let ccw = args.get_kw_arg_opt("ccw")?;
let radius = args.get_kw_arg("radius")?;
let axis = args.get_kw_arg("axis")?;
let length = args.get_kw_arg_opt("length")?;
let value = inner_helix(revolutions, angle_start, ccw, radius, axis, length, exec_state, args).await?;
Ok(KclValue::Helix { value })
}
/// Create a helix.
///
/// ```no_run
/// // Create a helix around the Z axis.
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 5,
/// length = 10,
/// radius = 5,
/// axis = 'Z',
/// )
///
///
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('YZ')
/// |> circle( center = [0, 0], radius = 0.5)
/// |> sweep(path = helixPath)
/// ```
///
/// ```no_run
/// // Create a helix around an edge.
/// helper001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 10], tag = $edge001)
///
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 5,
/// length = 10,
/// radius = 5,
/// axis = edge001,
/// )
///
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('XY')
/// |> circle( center = [0, 0], radius = 0.5 )
/// |> sweep(path = helixPath)
/// ```
///
/// ```no_run
/// // Create a helix around a custom axis.
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 5,
/// length = 10,
/// radius = 5,
/// axis = {
/// custom = {
/// axis = [0, 0, 1.0],
/// origin = [0, 0.25, 0]
/// }
/// }
/// )
///
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('XY')
/// |> circle( center = [0, 0], radius = 1 )
/// |> sweep(path = helixPath)
/// ```
#[stdlib {
name = "helix",
keywords = true,
unlabeled_first = false,
args = {
revolutions = { docs = "Number of revolutions."},
angle_start = { docs = "Start angle (in degrees)."},
ccw = { docs = "Is the helix rotation counter clockwise? The default is `false`.", include_in_snippet = false},
radius = { docs = "Radius of the helix."},
axis = { docs = "Axis to use for the helix."},
length = { docs = "Length of the helix. This is not necessary if the helix is created around an edge. If not given the length of the edge is used.", include_in_snippet = true},
},
feature_tree_operation = true,
}]
#[allow(clippy::too_many_arguments)]
async fn inner_helix(
revolutions: f64,
angle_start: f64,
ccw: Option<bool>,
radius: f64,
axis: Axis3dOrEdgeReference,
length: Option<f64>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<HelixValue>, KclError> {
let id = exec_state.next_uuid();
let helix_result = Box::new(HelixValue {
value: id,
artifact_id: id.into(),
revolutions,
angle_start,
ccw: ccw.unwrap_or(false),
units: exec_state.length_unit(),
meta: vec![args.source_range.into()],
});
if args.ctx.no_engine_commands().await {
return Ok(helix_result);
}
match axis {
Axis3dOrEdgeReference::Axis(axis) => {
let (axis, origin) = axis.axis_and_origin()?;
// Make sure they gave us a length.
let Some(length) = length else {
return Err(KclError::Semantic(crate::errors::KclErrorDetails {
message: "Length is required when creating a helix around an axis.".to_string(),
source_ranges: vec![args.source_range],
}));
};
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::EntityMakeHelixFromParams {
radius: LengthUnit(radius),
is_clockwise: !helix_result.ccw,
length: LengthUnit(length),
revolutions,
start_angle: Angle::from_degrees(angle_start),
axis,
center: origin,
}),
)
.await?;
}
Axis3dOrEdgeReference::Edge(edge) => {
let edge_id = edge.get_engine_id(exec_state, &args)?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::EntityMakeHelixFromEdge {
radius: LengthUnit(radius),
is_clockwise: !helix_result.ccw,
length: length.map(LengthUnit),
revolutions,
start_angle: Angle::from_degrees(angle_start),
edge_id,
}),
)
.await?;
}
};
Ok(helix_result)
}
/// Data for helix revolutions.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct HelixRevolutionsData {
/// Number of revolutions.
pub revolutions: f64,
/// Start angle (in degrees).
#[serde(rename = "angleStart")]
pub angle_start: f64,
/// Is the helix rotation counter clockwise?
/// The default is `false`.
#[serde(default)]
pub ccw: bool,
/// Length of the helix. If this argument is not provided, the height of
/// the solid is used.
pub length: Option<f64>,
}
/// Create a helix on a cylinder.
pub async fn helix_revolutions(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, solid): (HelixRevolutionsData, Box<Solid>) = args.get_data_and_solid()?;
let value = inner_helix_revolutions(data, solid, exec_state, args).await?;
Ok(KclValue::Solid { value })
}
/// Create a helix on a cylinder.
///
/// ```no_run
/// part001 = startSketchOn('XY')
/// |> circle( center= [5, 5], radius= 10 )
/// |> extrude(length = 10)
/// |> helixRevolutions({
/// angleStart = 0,
/// ccw = true,
/// revolutions = 16,
/// }, %)
/// ```
#[stdlib {
name = "helixRevolutions",
feature_tree_operation = true,
}]
async fn inner_helix_revolutions(
data: HelixRevolutionsData,
solid: Box<Solid>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::EntityMakeHelix {
cylinder_id: solid.id,
is_clockwise: !data.ccw,
length: LengthUnit(data.length.unwrap_or(solid.height)),
revolutions: data.revolutions,
start_angle: Angle::from_degrees(data.angle_start),
}),
)
.await?;
Ok(solid)
}

View File

@ -0,0 +1,181 @@
//! Standard library functions involved in importing files.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{coord::System, format::InputFormat3d, units::UnitLength};
use kittycad_modeling_cmds as kcmc;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{import_foreign, send_import_to_engine, ExecState, ImportedGeometry, KclValue, ZOO_COORD_SYSTEM},
std::Args,
};
/// Import format specifier
#[derive(serde :: Serialize, serde :: Deserialize, PartialEq, Debug, Clone, schemars :: JsonSchema)]
#[cfg_attr(feature = "tabled", derive(tabled::Tabled))]
#[serde(tag = "format")]
pub enum ImportFormat {
/// Autodesk Filmbox (FBX) format
#[serde(rename = "fbx")]
Fbx {},
/// Binary glTF 2.0. We refer to this as glTF since that is how our customers refer to
/// it, but this can also import binary glTF (glb).
#[serde(rename = "gltf")]
Gltf {},
/// Wavefront OBJ format.
#[serde(rename = "obj")]
Obj {
/// Co-ordinate system of input data.
/// Defaults to the [KittyCAD co-ordinate system.
coords: Option<System>,
/// The units of the input data. This is very important for correct scaling and when
/// calculating physics properties like mass, etc.
/// Defaults to millimeters.
units: UnitLength,
},
/// The PLY Polygon File Format.
#[serde(rename = "ply")]
Ply {
/// Co-ordinate system of input data.
/// Defaults to the [KittyCAD co-ordinate system.
coords: Option<System>,
/// The units of the input data. This is very important for correct scaling and when
/// calculating physics properties like mass, etc.
/// Defaults to millimeters.
units: UnitLength,
},
/// SolidWorks part (SLDPRT) format.
#[serde(rename = "sldprt")]
Sldprt {},
/// ISO 10303-21 (STEP) format.
#[serde(rename = "step")]
Step {},
/// *ST**ereo**L**ithography format.
#[serde(rename = "stl")]
Stl {
/// Co-ordinate system of input data.
/// Defaults to the [KittyCAD co-ordinate system.
coords: Option<System>,
/// The units of the input data. This is very important for correct scaling and when
/// calculating physics properties like mass, etc.
/// Defaults to millimeters.
units: UnitLength,
},
}
impl From<ImportFormat> for InputFormat3d {
fn from(format: ImportFormat) -> Self {
match format {
ImportFormat::Fbx {} => InputFormat3d::Fbx(Default::default()),
ImportFormat::Gltf {} => InputFormat3d::Gltf(Default::default()),
ImportFormat::Obj { coords, units } => InputFormat3d::Obj(kcmc::format::obj::import::Options {
coords: coords.unwrap_or(ZOO_COORD_SYSTEM),
units,
}),
ImportFormat::Ply { coords, units } => InputFormat3d::Ply(kcmc::format::ply::import::Options {
coords: coords.unwrap_or(ZOO_COORD_SYSTEM),
units,
}),
ImportFormat::Sldprt {} => InputFormat3d::Sldprt(kcmc::format::sldprt::import::Options {
split_closed_faces: false,
}),
ImportFormat::Step {} => InputFormat3d::Step(kcmc::format::step::import::Options {
split_closed_faces: false,
}),
ImportFormat::Stl { coords, units } => InputFormat3d::Stl(kcmc::format::stl::import::Options {
coords: coords.unwrap_or(ZOO_COORD_SYSTEM),
units,
}),
}
}
}
/// Import a CAD file.
/// For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters.
/// Otherwise you can specify the unit by passing in the options parameter.
/// If you import a gltf file, we will try to find the bin file and import it as well.
///
/// Import paths are relative to the current project directory. This only works in the desktop app
/// not in browser.
pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (file_path, options): (String, Option<ImportFormat>) = args.get_import_data()?;
let imported_geometry = inner_import(file_path, options, exec_state, args).await?;
Ok(KclValue::ImportedGeometry(imported_geometry))
}
/// Import a CAD file.
///
/// **DEPRECATED** Prefer to use import statements.
///
/// For formats lacking unit data (such as STL, OBJ, or PLY files), the default
/// unit of measurement is millimeters. Alternatively you may specify the unit
/// by passing your desired measurement unit in the options parameter. When
/// importing a GLTF file, the bin file will be imported as well. Import paths
/// are relative to the current project directory.
///
/// Note: The import command currently only works when using the native
/// Modeling App.
///
/// ```no_run
/// model = import("tests/inputs/cube.obj")
/// ```
///
/// ```no_run
/// model = import("tests/inputs/cube.obj", {format: "obj", units: "m"})
/// ```
///
/// ```no_run
/// model = import("tests/inputs/cube.gltf")
/// ```
///
/// ```no_run
/// model = import("tests/inputs/cube.sldprt")
/// ```
///
/// ```no_run
/// model = import("tests/inputs/cube.step")
/// ```
///
/// ```no_run
/// import height, buildSketch from 'common.kcl'
///
/// plane = 'XZ'
/// margin = 2
/// s1 = buildSketch(plane, [0, 0])
/// s2 = buildSketch(plane, [0, height() + margin])
/// ```
#[stdlib {
name = "import",
feature_tree_operation = true,
deprecated = true,
tags = [],
}]
async fn inner_import(
file_path: String,
options: Option<ImportFormat>,
exec_state: &mut ExecState,
args: Args,
) -> Result<ImportedGeometry, KclError> {
if file_path.is_empty() {
return Err(KclError::Semantic(KclErrorDetails {
message: "No file path was provided.".to_string(),
source_ranges: vec![args.source_range],
}));
}
let format = options.map(InputFormat3d::from);
send_import_to_engine(
import_foreign(
std::path::Path::new(&file_path),
format,
exec_state,
&args.ctx,
args.source_range,
)
.await?,
&args.ctx,
)
.await
}

View File

@ -0,0 +1,163 @@
//! Standard library lofts.
use std::num::NonZeroU32;
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Sketch, Solid},
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
};
const DEFAULT_V_DEGREE: u32 = 2;
/// Create a 3D surface or solid by interpolating between two or more sketches.
pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketches = args.get_unlabeled_kw_arg("sketches")?;
let v_degree: NonZeroU32 = args
.get_kw_arg_opt("vDegree")?
.unwrap_or(NonZeroU32::new(DEFAULT_V_DEGREE).unwrap());
// Attempt to approximate rational curves (such as arcs) using a bezier.
// This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios
// Over time, this field won't be necessary.
let bez_approximate_rational = args.get_kw_arg_opt("bezApproximateRational")?.unwrap_or(false);
// This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
let base_curve_index: Option<u32> = args.get_kw_arg_opt("baseCurveIndex")?;
// Tolerance for the loft operation.
let tolerance: Option<f64> = args.get_kw_arg_opt("tolerance")?;
let value = inner_loft(
sketches,
v_degree,
bez_approximate_rational,
base_curve_index,
tolerance,
exec_state,
args,
)
.await?;
Ok(KclValue::Solid { value })
}
/// Create a 3D surface or solid by interpolating between two or more sketches.
///
/// The sketches need to closed and on the same plane.
///
/// ```no_run
/// // Loft a square and a triangle.
/// squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// triangleSketch = startSketchOn(offsetPlane('XY', offset = 75))
/// |> startProfileAt([0, 125], %)
/// |> line(end = [-15, -30])
/// |> line(end = [30, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// loft([squareSketch, triangleSketch])
/// ```
///
/// ```no_run
/// // Loft a square, a circle, and another circle.
/// squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
/// |> circle( center = [0, 100], radius = 50 )
///
/// circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
/// |> circle( center = [0, 100], radius = 20 )
///
/// loft([squareSketch, circleSketch0, circleSketch1])
/// ```
///
/// ```no_run
/// // Loft a square, a circle, and another circle with options.
/// squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
/// |> circle( center = [0, 100], radius = 50 )
///
/// circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
/// |> circle( center = [0, 100], radius = 20 )
///
/// loft([squareSketch, circleSketch0, circleSketch1],
/// baseCurveIndex = 0,
/// bezApproximateRational = false,
/// tolerance = 0.000001,
/// vDegree = 2,
/// )
/// ```
#[stdlib {
name = "loft",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
sketches = {docs = "Which sketches to loft. Must include at least 2 sketches."},
v_degree = {docs = "Degree of the interpolation. Must be greater than zero. For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction. This defaults to 2, if not specified."},
bez_approximate_rational = {docs = "Attempt to approximate rational curves (such as arcs) using a bezier. This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios Over time, this field won't be necessary."},
base_curve_index = {docs = "This can be set to override the automatically determined topological base curve, which is usually the first section encountered."},
tolerance = {docs = "Tolerance for the loft operation."},
}
}]
async fn inner_loft(
sketches: Vec<Sketch>,
v_degree: NonZeroU32,
bez_approximate_rational: bool,
base_curve_index: Option<u32>,
tolerance: Option<f64>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
// Make sure we have at least two sketches.
if sketches.len() < 2 {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Loft requires at least two sketches, but only {} were provided.",
sketches.len()
),
source_ranges: vec![args.source_range],
}));
}
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Loft {
section_ids: sketches.iter().map(|group| group.id).collect(),
base_curve_index,
bez_approximate_rational,
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
v_degree,
}),
)
.await?;
// Using the first sketch as the base curve, idk we might want to change this later.
let mut sketch = sketches[0].clone();
// Override its id with the loft id so we can get its faces later
sketch.id = id;
do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await
}

View File

@ -0,0 +1,774 @@
//! Functions related to mathematics.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use super::args::FromArgs;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue},
std::args::{Args, TyF64},
};
/// Compute the remainder after dividing `num` by `div`.
/// If `num` is negative, the result will be too.
pub async fn rem(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let n = args.get_unlabeled_kw_arg("number to divide")?;
let d = args.get_kw_arg("divisor")?;
let remainder = inner_rem(n, d);
Ok(args.make_user_val_from_f64(remainder))
}
/// Compute the remainder after dividing `num` by `div`.
/// If `num` is negative, the result will be too.
///
/// ```no_run
/// assertEqual(rem( 7, divisor = 4), 3, 0.01, "remainder is 3" )
/// assertEqual(rem(-7, divisor = 4), -3, 0.01, "remainder is -3")
/// assertEqual(rem( 7, divisor = -4), 3, 0.01, "remainder is 3" )
/// assertEqual(rem( 6, divisor = 2.5), 1, 0.01, "remainder is 1" )
/// assertEqual(rem( 6.5, divisor = 2.5), 1.5, 0.01, "remainder is 1.5" )
/// assertEqual(rem( 6.5, divisor = 2), 0.5, 0.01, "remainder is 0.5" )
/// ```
#[stdlib {
name = "rem",
tags = ["math"],
keywords = true,
unlabeled_first = true,
args = {
num = {docs = "The number which will be divided by `divisor`."},
divisor = {docs = "The number which will divide `num`."},
}
}]
fn inner_rem(num: f64, divisor: f64) -> f64 {
num % divisor
}
/// Compute the cosine of a number (in radians).
pub async fn cos(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
Ok(args.make_user_val_from_f64_with_type(TyF64::count(num.cos())))
}
/// Compute the sine of a number (in radians).
pub async fn sin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
Ok(args.make_user_val_from_f64_with_type(TyF64::count(num.sin())))
}
/// Compute the tangent of a number (in radians).
pub async fn tan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
Ok(args.make_user_val_from_f64_with_type(TyF64::count(num.tan())))
}
/// Return the value of `pi`. Archimedes constant (π).
pub async fn pi(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_pi()?;
Ok(args.make_user_val_from_f64(result))
}
/// Return the value of `pi`. Archimedes constant (π).
///
/// **DEPRECATED** use the constant PI
///
/// ```no_run
/// circumference = 70
///
/// exampleSketch = startSketchOn("XZ")
/// |> circle( center = [0, 0], radius = circumference/ (2 * pi()) )
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "pi",
tags = ["math"],
deprecated = true,
}]
fn inner_pi() -> Result<f64, KclError> {
Ok(std::f64::consts::PI)
}
/// Compute the square root of a number.
pub async fn sqrt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_sqrt(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the square root of a number.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 50,
/// length = sqrt(2500),
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "sqrt",
tags = ["math"],
}]
fn inner_sqrt(num: f64) -> Result<f64, KclError> {
Ok(num.sqrt())
}
/// Compute the absolute value of a number.
pub async fn abs(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_abs(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the absolute value of a number.
///
/// ```no_run
/// myAngle = -120
///
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [8, 0])
/// |> angledLine({
/// angle = abs(myAngle),
/// length = 5,
/// }, %)
/// |> line(end = [-5, 0])
/// |> angledLine({
/// angle = myAngle,
/// length = 5,
/// }, %)
/// |> close()
///
/// baseExtrusion = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "abs",
tags = ["math"],
}]
fn inner_abs(num: f64) -> Result<f64, KclError> {
Ok(num.abs())
}
/// Round a number to the nearest integer.
pub async fn round(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_round(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Round a number to the nearest integer.
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(endAbsolute = [12, 10])
/// |> line(end = [round(7.02986), 0])
/// |> yLineTo(0, %)
/// |> close()
///
/// extrude001 = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "round",
tags = ["math"],
}]
fn inner_round(num: f64) -> Result<f64, KclError> {
Ok(num.round())
}
/// Compute the largest integer less than or equal to a number.
pub async fn floor(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_floor(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the largest integer less than or equal to a number.
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(endAbsolute = [12, 10])
/// |> line(end = [floor(7.02986), 0])
/// |> yLineTo(0, %)
/// |> close()
///
/// extrude001 = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "floor",
tags = ["math"],
}]
fn inner_floor(num: f64) -> Result<f64, KclError> {
Ok(num.floor())
}
/// Compute the smallest integer greater than or equal to a number.
pub async fn ceil(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_ceil(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the smallest integer greater than or equal to a number.
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(endAbsolute = [12, 10])
/// |> line(end = [ceil(7.02986), 0])
/// |> yLineTo(0, %)
/// |> close()
///
/// extrude001 = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "ceil",
tags = ["math"],
}]
fn inner_ceil(num: f64) -> Result<f64, KclError> {
Ok(num.ceil())
}
/// Compute the minimum of the given arguments.
pub async fn min(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let nums = args.get_number_array()?;
let result = inner_min(nums);
Ok(args.make_user_val_from_f64(result))
}
/// Compute the minimum of the given arguments.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 70,
/// length = min(15, 31, 4, 13, 22)
/// }, %)
/// |> line(end = [20, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "min",
tags = ["math"],
}]
fn inner_min(args: Vec<f64>) -> f64 {
let mut min = f64::MAX;
for arg in args.iter() {
if *arg < min {
min = *arg;
}
}
min
}
/// Compute the maximum of the given arguments.
pub async fn max(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let nums = args.get_number_array()?;
let result = inner_max(nums);
Ok(args.make_user_val_from_f64(result))
}
/// Compute the maximum of the given arguments.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 70,
/// length = max(15, 31, 4, 13, 22)
/// }, %)
/// |> line(end = [20, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "max",
tags = ["math"],
}]
fn inner_max(args: Vec<f64>) -> f64 {
let mut max = f64::MIN;
for arg in args.iter() {
if *arg > max {
max = *arg;
}
}
max
}
/// Compute the number to a power.
pub async fn pow(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let nums = args.get_number_array()?;
if nums.len() > 2 {
return Err(KclError::Type(KclErrorDetails {
message: format!("expected 2 arguments, got {}", nums.len()),
source_ranges: vec![args.source_range],
}));
}
if nums.len() <= 1 {
return Err(KclError::Type(KclErrorDetails {
message: format!("expected 2 arguments, got {}", nums.len()),
source_ranges: vec![args.source_range],
}));
}
let result = inner_pow(nums[0], nums[1])?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the number to a power.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 50,
/// length = pow(5, 2),
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "pow",
tags = ["math"],
}]
fn inner_pow(num: f64, pow: f64) -> Result<f64, KclError> {
Ok(num.powf(pow))
}
/// Compute the arccosine of a number (in radians).
pub async fn acos(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_acos(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the arccosine of a number (in radians).
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = toDegrees(acos(0.5)),
/// length = 10,
/// }, %)
/// |> line(end = [5, 0])
/// |> line(endAbsolute = [12, 0])
/// |> close()
///
/// extrude001 = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "acos",
tags = ["math"],
}]
fn inner_acos(num: f64) -> Result<f64, KclError> {
Ok(num.acos())
}
/// Compute the arcsine of a number (in radians).
pub async fn asin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_asin(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the arcsine of a number (in radians).
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = toDegrees(asin(0.5)),
/// length = 20,
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// extrude001 = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "asin",
tags = ["math"],
}]
fn inner_asin(num: f64) -> Result<f64, KclError> {
Ok(num.asin())
}
/// Compute the arctangent of a number (in radians).
pub async fn atan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_atan(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the arctangent of a number (in radians).
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = toDegrees(atan(1.25)),
/// length = 20,
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// extrude001 = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "atan",
tags = ["math"],
}]
fn inner_atan(num: f64) -> Result<f64, KclError> {
Ok(num.atan())
}
/// Compute the four quadrant arctangent of Y and X (in radians).
pub async fn atan2(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (y, x) = FromArgs::from_args(&args, 0)?;
let result = inner_atan2(y, x)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the four quadrant arctangent of Y and X (in radians).
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = toDegrees(atan2(1.25, 2)),
/// length = 20,
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// extrude001 = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "atan2",
tags = ["math"],
}]
fn inner_atan2(y: f64, x: f64) -> Result<f64, KclError> {
Ok(y.atan2(x))
}
/// Compute the logarithm of the number with respect to an arbitrary base.
///
/// The result might not be correctly rounded owing to implementation
/// details; `log2()` can produce more accurate results for base 2,
/// and `log10()` can produce more accurate results for base 10.
pub async fn log(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let nums = args.get_number_array()?;
if nums.len() > 2 {
return Err(KclError::Type(KclErrorDetails {
message: format!("expected 2 arguments, got {}", nums.len()),
source_ranges: vec![args.source_range],
}));
}
if nums.len() <= 1 {
return Err(KclError::Type(KclErrorDetails {
message: format!("expected 2 arguments, got {}", nums.len()),
source_ranges: vec![args.source_range],
}));
}
let result = inner_log(nums[0], nums[1])?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the logarithm of the number with respect to an arbitrary base.
///
/// The result might not be correctly rounded owing to implementation
/// details; `log2()` can produce more accurate results for base 2,
/// and `log10()` can produce more accurate results for base 10.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> line(end = [log(100, 5), 0])
/// |> line(end = [5, 8])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "log",
tags = ["math"],
}]
fn inner_log(num: f64, base: f64) -> Result<f64, KclError> {
Ok(num.log(base))
}
/// Compute the base 2 logarithm of the number.
pub async fn log2(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_log2(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the base 2 logarithm of the number.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> line(end = [log2(100), 0])
/// |> line(end = [5, 8])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "log2",
tags = ["math"],
}]
fn inner_log2(num: f64) -> Result<f64, KclError> {
Ok(num.log2())
}
/// Compute the base 10 logarithm of the number.
pub async fn log10(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_log10(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the base 10 logarithm of the number.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> line(end = [log10(100), 0])
/// |> line(end = [5, 8])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "log10",
tags = ["math"],
}]
fn inner_log10(num: f64) -> Result<f64, KclError> {
Ok(num.log10())
}
/// Compute the natural logarithm of the number.
pub async fn ln(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_ln(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the natural logarithm of the number.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> line(end = [ln(100), 15])
/// |> line(end = [5, -6])
/// |> line(end = [-10, -10])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "ln",
tags = ["math"],
}]
fn inner_ln(num: f64) -> Result<f64, KclError> {
Ok(num.ln())
}
/// Return the value of Eulers number `e`.
pub async fn e(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_e()?;
Ok(args.make_user_val_from_f64(result))
}
/// Return the value of Eulers number `e`.
///
/// **DEPRECATED** use the constant E
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 30,
/// length = 2 * e() ^ 2,
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "e",
tags = ["math"],
deprecated = true,
}]
fn inner_e() -> Result<f64, KclError> {
Ok(std::f64::consts::E)
}
/// Return the value of `tau`. The full circle constant (τ). Equal to 2π.
pub async fn tau(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_tau()?;
Ok(args.make_user_val_from_f64(result))
}
/// Return the value of `tau`. The full circle constant (τ). Equal to 2π.
///
/// **DEPRECATED** use the constant TAU
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 50,
/// length = 10 * tau(),
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "tau",
tags = ["math"],
deprecated = true,
}]
fn inner_tau() -> Result<f64, KclError> {
Ok(std::f64::consts::TAU)
}
/// Converts a number from degrees to radians.
pub async fn to_radians(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_to_radians(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Converts a number from degrees to radians.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 50,
/// length = 70 * cos(toRadians(45)),
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "toRadians",
tags = ["math"],
}]
fn inner_to_radians(num: f64) -> Result<f64, KclError> {
Ok(num.to_radians())
}
/// Converts a number from radians to degrees.
pub async fn to_degrees(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?;
let result = inner_to_degrees(num)?;
Ok(args.make_user_val_from_f64(result))
}
/// Converts a number from radians to degrees.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 50,
/// length = 70 * cos(toDegrees(pi()/4)),
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "toDegrees",
tags = ["math"],
}]
fn inner_to_degrees(num: f64) -> Result<f64, KclError> {
Ok(num.to_degrees())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_inner_max() {
let nums = vec![4.0, 5.0, 6.0];
let result = inner_max(nums);
assert_eq!(result, 6.0);
}
#[test]
fn test_inner_max_with_neg() {
let nums = vec![4.0, -5.0];
let result = inner_max(nums);
assert_eq!(result, 4.0);
}
#[test]
fn test_inner_min() {
let nums = vec![4.0, 5.0, 6.0];
let result = inner_min(nums);
assert_eq!(result, 4.0);
}
#[test]
fn test_inner_min_with_neg() {
let nums = vec![4.0, -5.0];
let result = inner_min(nums);
assert_eq!(result, -5.0);
}
}

View File

@ -0,0 +1,148 @@
//! Standard library mirror.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
execution::{ExecState, KclValue, Sketch, SketchSet},
std::{axis_or_reference::Axis2dOrEdgeReference, Args},
};
/// Data for a mirror.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Mirror2dData {
/// Axis to use as mirror.
pub axis: Axis2dOrEdgeReference,
}
/// Mirror a sketch.
///
/// Only works on unclosed sketches for now.
pub async fn mirror_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch_set): (Mirror2dData, SketchSet) = args.get_data_and_sketch_set()?;
let sketches = inner_mirror_2d(data, sketch_set, exec_state, args).await?;
Ok(sketches.into())
}
/// Mirror a sketch.
///
/// Only works on unclosed sketches for now.
///
/// Mirror occurs around a local sketch axis rather than a global axis.
///
/// ```no_run
/// // Mirror an un-closed sketch across the Y axis.
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 10], %)
/// |> line(end = [15, 0])
/// |> line(end = [-7, -3])
/// |> line(end = [9, -1])
/// |> line(end = [-8, -5])
/// |> line(end = [9, -3])
/// |> line(end = [-8, -3])
/// |> line(end = [9, -1])
/// |> line(end = [-19, -0])
/// |> mirror2d({axis = 'Y'}, %)
///
/// example = extrude(sketch001, length = 10)
/// ```
///
/// ```no_run
/// // Mirror a un-closed sketch across the Y axis.
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 8.5], %)
/// |> line(end = [20, -8.5])
/// |> line(end = [-20, -8.5])
/// |> mirror2d({axis = 'Y'}, %)
///
/// example = extrude(sketch001, length = 10)
/// ```
///
/// ```no_run
/// // Mirror a un-closed sketch across an edge.
/// helper001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 10], tag = $edge001)
///
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 8.5], %)
/// |> line(end = [20, -8.5])
/// |> line(end = [-20, -8.5])
/// |> mirror2d({axis = edge001}, %)
///
/// // example = extrude(sketch001, length = 10)
/// ```
///
/// ```no_run
/// // Mirror an un-closed sketch across a custom axis.
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 8.5], %)
/// |> line(end = [20, -8.5])
/// |> line(end = [-20, -8.5])
/// |> mirror2d({
/// axis = {
/// custom = {
/// axis = [0.0, 1.0],
/// origin = [0.0, 0.0]
/// }
/// }
/// }, %)
///
/// example = extrude(sketch001, length = 10)
/// ```
#[stdlib {
name = "mirror2d",
}]
async fn inner_mirror_2d(
data: Mirror2dData,
sketch_set: SketchSet,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Box<Sketch>>, KclError> {
let starting_sketches = match sketch_set {
SketchSet::Sketch(sketch) => vec![sketch],
SketchSet::Sketches(sketches) => sketches,
};
if args.ctx.no_engine_commands().await {
return Ok(starting_sketches);
}
match data.axis {
Axis2dOrEdgeReference::Axis(axis) => {
let (axis, origin) = axis.axis_and_origin()?;
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::EntityMirror {
ids: starting_sketches.iter().map(|sketch| sketch.id).collect(),
axis,
point: origin,
}),
)
.await?;
}
Axis2dOrEdgeReference::Edge(edge) => {
let edge_id = edge.get_engine_id(exec_state, &args)?;
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::EntityMirrorAcrossEdge {
ids: starting_sketches.iter().map(|sketch| sketch.id).collect(),
edge_id,
}),
)
.await?;
}
};
Ok(starting_sketches)
}

335
rust/kcl-lib/src/std/mod.rs Normal file
View File

@ -0,0 +1,335 @@
//! Functions implemented for language execution.
pub mod appearance;
pub mod args;
pub mod array;
pub mod assert;
pub mod axis_or_reference;
pub mod chamfer;
pub mod convert;
pub mod extrude;
pub mod fillet;
pub mod helix;
pub mod import;
pub mod loft;
pub mod math;
pub mod mirror;
pub mod patterns;
pub mod planes;
pub mod polar;
pub mod revolve;
pub mod segment;
pub mod shapes;
pub mod shell;
pub mod sketch;
pub mod sweep;
pub mod transform;
pub mod types;
pub mod units;
pub mod utils;
use anyhow::Result;
pub use args::Args;
use indexmap::IndexMap;
use kcl_derive_docs::stdlib;
use lazy_static::lazy_static;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
docs::StdLibFn,
errors::KclError,
execution::{ExecState, KclValue},
};
pub type StdFn = fn(
&mut ExecState,
Args,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<KclValue, KclError>> + Send + '_>>;
lazy_static! {
static ref CORE_FNS: Vec<Box<dyn StdLibFn>> = vec![
Box::new(LegLen),
Box::new(LegAngX),
Box::new(LegAngY),
Box::new(crate::std::appearance::Appearance),
Box::new(crate::std::convert::Int),
Box::new(crate::std::extrude::Extrude),
Box::new(crate::std::segment::SegEnd),
Box::new(crate::std::segment::SegEndX),
Box::new(crate::std::segment::SegEndY),
Box::new(crate::std::segment::SegStart),
Box::new(crate::std::segment::SegStartX),
Box::new(crate::std::segment::SegStartY),
Box::new(crate::std::segment::LastSegX),
Box::new(crate::std::segment::LastSegY),
Box::new(crate::std::segment::SegLen),
Box::new(crate::std::segment::SegAng),
Box::new(crate::std::segment::TangentToEnd),
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::Line),
Box::new(crate::std::sketch::XLineTo),
Box::new(crate::std::sketch::XLine),
Box::new(crate::std::sketch::YLineTo),
Box::new(crate::std::sketch::YLine),
Box::new(crate::std::sketch::AngledLineToX),
Box::new(crate::std::sketch::AngledLineToY),
Box::new(crate::std::sketch::AngledLine),
Box::new(crate::std::sketch::AngledLineOfXLength),
Box::new(crate::std::sketch::AngledLineOfYLength),
Box::new(crate::std::sketch::AngledLineThatIntersects),
Box::new(crate::std::sketch::StartSketchAt),
Box::new(crate::std::sketch::StartSketchOn),
Box::new(crate::std::sketch::StartProfileAt),
Box::new(crate::std::sketch::ProfileStartX),
Box::new(crate::std::sketch::ProfileStartY),
Box::new(crate::std::sketch::ProfileStart),
Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::ArcTo),
Box::new(crate::std::sketch::TangentialArc),
Box::new(crate::std::sketch::TangentialArcTo),
Box::new(crate::std::sketch::TangentialArcToRelative),
Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::sketch::Hole),
Box::new(crate::std::mirror::Mirror2D),
Box::new(crate::std::patterns::PatternLinear2D),
Box::new(crate::std::patterns::PatternLinear3D),
Box::new(crate::std::patterns::PatternCircular2D),
Box::new(crate::std::patterns::PatternCircular3D),
Box::new(crate::std::patterns::PatternTransform),
Box::new(crate::std::patterns::PatternTransform2D),
Box::new(crate::std::array::Reduce),
Box::new(crate::std::array::Map),
Box::new(crate::std::array::Push),
Box::new(crate::std::array::Pop),
Box::new(crate::std::chamfer::Chamfer),
Box::new(crate::std::fillet::Fillet),
Box::new(crate::std::fillet::GetOppositeEdge),
Box::new(crate::std::fillet::GetNextAdjacentEdge),
Box::new(crate::std::fillet::GetPreviousAdjacentEdge),
Box::new(crate::std::helix::Helix),
Box::new(crate::std::helix::HelixRevolutions),
Box::new(crate::std::shell::Shell),
Box::new(crate::std::shell::Hollow),
Box::new(crate::std::revolve::Revolve),
Box::new(crate::std::sweep::Sweep),
Box::new(crate::std::loft::Loft),
Box::new(crate::std::planes::OffsetPlane),
Box::new(crate::std::import::Import),
Box::new(crate::std::math::Acos),
Box::new(crate::std::math::Asin),
Box::new(crate::std::math::Atan),
Box::new(crate::std::math::Atan2),
Box::new(crate::std::math::Pi),
Box::new(crate::std::math::E),
Box::new(crate::std::math::Tau),
Box::new(crate::std::math::Sqrt),
Box::new(crate::std::math::Abs),
Box::new(crate::std::math::Rem),
Box::new(crate::std::math::Round),
Box::new(crate::std::math::Floor),
Box::new(crate::std::math::Ceil),
Box::new(crate::std::math::Min),
Box::new(crate::std::math::Max),
Box::new(crate::std::math::Pow),
Box::new(crate::std::math::Log),
Box::new(crate::std::math::Log2),
Box::new(crate::std::math::Log10),
Box::new(crate::std::math::Ln),
Box::new(crate::std::math::ToDegrees),
Box::new(crate::std::math::ToRadians),
Box::new(crate::std::units::Mm),
Box::new(crate::std::units::Inch),
Box::new(crate::std::units::Ft),
Box::new(crate::std::units::M),
Box::new(crate::std::units::Cm),
Box::new(crate::std::units::Yd),
Box::new(crate::std::polar::Polar),
Box::new(crate::std::assert::Assert),
Box::new(crate::std::assert::AssertEqual),
Box::new(crate::std::assert::AssertLessThan),
Box::new(crate::std::assert::AssertGreaterThan),
Box::new(crate::std::assert::AssertLessThanOrEq),
Box::new(crate::std::assert::AssertGreaterThanOrEq),
Box::new(crate::std::transform::Scale),
Box::new(crate::std::transform::Translate),
Box::new(crate::std::transform::Rotate),
];
}
pub fn name_in_stdlib(name: &str) -> bool {
CORE_FNS.iter().any(|f| f.name() == name)
}
pub fn get_stdlib_fn(name: &str) -> Option<Box<dyn StdLibFn>> {
CORE_FNS.iter().find(|f| f.name() == name).cloned()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StdFnProps {
pub name: String,
pub deprecated: bool,
}
impl StdFnProps {
fn default(name: &str) -> Self {
Self {
name: name.to_owned(),
deprecated: false,
}
}
}
pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProps) {
match (path, fn_name) {
("math", "cos") => (
|e, a| Box::pin(crate::std::math::cos(e, a)),
StdFnProps::default("std::math::cos"),
),
("math", "sin") => (
|e, a| Box::pin(crate::std::math::sin(e, a)),
StdFnProps::default("std::math::sin"),
),
("math", "tan") => (
|e, a| Box::pin(crate::std::math::tan(e, a)),
StdFnProps::default("std::math::tan"),
),
_ => unreachable!(),
}
}
pub struct StdLib {
pub fns: IndexMap<String, Box<dyn StdLibFn>>,
}
impl std::fmt::Debug for StdLib {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StdLib").field("fns.len()", &self.fns.len()).finish()
}
}
impl StdLib {
pub fn new() -> Self {
let fns = CORE_FNS
.clone()
.into_iter()
.map(|internal_fn| (internal_fn.name(), internal_fn))
.collect();
Self { fns }
}
// Get the combined hashmaps.
pub fn combined(&self) -> IndexMap<String, Box<dyn StdLibFn>> {
self.fns.clone()
}
pub fn get(&self, name: &str) -> Option<Box<dyn StdLibFn>> {
self.fns.get(name).cloned()
}
pub fn get_either(&self, name: &str) -> FunctionKind {
if let Some(f) = self.get(name) {
FunctionKind::Core(f)
} else {
FunctionKind::UserDefined
}
}
pub fn contains_key(&self, key: &str) -> bool {
self.fns.contains_key(key)
}
}
impl Default for StdLib {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub enum FunctionKind {
Core(Box<dyn StdLibFn>),
UserDefined,
}
/// Compute the length of the given leg.
pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg, ty) = args.get_hypotenuse_leg()?;
let result = inner_leg_length(hypotenuse, leg);
Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
}
/// Compute the length of the given leg.
///
/// ```no_run
/// legLen(5, 3)
/// ```
#[stdlib {
name = "legLen",
tags = ["utilities"],
}]
fn inner_leg_length(hypotenuse: f64, leg: f64) -> f64 {
(hypotenuse.powi(2) - f64::min(hypotenuse.abs(), leg.abs()).powi(2)).sqrt()
}
/// Compute the angle of the given leg for x.
pub async fn leg_angle_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg, ty) = args.get_hypotenuse_leg()?;
let result = inner_leg_angle_x(hypotenuse, leg);
Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
}
/// Compute the angle of the given leg for x.
///
/// ```no_run
/// legAngX(5, 3)
/// ```
#[stdlib {
name = "legAngX",
tags = ["utilities"],
}]
fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).acos().to_degrees()
}
/// Compute the angle of the given leg for y.
pub async fn leg_angle_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg, ty) = args.get_hypotenuse_leg()?;
let result = inner_leg_angle_y(hypotenuse, leg);
Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
}
/// Compute the angle of the given leg for y.
///
/// ```no_run
/// legAngY(5, 3)
/// ```
#[stdlib {
name = "legAngY",
tags = ["utilities"],
}]
fn inner_leg_angle_y(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).asin().to_degrees()
}
/// The primitive types that can be used in a KCL file.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Display, FromStr)]
#[serde(rename_all = "lowercase")]
#[display(style = "lowercase")]
pub enum Primitive {
/// A boolean value.
Bool,
/// A number value.
Number,
/// A string value.
String,
/// A uuid value.
Uuid,
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,163 @@
//! Standard library plane helpers.
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Color, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use super::sketch::PlaneData;
use crate::{
errors::KclError,
execution::{ExecState, KclValue, Plane, PlaneType},
std::Args,
};
/// Offset a plane by a distance along its normal.
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let std_plane = args.get_unlabeled_kw_arg("plane")?;
let offset = args.get_kw_arg("offset")?;
let plane = inner_offset_plane(std_plane, offset, exec_state).await?;
make_offset_plane_in_engine(&plane, exec_state, &args).await?;
Ok(KclValue::Plane { value: Box::new(plane) })
}
/// Offset a plane by a distance along its normal.
///
/// For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ'
/// plane and 10 units away from it.
///
/// ```no_run
/// // Loft a square and a circle on the `XY` plane using offset.
/// squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane('XY', offset = 150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `XZ` plane using offset.
/// squareSketch = startSketchOn('XZ')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane('XZ', offset = 150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `YZ` plane using offset.
/// squareSketch = startSketchOn('YZ')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane('YZ', offset = 150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `-XZ` plane using offset.
/// squareSketch = startSketchOn('-XZ')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane('-XZ', offset = -150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
/// ```no_run
/// // A circle on the XY plane
/// startSketchOn("XY")
/// |> startProfileAt([0, 0], %)
/// |> circle( radius = 10, center = [0, 0] )
///
/// // Triangle on the plane 4 units above
/// startSketchOn(offsetPlane("XY", offset = 4))
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> close()
/// ```
#[stdlib {
name = "offsetPlane",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
plane = { docs = "The plane (e.g. 'XY') which this new plane is created from." },
offset = { docs = "Distance from the standard plane this new plane will be created at." },
}
}]
async fn inner_offset_plane(plane: PlaneData, offset: f64, exec_state: &mut ExecState) -> Result<Plane, KclError> {
let mut plane = Plane::from_plane_data(plane, exec_state);
// Though offset planes might be derived from standard planes, they are not
// standard planes themselves.
plane.value = PlaneType::Custom;
plane.origin += plane.z_axis * offset;
Ok(plane)
}
// Engine-side effectful creation of an actual plane object.
// offset planes are shown by default, and hidden by default if they
// are used as a sketch plane. That hiding command is sent within inner_start_profile_at
async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> {
// Create new default planes.
let default_size = 100.0;
let color = Color {
r: 0.6,
g: 0.6,
b: 0.6,
a: 0.3,
};
args.batch_modeling_cmd(
plane.id,
ModelingCmd::from(mcmd::MakePlane {
clobber: false,
origin: plane.origin.into(),
size: LengthUnit(default_size),
x_axis: plane.x_axis.into(),
y_axis: plane.y_axis.into(),
hide: Some(false),
}),
)
.await?;
// Set the color.
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::PlaneSetColor {
color,
plane_id: plane.id,
}),
)
.await?;
Ok(())
}

View File

@ -0,0 +1,55 @@
//! Functions related to polar coordinates.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
execution::{ExecState, KclValue},
std::args::{Args, TyF64},
};
/// Data for polar coordinates.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct PolarCoordsData {
/// The angle of the line (in degrees).
pub angle: f64,
/// The length of the line.
pub length: TyF64,
}
/// Convert from polar/sphere coordinates to cartesian coordinates.
pub async fn polar(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let data: PolarCoordsData = args.get_data()?;
let result = inner_polar(&data)?;
args.make_user_val_from_f64_array(result.to_vec(), &data.length.ty)
}
/// Convert polar/sphere (azimuth, elevation, distance) coordinates to
/// cartesian (x/y/z grid) coordinates.
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = polar({angle: 30, length: 5}), tag = $thing)
/// |> line(end = [0, 5])
/// |> line(end = [segEndX(thing), 0])
/// |> line(end = [-20, 10])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "polar",
}]
fn inner_polar(data: &PolarCoordsData) -> Result<[f64; 2], KclError> {
let angle = data.angle.to_radians();
let x = data.length.n * angle.cos();
let y = data.length.n * angle.sin();
Ok([x, y])
}

View File

@ -0,0 +1,234 @@
//! Standard library revolution surfaces.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Sketch, Solid},
std::{axis_or_reference::Axis2dOrEdgeReference, extrude::do_post_extrude, fillet::default_tolerance, Args},
};
/// Data for revolution surfaces.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct RevolveData {
/// Angle to revolve (in degrees). Default is 360.
#[serde(default)]
#[schemars(range(min = -360.0, max = 360.0))]
pub angle: Option<f64>,
/// Axis of revolution.
pub axis: Axis2dOrEdgeReference,
/// Tolerance for the revolve operation.
#[serde(default)]
pub tolerance: Option<f64>,
}
/// Revolve a sketch around an axis.
pub async fn revolve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch): (RevolveData, Sketch) = args.get_data_and_sketch()?;
let value = inner_revolve(data, sketch, exec_state, args).await?;
Ok(KclValue::Solid { value })
}
/// Rotate a sketch around some provided axis, creating a solid from its extent.
///
/// This, like extrude, is able to create a 3-dimensional solid from a
/// 2-dimensional sketch. However, unlike extrude, this creates a solid
/// by using the extent of the sketch as its revolved around an axis rather
/// than using the extent of the sketch linearly translated through a third
/// dimension.
///
/// Revolve occurs around a local sketch axis rather than a global axis.
///
/// ```no_run
/// part001 = startSketchOn('XY')
/// |> startProfileAt([4, 12], %)
/// |> line(end = [2, 0])
/// |> line(end = [0, -6])
/// |> line(end = [4, -6])
/// |> line(end = [0, -6])
/// |> line(end = [-3.75, -4.5])
/// |> line(end = [0, -5.5])
/// |> line(end = [-2, 0])
/// |> close()
/// |> revolve({axis = 'y'}, %) // default angle is 360
/// ```
///
/// ```no_run
/// // A donut shape.
/// sketch001 = startSketchOn('XY')
/// |> circle( center = [15, 0], radius = 5 )
/// |> revolve({
/// angle = 360,
/// axis = 'y'
/// }, %)
/// ```
///
/// ```no_run
/// part001 = startSketchOn('XY')
/// |> startProfileAt([4, 12], %)
/// |> line(end = [2, 0])
/// |> line(end = [0, -6])
/// |> line(end = [4, -6])
/// |> line(end = [0, -6])
/// |> line(end = [-3.75, -4.5])
/// |> line(end = [0, -5.5])
/// |> line(end = [-2, 0])
/// |> close()
/// |> revolve({axis = 'y', angle = 180}, %)
/// ```
///
/// ```no_run
/// part001 = startSketchOn('XY')
/// |> startProfileAt([4, 12], %)
/// |> line(end = [2, 0])
/// |> line(end = [0, -6])
/// |> line(end = [4, -6])
/// |> line(end = [0, -6])
/// |> line(end = [-3.75, -4.5])
/// |> line(end = [0, -5.5])
/// |> line(end = [-2, 0])
/// |> close()
/// |> revolve({axis = 'y', angle = 180}, %)
/// part002 = startSketchOn(part001, 'end')
/// |> startProfileAt([4.5, -5], %)
/// |> line(end = [0, 5])
/// |> line(end = [5, 0])
/// |> line(end = [0, -5])
/// |> close()
/// |> extrude(length = 5)
/// ```
///
/// ```no_run
/// box = startSketchOn('XY')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 20])
/// |> line(end = [20, 0])
/// |> line(end = [0, -20])
/// |> close()
/// |> extrude(length = 20)
///
/// sketch001 = startSketchOn(box, "END")
/// |> circle( center = [10,10], radius = 4 )
/// |> revolve({
/// angle = -90,
/// axis = 'y'
/// }, %)
/// ```
///
/// ```no_run
/// box = startSketchOn('XY')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 20])
/// |> line(end = [20, 0])
/// |> line(end = [0, -20], tag = $revolveAxis)
/// |> close()
/// |> extrude(length = 20)
///
/// sketch001 = startSketchOn(box, "END")
/// |> circle( center = [10,10], radius = 4 )
/// |> revolve({
/// angle = 90,
/// axis = getOppositeEdge(revolveAxis)
/// }, %)
/// ```
///
/// ```no_run
/// box = startSketchOn('XY')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 20])
/// |> line(end = [20, 0])
/// |> line(end = [0, -20], tag = $revolveAxis)
/// |> close()
/// |> extrude(length = 20)
///
/// sketch001 = startSketchOn(box, "END")
/// |> circle( center = [10,10], radius = 4 )
/// |> revolve({
/// angle = 90,
/// axis = getOppositeEdge(revolveAxis),
/// tolerance: 0.0001
/// }, %)
/// ```
///
/// ```no_run
/// sketch001 = startSketchOn('XY')
/// |> startProfileAt([10, 0], %)
/// |> line(end = [5, -5])
/// |> line(end = [5, 5])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// part001 = revolve({
/// axis = {
/// custom: {
/// axis = [0.0, 1.0],
/// origin: [0.0, 0.0]
/// }
/// }
/// }, sketch001)
/// ```
#[stdlib {
name = "revolve",
feature_tree_operation = true,
}]
async fn inner_revolve(
data: RevolveData,
sketch: Sketch,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
if let Some(angle) = data.angle {
// Return an error if the angle is zero.
// We don't use validate() here because we want to return a specific error message that is
// nice and we use the other data in the docs, so we still need use the derive above for the json schema.
if !(-360.0..=360.0).contains(&angle) || angle == 0.0 {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Expected angle to be between -360 and 360 and not 0, found `{}`", angle),
source_ranges: vec![args.source_range],
}));
}
}
let angle = Angle::from_degrees(data.angle.unwrap_or(360.0));
let id = exec_state.next_uuid();
match data.axis {
Axis2dOrEdgeReference::Axis(axis) => {
let (axis, origin) = axis.axis_and_origin()?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Revolve {
angle,
target: sketch.id.into(),
axis,
origin,
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
axis_is_2d: true,
}),
)
.await?;
}
Axis2dOrEdgeReference::Edge(edge) => {
let edge_id = edge.get_engine_id(exec_state, &args)?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::RevolveAboutEdge {
angle,
target: sketch.id.into(),
edge_id,
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
}),
)
.await?;
}
}
do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await
}

View File

@ -0,0 +1,705 @@
//! Functions related to line segments.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kittycad_modeling_cmds::shared::Angle;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Point2d, Sketch, TagIdentifier},
std::{utils::between, Args},
};
/// Returns the point at the end of the given segment.
pub async fn segment_end(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
let result = inner_segment_end(&tag, exec_state, args.clone())?;
args.make_user_val_from_point(result)
}
/// Compute the ending point of the provided line segment.
///
/// ```no_run
/// w = 15
/// cube = startSketchOn('XY')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [w, 0], tag = $line1)
/// |> line(end = [0, w], tag = $line2)
/// |> line(end = [-w, 0], tag = $line3)
/// |> line(end = [0, -w], tag = $line4)
/// |> close()
/// |> extrude(length = 5)
///
/// fn cylinder(radius, tag) {
/// return startSketchOn('XY')
/// |> startProfileAt([0, 0], %)
/// |> circle(radius = radius, center = segEnd(tag) )
/// |> extrude(length = radius)
/// }
///
/// cylinder(1, line1)
/// cylinder(2, line2)
/// cylinder(3, line3)
/// cylinder(4, line4)
/// ```
#[stdlib {
name = "segEnd",
keywords = true,
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
}
}]
fn inner_segment_end(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<[f64; 2], KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
Ok(path.get_base().to)
}
/// Returns the segment end of x.
pub async fn segment_end_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
let result = inner_segment_end_x(&tag, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the ending point of the provided line segment along the 'x' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [20, 0], tag = $thing)
/// |> line(end = [0, 5])
/// |> line(end = [segEndX(thing), 0])
/// |> line(end = [-20, 10])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segEndX",
keywords = true,
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
}
}]
fn inner_segment_end_x(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
Ok(path.get_base().to[0])
}
/// Returns the segment end of y.
pub async fn segment_end_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
let result = inner_segment_end_y(&tag, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the ending point of the provided line segment along the 'y' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [20, 0])
/// |> line(end = [0, 3], tag = $thing)
/// |> line(end = [-10, 0])
/// |> line(end = [0, segEndY(thing)])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segEndY",
keywords = true,
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
}
}]
fn inner_segment_end_y(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
Ok(path.get_to()[1])
}
/// Returns the point at the start of the given segment.
pub async fn segment_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
let result = inner_segment_start(&tag, exec_state, args.clone())?;
args.make_user_val_from_point(result)
}
/// Compute the starting point of the provided line segment.
///
/// ```no_run
/// w = 15
/// cube = startSketchOn('XY')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [w, 0], tag = $line1)
/// |> line(end = [0, w], tag = $line2)
/// |> line(end = [-w, 0], tag = $line3)
/// |> line(end = [0, -w], tag = $line4)
/// |> close()
/// |> extrude(length = 5)
///
/// fn cylinder(radius, tag) {
/// return startSketchOn('XY')
/// |> startProfileAt([0, 0], %)
/// |> circle( radius = radius, center = segStart(tag) )
/// |> extrude(length = radius)
/// }
///
/// cylinder(1, line1)
/// cylinder(2, line2)
/// cylinder(3, line3)
/// cylinder(4, line4)
/// ```
#[stdlib {
name = "segStart",
keywords = true,
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
}
}]
fn inner_segment_start(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<[f64; 2], KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
Ok(path.get_from().to_owned())
}
/// Returns the segment start of x.
pub async fn segment_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
let result = inner_segment_start_x(&tag, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the starting point of the provided line segment along the 'x' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [20, 0], tag = $thing)
/// |> line(end = [0, 5])
/// |> line(end = [20 - segStartX(thing), 0])
/// |> line(end = [-20, 10])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segStartX",
keywords = true,
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
}
}]
fn inner_segment_start_x(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
Ok(path.get_from()[0])
}
/// Returns the segment start of y.
pub async fn segment_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
let result = inner_segment_start_y(&tag, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the starting point of the provided line segment along the 'y' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [20, 0])
/// |> line(end = [0, 3], tag = $thing)
/// |> line(end = [-10, 0])
/// |> line(end = [0, 20-segStartY(thing)])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segStartY",
keywords = true,
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
}
}]
fn inner_segment_start_y(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
Ok(path.get_from()[1])
}
/// Returns the last segment of x.
pub async fn last_segment_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch")?;
let result = inner_last_segment_x(sketch, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Extract the 'x' axis value of the last line segment in the provided 2-d
/// sketch.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> line(end = [5, 0])
/// |> line(end = [20, 5])
/// |> line(end = [lastSegX(%), 0])
/// |> line(end = [-15, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "lastSegX",
keywords = true,
unlabeled_first = true,
args = {
sketch = { docs = "The sketch whose line segment is being queried"},
}
}]
fn inner_last_segment_x(sketch: Sketch, args: Args) -> Result<f64, KclError> {
let last_line = sketch
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a Sketch with at least one segment, found `{:?}`", sketch),
source_ranges: vec![args.source_range],
})
})?
.get_base();
Ok(last_line.to[0])
}
/// Returns the last segment of y.
pub async fn last_segment_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch")?;
let result = inner_last_segment_y(sketch, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Extract the 'y' axis value of the last line segment in the provided 2-d
/// sketch.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> line(end = [5, 0])
/// |> line(end = [20, 5])
/// |> line(end = [0, lastSegY(%)])
/// |> line(end = [-15, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "lastSegY",
keywords = true,
unlabeled_first = true,
args = {
sketch = { docs = "The sketch whose line segment is being queried"},
}
}]
fn inner_last_segment_y(sketch: Sketch, args: Args) -> Result<f64, KclError> {
let last_line = sketch
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a Sketch with at least one segment, found `{:?}`", sketch),
source_ranges: vec![args.source_range],
})
})?
.get_base();
Ok(last_line.to[1])
}
/// Returns the length of the segment.
pub async fn segment_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
let result = inner_segment_length(&tag, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the length of the provided line segment.
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 60,
/// length = 10,
/// }, %, $thing)
/// |> tangentialArc({
/// offset = -120,
/// radius = 5,
/// }, %)
/// |> angledLine({
/// angle = -60,
/// length = segLen(thing),
/// }, %)
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segLen",
keywords = true,
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
}
}]
fn inner_segment_length(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
let result = path.length();
Ok(result)
}
/// Returns the angle of the segment.
pub async fn segment_angle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
let result = inner_segment_angle(&tag, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Compute the angle (in degrees) of the provided line segment.
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> line(end = [5, 10], tag = $seg01)
/// |> line(end = [-10, 0])
/// |> angledLine([segAng(seg01), 10], %)
/// |> line(end = [-10, 0])
/// |> angledLine([segAng(seg01), -15], %)
/// |> close()
///
/// example = extrude(exampleSketch, length = 4)
/// ```
#[stdlib {
name = "segAng",
keywords = true,
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
}
}]
fn inner_segment_angle(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
let result = between(path.get_from().into(), path.get_to().into());
Ok(result.to_degrees())
}
/// Returns the angle coming out of the end of the segment in degrees.
pub async fn tangent_to_end(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
let result = inner_tangent_to_end(&tag, exec_state, args.clone()).await?;
Ok(args.make_user_val_from_f64(result))
}
/// Returns the angle coming out of the end of the segment in degrees.
///
/// ```no_run
/// // Horizontal pill.
/// pillSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [20, 0])
/// |> tangentialArcToRelative([0, 10], %, $arc1)
/// |> angledLine({
/// angle: tangentToEnd(arc1),
/// length: 20,
/// }, %)
/// |> tangentialArcToRelative([0, -10], %)
/// |> close()
///
/// pillExtrude = extrude(pillSketch, length = 10)
/// ```
///
/// ```no_run
/// // Vertical pill. Use absolute coordinate for arc.
/// pillSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 20])
/// |> tangentialArcTo([10, 20], %, $arc1)
/// |> angledLine({
/// angle: tangentToEnd(arc1),
/// length: 20,
/// }, %)
/// |> tangentialArcToRelative([-10, 0], %)
/// |> close()
///
/// pillExtrude = extrude(pillSketch, length = 10)
/// ```
///
/// ```no_run
/// rectangleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0], tag = $seg1)
/// |> angledLine({
/// angle: tangentToEnd(seg1),
/// length: 10,
/// }, %)
/// |> line(end = [0, 10])
/// |> line(end = [-20, 0])
/// |> close()
///
/// rectangleExtrude = extrude(rectangleSketch, length = 10)
/// ```
///
/// ```no_run
/// bottom = startSketchOn("XY")
/// |> startProfileAt([0, 0], %)
/// |> arcTo({
/// end: [10, 10],
/// interior: [5, 1]
/// }, %, $arc1)
/// |> angledLine([tangentToEnd(arc1), 20], %)
/// |> close()
/// ```
///
/// ```no_run
/// circSketch = startSketchOn("XY")
/// |> circle( center= [0, 0], radius= 3 , tag= $circ)
///
/// triangleSketch = startSketchOn("XY")
/// |> startProfileAt([-5, 0], %)
/// |> angledLine([tangentToEnd(circ), 10], %)
/// |> line(end = [-15, 0])
/// |> close()
/// ```
#[stdlib {
name = "tangentToEnd",
keywords = true,
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
}
}]
async fn inner_tangent_to_end(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
let from = Point2d::from(path.get_to());
// Undocumented voodoo from get_tangential_arc_to_info
let tangent_info = path.get_tangential_info();
let tan_previous_point = tangent_info.tan_previous_point(from.into());
// Calculate the end point from the angle and radius.
// atan2 outputs radians.
let previous_end_tangent = Angle::from_radians(f64::atan2(
from.y - tan_previous_point[1],
from.x - tan_previous_point[0],
));
Ok(previous_end_tangent.to_degrees())
}
/// Returns the angle to match the given length for x.
pub async fn angle_to_match_length_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
let result = inner_angle_to_match_length_x(&tag, to, sketch, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Returns the angle to match the given length for x.
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [2, 5], tag = $seg01)
/// |> angledLineToX([
/// -angleToMatchLengthX(seg01, 7, %),
/// 10
/// ], %)
/// |> close()
///
/// extrusion = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "angleToMatchLengthX",
}]
fn inner_angle_to_match_length_x(
tag: &TagIdentifier,
to: f64,
sketch: Sketch,
exec_state: &mut ExecState,
args: Args,
) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
let length = path.length();
let last_line = sketch
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a Sketch with at least one segment, found `{:?}`", sketch),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let diff = (to - last_line.to[0]).abs();
let angle_r = (diff / length).acos();
if diff > length {
Ok(0.0)
} else {
Ok(angle_r.to_degrees())
}
}
/// Returns the angle to match the given length for y.
pub async fn angle_to_match_length_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
let result = inner_angle_to_match_length_y(&tag, to, sketch, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64(result))
}
/// Returns the angle to match the given length for y.
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [1, 2], tag = $seg01)
/// |> angledLine({
/// angle = angleToMatchLengthY(seg01, 15, %),
/// length = 5,
/// }, %)
/// |> yLineTo(0, %)
/// |> close()
///
/// extrusion = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "angleToMatchLengthY",
}]
fn inner_angle_to_match_length_y(
tag: &TagIdentifier,
to: f64,
sketch: Sketch,
exec_state: &mut ExecState,
args: Args,
) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
let length = path.length();
let last_line = sketch
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a Sketch with at least one segment, found `{:?}`", sketch),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let diff = (to - last_line.to[1]).abs();
let angle_r = (diff / length).asin();
if diff > length {
Ok(0.0)
} else {
Ok(angle_r.to_degrees())
}
}

View File

@ -0,0 +1,465 @@
//! Standard library shapes.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{
each_cmd as mcmd,
length_unit::LengthUnit,
shared::{Angle, Point2d as KPoint2d},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use kittycad_modeling_cmds::shared::PathSegment;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{BasePath, ExecState, GeoMeta, KclValue, Path, Sketch, SketchSurface},
parsing::ast::types::TagNode,
std::{
sketch::NEW_TAG_KW,
utils::{calculate_circle_center, distance},
Args,
},
};
/// A sketch surface or a sketch.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(untagged)]
pub enum SketchOrSurface {
SketchSurface(SketchSurface),
Sketch(Box<Sketch>),
}
/// Sketch a circle.
pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_or_surface = args.get_unlabeled_kw_arg("sketchOrSurface")?;
let center = args.get_kw_arg("center")?;
let radius = args.get_kw_arg("radius")?;
let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
let sketch = inner_circle(sketch_or_surface, center, radius, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
value: Box::new(sketch),
})
}
/// Construct a 2-dimensional circle, of the specified radius, centered at
/// the provided (x, y) origin point.
///
/// ```no_run
/// exampleSketch = startSketchOn("-XZ")
/// |> circle( center = [0, 0], radius = 10 )
///
/// example = extrude(exampleSketch, length = 5)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([-15, 0], %)
/// |> line(end = [30, 0])
/// |> line(end = [0, 30])
/// |> line(end = [-30, 0])
/// |> close()
/// |> hole(circle( center = [0, 15], radius = 5), %)
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "circle",
keywords = true,
unlabeled_first = true,
args = {
sketch_or_surface = {docs = "Plane or surface to sketch on."},
center = {docs = "The center of the circle."},
radius = {docs = "The radius of the circle."},
tag = { docs = "Create a new tag which refers to this circle"},
}
}]
async fn inner_circle(
sketch_or_surface: SketchOrSurface,
center: [f64; 2],
radius: f64,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
let sketch_surface = match sketch_or_surface {
SketchOrSurface::SketchSurface(surface) => surface,
SketchOrSurface::Sketch(s) => s.on,
};
let units = sketch_surface.units();
let sketch = crate::std::sketch::inner_start_profile_at(
[center[0] + radius, center[1]],
sketch_surface,
None,
exec_state,
args.clone(),
)
.await?;
let from = [center[0] + radius, center[1]];
let angle_start = Angle::zero();
let angle_end = Angle::turn();
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::ExtendPath {
path: sketch.id.into(),
segment: PathSegment::Arc {
start: angle_start,
end: angle_end,
center: KPoint2d::from(center).map(LengthUnit),
radius: radius.into(),
relative: false,
},
}),
)
.await?;
let current_path = Path::Circle {
base: BasePath {
from,
to: from,
tag: tag.clone(),
units,
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
radius,
center,
ccw: angle_start < angle_end,
};
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
}
new_sketch.paths.push(current_path);
args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
.await?;
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])
/// |> extrude(length = 5)
/// ```
#[stdlib {
name = "circleThreePoint",
keywords = true,
unlabeled_first = true,
args = {
p1 = {docs = "1st point to derive the circle."},
p2 = {docs = "2nd point to derive the circle."},
p3 = {docs = "3rd point to derive the circle."},
sketch_surface_or_group = {docs = "Plane or surface to sketch on."},
tag = {docs = "Identifier for the circle to reference elsewhere."},
}
}]
// Similar to inner_circle, but needs to retain 3-point information in the
// path so it can be used for other features, otherwise it's lost.
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);
// It can be the distance to any of the 3 points - they all lay on the circumference.
let radius = distance(center.into(), p2.into());
let sketch_surface = match sketch_surface_or_group {
SketchOrSurface::SketchSurface(surface) => surface,
SketchOrSurface::Sketch(group) => group.on,
};
let sketch = crate::std::sketch::inner_start_profile_at(
[center[0] + radius, center[1]],
sketch_surface,
None,
exec_state,
args.clone(),
)
.await?;
let from = [center[0] + radius, center[1]];
let angle_start = Angle::zero();
let angle_end = Angle::turn();
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::ExtendPath {
path: sketch.id.into(),
segment: PathSegment::Arc {
start: angle_start,
end: angle_end,
center: KPoint2d::from(center).map(LengthUnit),
radius: radius.into(),
relative: false,
},
}),
)
.await?;
let current_path = Path::CircleThreePoint {
base: BasePath {
from,
to: from,
tag: tag.clone(),
units: sketch.units,
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
p1,
p2,
p3,
};
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
}
new_sketch.paths.push(current_path);
args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
.await?;
Ok(new_sketch)
}
/// Type of the polygon
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Default)]
#[ts(export)]
#[serde(rename_all = "lowercase")]
pub enum PolygonType {
#[default]
Inscribed,
Circumscribed,
}
/// Data for drawing a polygon
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct PolygonData {
/// The radius of the polygon
pub radius: f64,
/// The number of sides in the polygon
pub num_sides: u64,
/// The center point of the polygon
pub center: [f64; 2],
/// The type of the polygon (inscribed or circumscribed)
#[serde(skip)]
pub polygon_type: PolygonType,
/// Whether the polygon is inscribed (true) or circumscribed (false) about a circle with the specified radius
#[serde(default = "default_inscribed")]
pub inscribed: bool,
}
fn default_inscribed() -> bool {
true
}
/// Create a regular polygon with the specified number of sides and radius.
pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch_surface_or_group, tag): (PolygonData, SketchOrSurface, Option<TagNode>) =
args.get_polygon_args()?;
let sketch = inner_polygon(data, sketch_surface_or_group, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
value: Box::new(sketch),
})
}
/// Create a regular polygon with the specified number of sides that is either inscribed or circumscribed around a circle of the specified radius.
///
/// ```no_run
/// // Create a regular hexagon inscribed in a circle of radius 10
/// hex = startSketchOn('XY')
/// |> polygon({
/// radius = 10,
/// numSides = 6,
/// center = [0, 0],
/// inscribed = true,
/// }, %)
///
/// example = extrude(hex, length = 5)
/// ```
///
/// ```no_run
/// // Create a square circumscribed around a circle of radius 5
/// square = startSketchOn('XY')
/// |> polygon({
/// radius = 5.0,
/// numSides = 4,
/// center = [10, 10],
/// inscribed = false,
/// }, %)
/// example = extrude(square, length = 5)
/// ```
#[stdlib {
name = "polygon",
}]
async fn inner_polygon(
data: PolygonData,
sketch_surface_or_group: SketchOrSurface,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
if data.num_sides < 3 {
return Err(KclError::Type(KclErrorDetails {
message: "Polygon must have at least 3 sides".to_string(),
source_ranges: vec![args.source_range],
}));
}
if data.radius <= 0.0 {
return Err(KclError::Type(KclErrorDetails {
message: "Radius must be greater than 0".to_string(),
source_ranges: vec![args.source_range],
}));
}
let sketch_surface = match sketch_surface_or_group {
SketchOrSurface::SketchSurface(surface) => surface,
SketchOrSurface::Sketch(group) => group.on,
};
let half_angle = std::f64::consts::PI / data.num_sides as f64;
let radius_to_vertices = match data.polygon_type {
PolygonType::Inscribed => data.radius,
PolygonType::Circumscribed => data.radius / half_angle.cos(),
};
let angle_step = 2.0 * std::f64::consts::PI / data.num_sides as f64;
let vertices: Vec<[f64; 2]> = (0..data.num_sides)
.map(|i| {
let angle = angle_step * i as f64;
[
data.center[0] + radius_to_vertices * angle.cos(),
data.center[1] + radius_to_vertices * angle.sin(),
]
})
.collect();
let mut sketch =
crate::std::sketch::inner_start_profile_at(vertices[0], sketch_surface, None, exec_state, args.clone()).await?;
// Draw all the lines with unique IDs and modified tags
for vertex in vertices.iter().skip(1) {
let from = sketch.current_pen_position()?;
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::ExtendPath {
path: sketch.id.into(),
segment: PathSegment::Line {
end: KPoint2d::from(*vertex).with_z(0.0).map(LengthUnit),
relative: false,
},
}),
)
.await?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to: *vertex,
tag: tag.clone(),
units: sketch.units,
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
if let Some(tag) = &tag {
sketch.add_tag(tag, &current_path);
}
sketch.paths.push(current_path);
}
// Close the polygon by connecting back to the first vertex with a new ID
let from = sketch.current_pen_position()?;
let close_id = exec_state.next_uuid();
args.batch_modeling_cmd(
close_id,
ModelingCmd::from(mcmd::ExtendPath {
path: sketch.id.into(),
segment: PathSegment::Line {
end: KPoint2d::from(vertices[0]).with_z(0.0).map(LengthUnit),
relative: false,
},
}),
)
.await?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to: vertices[0],
tag: tag.clone(),
units: sketch.units,
geo_meta: GeoMeta {
id: close_id,
metadata: args.source_range.into(),
},
},
};
if let Some(tag) = &tag {
sketch.add_tag(tag, &current_path);
}
sketch.paths.push(current_path);
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
)
.await?;
Ok(sketch)
}

View File

@ -0,0 +1,331 @@
//! Standard library shells.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Solid, SolidSet},
std::{sketch::FaceTag, Args},
};
/// Create a shell.
pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid_set = args.get_unlabeled_kw_arg("solidSet")?;
let thickness = args.get_kw_arg("thickness")?;
let faces = args.get_kw_arg("faces")?;
let result = inner_shell(solid_set, thickness, faces, exec_state, args).await?;
Ok(result.into())
}
/// Remove volume from a 3-dimensional shape such that a wall of the
/// provided thickness remains, taking volume starting at the provided
/// face, leaving it open in that direction.
///
/// ```no_run
/// // Remove the end face for the extrusion.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
///
/// // Remove the end face for the extrusion.
/// shell(
/// firstSketch,
/// faces = ['end'],
/// thickness = 0.25,
/// )
/// ```
///
/// ```no_run
/// // Remove the start face for the extrusion.
/// firstSketch = startSketchOn('-XZ')
/// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
///
/// // Remove the start face for the extrusion.
/// shell(
/// firstSketch,
/// faces = ['start'],
/// thickness = 0.25,
/// )
/// ```
///
/// ```no_run
/// // Remove a tagged face and the end face for the extrusion.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0], tag = $myTag)
/// |> close()
/// |> extrude(length = 6)
///
/// // Remove a tagged face for the extrusion.
/// shell(
/// firstSketch,
/// faces = [myTag],
/// thickness = 0.25,
/// )
/// ```
///
/// ```no_run
/// // Remove multiple faces at once.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0], tag = $myTag)
/// |> close()
/// |> extrude(length = 6)
///
/// // Remove a tagged face and the end face for the extrusion.
/// shell(
/// firstSketch,
/// faces = [myTag, 'end'],
/// thickness = 0.25,
/// )
/// ```
///
/// ```no_run
/// // Shell a sketch on face.
/// size = 100
/// case = startSketchOn('-XZ')
/// |> startProfileAt([-size, -size], %)
/// |> line(end = [2 * size, 0])
/// |> line(end = [0, 2 * size])
/// |> tangentialArcTo([-size, size], %)
/// |> close()
/// |> extrude(length = 65)
///
/// thing1 = startSketchOn(case, 'end')
/// |> circle( center = [-size / 2, -size / 2], radius = 25 )
/// |> extrude(length = 50)
///
/// thing2 = startSketchOn(case, 'end')
/// |> circle( center = [size / 2, -size / 2], radius = 25 )
/// |> extrude(length = 50)
///
/// // We put "case" in the shell function to shell the entire object.
/// shell(case, faces = ['start'], thickness = 5)
/// ```
///
/// ```no_run
/// // Shell a sketch on face object on the end face.
/// size = 100
/// case = startSketchOn('XY')
/// |> startProfileAt([-size, -size], %)
/// |> line(end = [2 * size, 0])
/// |> line(end = [0, 2 * size])
/// |> tangentialArcTo([-size, size], %)
/// |> close()
/// |> extrude(length = 65)
///
/// thing1 = startSketchOn(case, 'end')
/// |> circle( center = [-size / 2, -size / 2], radius = 25 )
/// |> extrude(length = 50)
///
/// thing2 = startSketchOn(case, 'end')
/// |> circle( center = [size / 2, -size / 2], radius = 25 )
/// |> extrude(length = 50)
///
/// // We put "thing1" in the shell function to shell the end face of the object.
/// shell(thing1, faces = ['end'], thickness = 5)
/// ```
///
/// ```no_run
/// // Shell sketched on face objects on the end face, include all sketches to shell
/// // the entire object.
///
/// size = 100
/// case = startSketchOn('XY')
/// |> startProfileAt([-size, -size], %)
/// |> line(end = [2 * size, 0])
/// |> line(end = [0, 2 * size])
/// |> tangentialArcTo([-size, size], %)
/// |> close()
/// |> extrude(length = 65)
///
/// thing1 = startSketchOn(case, 'end')
/// |> circle( center = [-size / 2, -size / 2], radius = 25 )
/// |> extrude(length = 50)
///
/// thing2 = startSketchOn(case, 'end')
/// |> circle( center = [size / 2, -size / 2], radius = 25)
/// |> extrude(length = 50)
///
/// // We put "thing1" and "thing2" in the shell function to shell the end face of the object.
/// shell([thing1, thing2], faces = ['end'], thickness = 5)
/// ```
#[stdlib {
name = "shell",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "Which solid (or solids) to shell out"},
thickness = {docs = "The thickness of the shell"},
faces = {docs = "The faces you want removed"},
}
}]
async fn inner_shell(
solid_set: SolidSet,
thickness: f64,
faces: Vec<FaceTag>,
exec_state: &mut ExecState,
args: Args,
) -> Result<SolidSet, KclError> {
if faces.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "You must shell at least one face".to_string(),
source_ranges: vec![args.source_range],
}));
}
let solids: Vec<Box<Solid>> = solid_set.clone().into();
if solids.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "You must shell at least one solid".to_string(),
source_ranges: vec![args.source_range],
}));
}
let mut face_ids = Vec::new();
for solid in &solids {
// Flush the batch for our fillets/chamfers if there are any.
// If we do not do these for sketch on face, things will fail with face does not exist.
args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?;
for tag in &faces {
let extrude_plane_id = tag.get_face_id(solid, exec_state, &args, false).await?;
face_ids.push(extrude_plane_id);
}
}
if face_ids.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "Expected at least one valid face".to_string(),
source_ranges: vec![args.source_range],
}));
}
// Make sure all the solids have the same id, as we are going to shell them all at
// once.
if !solids.iter().all(|eg| eg.id == solids[0].id) {
return Err(KclError::Type(KclErrorDetails {
message: "All solids stem from the same root object, like multiple sketch on face extrusions, etc."
.to_string(),
source_ranges: vec![args.source_range],
}));
}
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dShellFace {
hollow: false,
face_ids,
object_id: solids[0].id,
shell_thickness: LengthUnit(thickness),
}),
)
.await?;
Ok(solid_set)
}
/// Make the inside of a 3D object hollow.
pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (thickness, solid): (f64, Box<Solid>) = args.get_data_and_solid()?;
let value = inner_hollow(thickness, solid, exec_state, args).await?;
Ok(KclValue::Solid { value })
}
/// Make the inside of a 3D object hollow.
///
/// Remove volume from a 3-dimensional shape such that a wall of the
/// provided thickness remains around the exterior of the shape.
///
/// ```no_run
/// // Hollow a basic sketch.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
/// |> hollow (0.25, %)
/// ```
///
/// ```no_run
/// // Hollow a basic sketch.
/// firstSketch = startSketchOn('-XZ')
/// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
/// |> hollow (0.5, %)
/// ```
///
/// ```no_run
/// // Hollow a sketch on face object.
/// size = 100
/// case = startSketchOn('-XZ')
/// |> startProfileAt([-size, -size], %)
/// |> line(end = [2 * size, 0])
/// |> line(end = [0, 2 * size])
/// |> tangentialArcTo([-size, size], %)
/// |> close()
/// |> extrude(length = 65)
///
/// thing1 = startSketchOn(case, 'end')
/// |> circle( center = [-size / 2, -size / 2], radius = 25 )
/// |> extrude(length = 50)
///
/// thing2 = startSketchOn(case, 'end')
/// |> circle( center = [size / 2, -size / 2], radius = 25 )
/// |> extrude(length = 50)
///
/// hollow(0.5, case)
/// ```
#[stdlib {
name = "hollow",
feature_tree_operation = true,
}]
async fn inner_hollow(
thickness: f64,
solid: Box<Solid>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
// Flush the batch for our fillets/chamfers if there are any.
// If we do not do these for sketch on face, things will fail with face does not exist.
args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?;
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dShellFace {
hollow: true,
face_ids: Vec::new(), // This is empty because we want to hollow the entire object.
object_id: solid.id,
shell_thickness: LengthUnit(thickness),
}),
)
.await?;
Ok(solid)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,133 @@
//! Standard library sweep.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
execution::{ExecState, Helix, KclValue, Sketch, Solid},
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
};
/// A path to sweep along.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(untagged)]
pub enum SweepPath {
Sketch(Sketch),
Helix(Box<Helix>),
}
/// Extrude a sketch along a path.
pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch")?;
let path: SweepPath = args.get_kw_arg("path")?;
let sectional = args.get_kw_arg_opt("sectional")?;
let tolerance = args.get_kw_arg_opt("tolerance")?;
let value = inner_sweep(sketch, path, sectional, tolerance, exec_state, args).await?;
Ok(KclValue::Solid { value })
}
/// Extrude a sketch along a path.
///
/// This, like extrude, is able to create a 3-dimensional solid from a
/// 2-dimensional sketch. However, unlike extrude, this creates a solid
/// by using the extent of the sketch as its path. This is useful for
/// creating more complex shapes that can't be created with a simple
/// extrusion.
///
/// ```no_run
/// // Create a pipe using a sweep.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn('XZ')
/// |> startProfileAt([0.05, 0.05], %)
/// |> line(end = [0, 7])
/// |> tangentialArc({
/// offset: 90,
/// radius: 5
/// }, %)
/// |> line(end = [-3, 0])
/// |> tangentialArc({
/// offset: -90,
/// radius: 5
/// }, %)
/// |> line(end = [0, 7])
///
/// // Create a hole for the pipe.
/// pipeHole = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 1.5,
/// )
///
/// sweepSketch = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 2,
/// )
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// ```
///
/// ```no_run
/// // Create a spring by sweeping around a helix path.
///
/// // Create a helix around the Z axis.
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 4,
/// length = 10,
/// radius = 5,
/// axis = 'Z',
/// )
///
///
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('YZ')
/// |> circle( center = [0, 0], radius = 1)
/// |> sweep(path = helixPath)
/// ```
#[stdlib {
name = "sweep",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
sketch = { docs = "The sketch that should be swept in space" },
path = { docs = "The path to sweep the sketch along" },
sectional = { docs = "If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components." },
tolerance = { docs = "Tolerance for this operation" },
}
}]
async fn inner_sweep(
sketch: Sketch,
path: SweepPath,
sectional: Option<bool>,
tolerance: Option<f64>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Sweep {
target: sketch.id.into(),
trajectory: match path {
SweepPath::Sketch(sketch) => sketch.id.into(),
SweepPath::Helix(helix) => helix.value.into(),
},
sectional: sectional.unwrap_or(false),
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
}),
)
.await?;
do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await
}

View File

@ -0,0 +1,715 @@
//! Standard library transforms.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{
each_cmd as mcmd,
length_unit::LengthUnit,
shared,
shared::{Point3d, Point4d},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Solid},
std::Args,
};
/// Scale a solid.
pub async fn scale(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?;
let scale = args.get_kw_arg("scale")?;
let global = args.get_kw_arg_opt("global")?;
let solid = inner_scale(solid, scale, global, exec_state, args).await?;
Ok(KclValue::Solid { value: solid })
}
/// Scale a solid.
///
/// By default the transform is applied in local sketch axis, therefore the origin will not move.
///
/// If you want to apply the transform in global space, set `global` to `true`. The origin of the
/// model will move. If the model is not centered on origin and you scale globally it will
/// look like the model moves and gets bigger at the same time. Say you have a square
/// `(1,1) - (1,2) - (2,2) - (2,1)` and you scale by 2 globally it will become
/// `(2,2) - (2,4)`...etc so the origin has moved from `(1.5, 1.5)` to `(2,2)`.
///
/// ```no_run
/// // Scale a pipe.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn('XZ')
/// |> startProfileAt([0.05, 0.05], %)
/// |> line(end = [0, 7])
/// |> tangentialArc({
/// offset: 90,
/// radius: 5
/// }, %)
/// |> line(end = [-3, 0])
/// |> tangentialArc({
/// offset: -90,
/// radius: 5
/// }, %)
/// |> line(end = [0, 7])
///
/// // Create a hole for the pipe.
/// pipeHole = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 1.5,
/// )
///
/// sweepSketch = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 2,
/// )
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> scale(
/// scale = [1.0, 1.0, 2.5],
/// )
/// ```
#[stdlib {
name = "scale",
feature_tree_operation = false,
keywords = true,
unlabeled_first = true,
args = {
solid = {docs = "The solid to scale."},
scale = {docs = "The scale factor for the x, y, and z axes."},
global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
}
}]
async fn inner_scale(
solid: Box<Solid>,
scale: [f64; 3],
global: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid.id,
transforms: vec![shared::ComponentTransform {
scale: Some(shared::TransformBy::<Point3d<f64>> {
property: Point3d {
x: scale[0],
y: scale[1],
z: scale[2],
},
set: false,
is_local: !global.unwrap_or(false),
}),
translate: None,
rotate_rpy: None,
rotate_angle_axis: None,
}],
}),
)
.await?;
Ok(solid)
}
/// Move a solid.
pub async fn translate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?;
let translate = args.get_kw_arg("translate")?;
let global = args.get_kw_arg_opt("global")?;
let solid = inner_translate(solid, translate, global, exec_state, args).await?;
Ok(KclValue::Solid { value: solid })
}
/// Move a solid.
///
/// ```no_run
/// // Move a pipe.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn('XZ')
/// |> startProfileAt([0.05, 0.05], %)
/// |> line(end = [0, 7])
/// |> tangentialArc({
/// offset: 90,
/// radius: 5
/// }, %)
/// |> line(end = [-3, 0])
/// |> tangentialArc({
/// offset: -90,
/// radius: 5
/// }, %)
/// |> line(end = [0, 7])
///
/// // Create a hole for the pipe.
/// pipeHole = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 1.5,
/// )
///
/// sweepSketch = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 2,
/// )
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> translate(
/// translate = [1.0, 1.0, 2.5],
/// )
/// ```
#[stdlib {
name = "translate",
feature_tree_operation = false,
keywords = true,
unlabeled_first = true,
args = {
solid = {docs = "The solid to move."},
translate = {docs = "The amount to move the solid in all three axes."},
global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
}
}]
async fn inner_translate(
solid: Box<Solid>,
translate: [f64; 3],
global: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid.id,
transforms: vec![shared::ComponentTransform {
translate: Some(shared::TransformBy::<Point3d<LengthUnit>> {
property: shared::Point3d {
x: LengthUnit(translate[0]),
y: LengthUnit(translate[1]),
z: LengthUnit(translate[2]),
},
set: false,
is_local: !global.unwrap_or(false),
}),
scale: None,
rotate_rpy: None,
rotate_angle_axis: None,
}],
}),
)
.await?;
Ok(solid)
}
/// Rotate a solid.
pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?;
let roll = args.get_kw_arg_opt("roll")?;
let pitch = args.get_kw_arg_opt("pitch")?;
let yaw = args.get_kw_arg_opt("yaw")?;
let axis = args.get_kw_arg_opt("axis")?;
let angle = args.get_kw_arg_opt("angle")?;
let global = args.get_kw_arg_opt("global")?;
// Check if no rotation values are provided.
if roll.is_none() && pitch.is_none() && yaw.is_none() && axis.is_none() && angle.is_none() {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided.".to_string(),
source_ranges: vec![args.source_range],
}));
}
// If they give us a roll, pitch, or yaw, they must give us all three.
if roll.is_some() || pitch.is_some() || yaw.is_some() {
if roll.is_none() {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected `roll` to be provided when `pitch` or `yaw` is provided.".to_string(),
source_ranges: vec![args.source_range],
}));
}
if pitch.is_none() {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected `pitch` to be provided when `roll` or `yaw` is provided.".to_string(),
source_ranges: vec![args.source_range],
}));
}
if yaw.is_none() {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected `yaw` to be provided when `roll` or `pitch` is provided.".to_string(),
source_ranges: vec![args.source_range],
}));
}
// Ensure they didn't also provide an axis or angle.
if axis.is_some() || angle.is_some() {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."
.to_string(),
source_ranges: vec![args.source_range],
}));
}
}
// If they give us an axis or angle, they must give us both.
if axis.is_some() || angle.is_some() {
if axis.is_none() {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected `axis` to be provided when `angle` is provided.".to_string(),
source_ranges: vec![args.source_range],
}));
}
if angle.is_none() {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected `angle` to be provided when `axis` is provided.".to_string(),
source_ranges: vec![args.source_range],
}));
}
// Ensure they didn't also provide a roll, pitch, or yaw.
if roll.is_some() || pitch.is_some() || yaw.is_some() {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected `roll`, `pitch`, and `yaw` to not be provided when `axis` and `angle` are provided."
.to_string(),
source_ranges: vec![args.source_range],
}));
}
}
// Validate the roll, pitch, and yaw values.
if let Some(roll) = roll {
if !(-360.0..=360.0).contains(&roll) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Expected roll to be between -360 and 360, found `{}`", roll),
source_ranges: vec![args.source_range],
}));
}
}
if let Some(pitch) = pitch {
if !(-360.0..=360.0).contains(&pitch) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Expected pitch to be between -360 and 360, found `{}`", pitch),
source_ranges: vec![args.source_range],
}));
}
}
if let Some(yaw) = yaw {
if !(-360.0..=360.0).contains(&yaw) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Expected yaw to be between -360 and 360, found `{}`", yaw),
source_ranges: vec![args.source_range],
}));
}
}
// Validate the axis and angle values.
if let Some(angle) = angle {
if !(-360.0..=360.0).contains(&angle) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Expected angle to be between -360 and 360, found `{}`", angle),
source_ranges: vec![args.source_range],
}));
}
}
let solid = inner_rotate(solid, roll, pitch, yaw, axis, angle, global, exec_state, args).await?;
Ok(KclValue::Solid { value: solid })
}
/// Rotate a solid.
///
/// ### Using Roll, Pitch, and Yaw
///
/// When rotating a part in 3D space, "roll," "pitch," and "yaw" refer to the
/// three rotational axes used to describe its orientation: roll is rotation
/// around the longitudinal axis (front-to-back), pitch is rotation around the
/// lateral axis (wing-to-wing), and yaw is rotation around the vertical axis
/// (up-down); essentially, it's like tilting the part on its side (roll),
/// tipping the nose up or down (pitch), and turning it left or right (yaw).
///
/// So, in the context of a 3D model:
///
/// - **Roll**: Imagine spinning a pencil on its tip - that's a roll movement.
///
/// - **Pitch**: Think of a seesaw motion, where the object tilts up or down along its side axis.
///
/// - **Yaw**: Like turning your head left or right, this is a rotation around the vertical axis
///
/// ### Using an Axis and Angle
///
/// When rotating a part around an axis, you specify the axis of rotation and the angle of
/// rotation.
///
/// ```no_run
/// // Rotate a pipe with roll, pitch, and yaw.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn('XZ')
/// |> startProfileAt([0.05, 0.05], %)
/// |> line(end = [0, 7])
/// |> tangentialArc({
/// offset: 90,
/// radius: 5
/// }, %)
/// |> line(end = [-3, 0])
/// |> tangentialArc({
/// offset: -90,
/// radius: 5
/// }, %)
/// |> line(end = [0, 7])
///
/// // Create a hole for the pipe.
/// pipeHole = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 1.5,
/// )
///
/// sweepSketch = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 2,
/// )
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> rotate(
/// roll = 10,
/// pitch = 10,
/// yaw = 90,
/// )
/// ```
///
/// ```no_run
/// // Rotate a pipe about an axis with an angle.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn('XZ')
/// |> startProfileAt([0.05, 0.05], %)
/// |> line(end = [0, 7])
/// |> tangentialArc({
/// offset: 90,
/// radius: 5
/// }, %)
/// |> line(end = [-3, 0])
/// |> tangentialArc({
/// offset: -90,
/// radius: 5
/// }, %)
/// |> line(end = [0, 7])
///
/// // Create a hole for the pipe.
/// pipeHole = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 1.5,
/// )
///
/// sweepSketch = startSketchOn('XY')
/// |> circle(
/// center = [0, 0],
/// radius = 2,
/// )
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> rotate(
/// axis = [0, 0, 1.0],
/// angle = 90,
/// )
/// ```
#[stdlib {
name = "rotate",
feature_tree_operation = false,
keywords = true,
unlabeled_first = true,
args = {
solid = {docs = "The solid to rotate."},
roll = {docs = "The roll angle in degrees. Must be used with `pitch` and `yaw`. Must be between -360 and 360.", include_in_snippet = true},
pitch = {docs = "The pitch angle in degrees. Must be used with `roll` and `yaw`. Must be between -360 and 360.", include_in_snippet = true},
yaw = {docs = "The yaw angle in degrees. Must be used with `roll` and `pitch`. Must be between -360 and 360.", include_in_snippet = true},
axis = {docs = "The axis to rotate around. Must be used with `angle`.", include_in_snippet = false},
angle = {docs = "The angle to rotate in degrees. Must be used with `axis`. Must be between -360 and 360.", include_in_snippet = false},
global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
}
}]
#[allow(clippy::too_many_arguments)]
async fn inner_rotate(
solid: Box<Solid>,
roll: Option<f64>,
pitch: Option<f64>,
yaw: Option<f64>,
axis: Option<[f64; 3]>,
angle: Option<f64>,
global: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
let id = exec_state.next_uuid();
if let (Some(roll), Some(pitch), Some(yaw)) = (roll, pitch, yaw) {
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid.id,
transforms: vec![shared::ComponentTransform {
rotate_rpy: Some(shared::TransformBy::<Point3d<f64>> {
property: shared::Point3d {
x: roll,
y: pitch,
z: yaw,
},
set: false,
is_local: !global.unwrap_or(false),
}),
scale: None,
rotate_angle_axis: None,
translate: None,
}],
}),
)
.await?;
}
if let (Some(axis), Some(angle)) = (axis, angle) {
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid.id,
transforms: vec![shared::ComponentTransform {
rotate_angle_axis: Some(shared::TransformBy::<Point4d<f64>> {
property: shared::Point4d {
x: axis[0],
y: axis[1],
z: axis[2],
w: angle,
},
set: false,
is_local: !global.unwrap_or(false),
}),
scale: None,
rotate_rpy: None,
translate: None,
}],
}),
)
.await?;
}
Ok(solid)
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::execution::parse_execute;
const PIPE: &str = r#"sweepPath = startSketchOn('XZ')
|> startProfileAt([0.05, 0.05], %)
|> line(end = [0, 7])
|> tangentialArc({
offset: 90,
radius: 5
}, %)
|> line(end = [-3, 0])
|> tangentialArc({
offset: -90,
radius: 5
}, %)
|> line(end = [0, 7])
// Create a hole for the pipe.
pipeHole = startSketchOn('XY')
|> circle(
center = [0, 0],
radius = 1.5,
)
sweepSketch = startSketchOn('XY')
|> circle(
center = [0, 0],
radius = 2,
)
|> hole(pipeHole, %)
|> sweep(
path = sweepPath,
)"#;
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_empty() {
let ast = PIPE.to_string()
+ r#"
|> rotate()
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided."#.to_string()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_axis_no_angle() {
let ast = PIPE.to_string()
+ r#"
|> rotate(
axis = [0, 0, 1.0],
)
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected `angle` to be provided when `axis` is provided."#.to_string()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_angle_no_axis() {
let ast = PIPE.to_string()
+ r#"
|> rotate(
angle = 90,
)
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected `axis` to be provided when `angle` is provided."#.to_string()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_angle_out_of_range() {
let ast = PIPE.to_string()
+ r#"
|> rotate(
axis = [0, 0, 1.0],
angle = 900,
)
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected angle to be between -360 and 360, found `900`"#.to_string()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_angle_axis_yaw() {
let ast = PIPE.to_string()
+ r#"
|> rotate(
axis = [0, 0, 1.0],
angle = 90,
yaw = 90,
)
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected `roll` to be provided when `pitch` or `yaw` is provided."#.to_string()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_yaw_no_pitch() {
let ast = PIPE.to_string()
+ r#"
|> rotate(
yaw = 90,
)
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected `roll` to be provided when `pitch` or `yaw` is provided."#.to_string()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_yaw_out_of_range() {
let ast = PIPE.to_string()
+ r#"
|> rotate(
yaw = 900,
pitch = 90,
roll = 90,
)
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected yaw to be between -360 and 360, found `900`"#.to_string()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_roll_out_of_range() {
let ast = PIPE.to_string()
+ r#"
|> rotate(
yaw = 90,
pitch = 90,
roll = 900,
)
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected roll to be between -360 and 360, found `900`"#.to_string()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_pitch_out_of_range() {
let ast = PIPE.to_string()
+ r#"
|> rotate(
yaw = 90,
pitch = 900,
roll = 90,
)
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected pitch to be between -360 and 360, found `900`"#.to_string()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rotate_roll_pitch_yaw_with_angle() {
let ast = PIPE.to_string()
+ r#"
|> rotate(
yaw = 90,
pitch = 90,
roll = 90,
angle = 90,
)
"#;
let result = parse_execute(&ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
.to_string()
);
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,260 @@
//! Functions related to unitsematics.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use crate::{
errors::KclError,
execution::{ExecState, KclValue, UnitLen},
std::Args,
};
/// Millimeters conversion factor for current projects units.
pub async fn mm(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_mm(exec_state)?;
Ok(args.make_user_val_from_f64(result))
}
/// Millimeters conversion factor for current projects units.
///
/// No matter what units the current project uses, this function will always return the conversion
/// factor to millimeters.
///
/// For example, if the current project uses inches, this function will return `(1/25.4)`.
/// If the current project uses millimeters, this function will return `1`.
///
/// **Caution**: This function is only intended to be used when you absolutely MUST
/// have different units in your code than the project settings. Otherwise, it is
/// a bad pattern to use this function.
///
/// We merely provide these functions for convenience and readability, as
/// `10 * mm()` is more readable that your intent is "I want 10 millimeters" than
/// `10 * (1/25.4)`, if the project settings are in inches.
///
/// ```no_run
/// totalWidth = 10 * mm()
/// ```
#[stdlib {
name = "mm",
tags = ["units"],
}]
fn inner_mm(exec_state: &ExecState) -> Result<f64, KclError> {
match exec_state.length_unit() {
UnitLen::Mm => Ok(1.0),
UnitLen::Inches => Ok(measurements::Length::from_millimeters(1.0).as_inches()),
UnitLen::Feet => Ok(measurements::Length::from_millimeters(1.0).as_feet()),
UnitLen::M => Ok(measurements::Length::from_millimeters(1.0).as_meters()),
UnitLen::Cm => Ok(measurements::Length::from_millimeters(1.0).as_centimeters()),
UnitLen::Yards => Ok(measurements::Length::from_millimeters(1.0).as_yards()),
}
}
/// Inches conversion factor for current projects units.
pub async fn inch(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_inch(exec_state)?;
Ok(args.make_user_val_from_f64(result))
}
/// Inches conversion factor for current projects units.
///
/// No matter what units the current project uses, this function will always return the conversion
/// factor to inches.
///
/// For example, if the current project uses inches, this function will return `1`.
/// If the current project uses millimeters, this function will return `25.4`.
///
/// **Caution**: This function is only intended to be used when you absolutely MUST
/// have different units in your code than the project settings. Otherwise, it is
/// a bad pattern to use this function.
///
/// We merely provide these functions for convenience and readability, as
/// `10 * inch()` is more readable that your intent is "I want 10 inches" than
/// `10 * 25.4`, if the project settings are in millimeters.
///
/// ```no_run
/// totalWidth = 10 * inch()
/// ```
#[stdlib {
name = "inch",
tags = ["units"],
}]
fn inner_inch(exec_state: &ExecState) -> Result<f64, KclError> {
match exec_state.length_unit() {
UnitLen::Mm => Ok(measurements::Length::from_inches(1.0).as_millimeters()),
UnitLen::Inches => Ok(1.0),
UnitLen::Feet => Ok(measurements::Length::from_inches(1.0).as_feet()),
UnitLen::M => Ok(measurements::Length::from_inches(1.0).as_meters()),
UnitLen::Cm => Ok(measurements::Length::from_inches(1.0).as_centimeters()),
UnitLen::Yards => Ok(measurements::Length::from_inches(1.0).as_yards()),
}
}
/// Feet conversion factor for current projects units.
pub async fn ft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_ft(exec_state)?;
Ok(args.make_user_val_from_f64(result))
}
/// Feet conversion factor for current projects units.
///
/// No matter what units the current project uses, this function will always return the conversion
/// factor to feet.
///
/// For example, if the current project uses inches, this function will return `12`.
/// If the current project uses millimeters, this function will return `304.8`.
/// If the current project uses feet, this function will return `1`.
///
/// **Caution**: This function is only intended to be used when you absolutely MUST
/// have different units in your code than the project settings. Otherwise, it is
/// a bad pattern to use this function.
///
/// We merely provide these functions for convenience and readability, as
/// `10 * ft()` is more readable that your intent is "I want 10 feet" than
/// `10 * 304.8`, if the project settings are in millimeters.
///
/// ```no_run
/// totalWidth = 10 * ft()
/// ```
#[stdlib {
name = "ft",
tags = ["units"],
}]
fn inner_ft(exec_state: &ExecState) -> Result<f64, KclError> {
match exec_state.length_unit() {
UnitLen::Mm => Ok(measurements::Length::from_feet(1.0).as_millimeters()),
UnitLen::Inches => Ok(measurements::Length::from_feet(1.0).as_inches()),
UnitLen::Feet => Ok(1.0),
UnitLen::M => Ok(measurements::Length::from_feet(1.0).as_meters()),
UnitLen::Cm => Ok(measurements::Length::from_feet(1.0).as_centimeters()),
UnitLen::Yards => Ok(measurements::Length::from_feet(1.0).as_yards()),
}
}
/// Meters conversion factor for current projects units.
pub async fn m(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_m(exec_state)?;
Ok(args.make_user_val_from_f64(result))
}
/// Meters conversion factor for current projects units.
///
/// No matter what units the current project uses, this function will always return the conversion
/// factor to meters.
///
/// For example, if the current project uses inches, this function will return `39.3701`.
/// If the current project uses millimeters, this function will return `1000`.
/// If the current project uses meters, this function will return `1`.
///
/// **Caution**: This function is only intended to be used when you absolutely MUST
/// have different units in your code than the project settings. Otherwise, it is
/// a bad pattern to use this function.
///
/// We merely provide these functions for convenience and readability, as
/// `10 * m()` is more readable that your intent is "I want 10 meters" than
/// `10 * 1000`, if the project settings are in millimeters.
///
/// ```no_run
/// totalWidth = 10 * m()
/// ```
#[stdlib {
name = "m",
tags = ["units"],
}]
fn inner_m(exec_state: &ExecState) -> Result<f64, KclError> {
match exec_state.length_unit() {
UnitLen::Mm => Ok(measurements::Length::from_meters(1.0).as_millimeters()),
UnitLen::Inches => Ok(measurements::Length::from_meters(1.0).as_inches()),
UnitLen::Feet => Ok(measurements::Length::from_meters(1.0).as_feet()),
UnitLen::M => Ok(1.0),
UnitLen::Cm => Ok(measurements::Length::from_meters(1.0).as_centimeters()),
UnitLen::Yards => Ok(measurements::Length::from_meters(1.0).as_yards()),
}
}
/// Centimeters conversion factor for current projects units.
pub async fn cm(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_cm(exec_state)?;
Ok(args.make_user_val_from_f64(result))
}
/// Centimeters conversion factor for current projects units.
///
/// No matter what units the current project uses, this function will always return the conversion
/// factor to centimeters.
///
/// For example, if the current project uses inches, this function will return `0.393701`.
/// If the current project uses millimeters, this function will return `10`.
/// If the current project uses centimeters, this function will return `1`.
///
/// **Caution**: This function is only intended to be used when you absolutely MUST
/// have different units in your code than the project settings. Otherwise, it is
/// a bad pattern to use this function.
///
/// We merely provide these functions for convenience and readability, as
/// `10 * cm()` is more readable that your intent is "I want 10 centimeters" than
/// `10 * 10`, if the project settings are in millimeters.
///
/// ```no_run
/// totalWidth = 10 * cm()
/// ```
#[stdlib {
name = "cm",
tags = ["units"],
}]
fn inner_cm(exec_state: &ExecState) -> Result<f64, KclError> {
match exec_state.length_unit() {
UnitLen::Mm => Ok(measurements::Length::from_centimeters(1.0).as_millimeters()),
UnitLen::Inches => Ok(measurements::Length::from_centimeters(1.0).as_inches()),
UnitLen::Feet => Ok(measurements::Length::from_centimeters(1.0).as_feet()),
UnitLen::M => Ok(measurements::Length::from_centimeters(1.0).as_meters()),
UnitLen::Cm => Ok(1.0),
UnitLen::Yards => Ok(measurements::Length::from_centimeters(1.0).as_yards()),
}
}
/// Yards conversion factor for current projects units.
pub async fn yd(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_yd(exec_state)?;
Ok(args.make_user_val_from_f64(result))
}
/// Yards conversion factor for current projects units.
///
/// No matter what units the current project uses, this function will always return the conversion
/// factor to yards.
///
/// For example, if the current project uses inches, this function will return `36`.
/// If the current project uses millimeters, this function will return `914.4`.
/// If the current project uses yards, this function will return `1`.
///
/// **Caution**: This function is only intended to be used when you absolutely MUST
/// have different units in your code than the project settings. Otherwise, it is
/// a bad pattern to use this function.
///
/// We merely provide these functions for convenience and readability, as
/// `10 * yd()` is more readable that your intent is "I want 10 yards" than
/// `10 * 914.4`, if the project settings are in millimeters.
///
/// ```no_run
/// totalWidth = 10 * yd()
/// ```
#[stdlib {
name = "yd",
tags = ["units"],
}]
fn inner_yd(exec_state: &ExecState) -> Result<f64, KclError> {
match exec_state.length_unit() {
UnitLen::Mm => Ok(measurements::Length::from_yards(1.0).as_millimeters()),
UnitLen::Inches => Ok(measurements::Length::from_yards(1.0).as_inches()),
UnitLen::Feet => Ok(measurements::Length::from_yards(1.0).as_feet()),
UnitLen::M => Ok(measurements::Length::from_yards(1.0).as_meters()),
UnitLen::Cm => Ok(measurements::Length::from_yards(1.0).as_centimeters()),
UnitLen::Yards => Ok(1.0),
}
}

View File

@ -0,0 +1,872 @@
use std::{collections::HashSet, f64::consts::PI};
use kittycad_modeling_cmds::shared::Angle;
use crate::{
errors::{KclError, KclErrorDetails},
execution::Point2d,
source_range::SourceRange,
};
/// Count the number of unique items in a `Vec` in O(n) time.
pub(crate) fn unique_count<T: Eq + std::hash::Hash>(vec: Vec<T>) -> usize {
// Add to a set.
let mut set = HashSet::with_capacity(vec.len());
for item in vec {
set.insert(item);
}
set.len()
}
/// 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;
let y = b.y - a.y;
normalize(Angle::from_radians(y.atan2(x)))
}
/// Normalize the angle
pub fn normalize(angle: Angle) -> Angle {
let deg = angle.to_degrees();
let result = ((deg % 360.0) + 360.0) % 360.0;
Angle::from_degrees(if result > 180.0 { result - 360.0 } else { result })
}
/// Gives the ▲-angle between from and to angles (shortest path)
///
/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
/// # Examples
///
/// ```
/// use std::f64::consts::PI;
///
/// use kcl_lib::std::utils::Angle;
///
/// assert_eq!(
/// Angle::delta(Angle::from_radians(PI / 8.0), Angle::from_radians(PI / 4.0)),
/// Angle::from_radians(PI / 8.0)
/// );
/// ```
pub fn delta(from_angle: Angle, to_angle: Angle) -> Angle {
let norm_from_angle = normalize_rad(from_angle.to_radians());
let norm_to_angle = normalize_rad(to_angle.to_radians());
let provisional = norm_to_angle - norm_from_angle;
if provisional > -PI && provisional <= PI {
return Angle::from_radians(provisional);
}
if provisional > PI {
return Angle::from_radians(provisional - 2.0 * PI);
}
if provisional < -PI {
return Angle::from_radians(provisional + 2.0 * PI);
}
Angle::default()
}
pub fn normalize_rad(angle: f64) -> f64 {
let draft = angle % (2.0 * PI);
if draft < 0.0 {
draft + 2.0 * PI
} else {
draft
}
}
pub fn calculate_intersection_of_two_lines(line1: &[Point2d; 2], line2_angle: f64, line2_point: Point2d) -> Point2d {
let line2_point_b = Point2d {
x: line2_point.x + f64::cos(line2_angle.to_radians()) * 10.0,
y: line2_point.y + f64::sin(line2_angle.to_radians()) * 10.0,
};
intersect(line1[0], line1[1], line2_point, line2_point_b)
}
pub fn intersect(p1: Point2d, p2: Point2d, p3: Point2d, p4: Point2d) -> Point2d {
let slope = |p1: Point2d, p2: Point2d| (p1.y - p2.y) / (p1.x - p2.x);
let constant = |p1: Point2d, p2: Point2d| p1.y - slope(p1, p2) * p1.x;
let get_y = |for_x: f64, p1: Point2d, p2: Point2d| slope(p1, p2) * for_x + constant(p1, p2);
if p1.x == p2.x {
return Point2d {
x: p1.x,
y: get_y(p1.x, p3, p4),
};
}
if p3.x == p4.x {
return Point2d {
x: p3.x,
y: get_y(p3.x, p1, p2),
};
}
let x = (constant(p3, p4) - constant(p1, p2)) / (slope(p1, p2) - slope(p3, p4));
let y = get_y(x, p1, p2);
Point2d { x, y }
}
pub fn intersection_with_parallel_line(
line1: &[Point2d; 2],
line1_offset: f64,
line2_angle: f64,
line2_point: Point2d,
) -> Point2d {
calculate_intersection_of_two_lines(&offset_line(line1_offset, line1[0], line1[1]), line2_angle, line2_point)
}
fn offset_line(offset: f64, p1: Point2d, p2: Point2d) -> [Point2d; 2] {
if p1.x == p2.x {
let direction = (p1.y - p2.y).signum();
return [
Point2d {
x: p1.x + offset * direction,
y: p1.y,
},
Point2d {
x: p2.x + offset * direction,
y: p2.y,
},
];
}
if p1.y == p2.y {
let direction = (p2.x - p1.x).signum();
return [
Point2d {
x: p1.x,
y: p1.y + offset * direction,
},
Point2d {
x: p2.x,
y: p2.y + offset * direction,
},
];
}
let x_offset = offset / f64::sin(f64::atan2(p1.y - p2.y, p1.x - p2.x));
[
Point2d {
x: p1.x + x_offset,
y: p1.y,
},
Point2d {
x: p2.x + x_offset,
y: p2.y,
},
]
}
pub fn get_y_component(angle: Angle, x: f64) -> Point2d {
let normalised_angle = ((angle.to_degrees() % 360.0) + 360.0) % 360.0; // between 0 and 360
let y = x * f64::tan(normalised_angle.to_radians());
let sign = if normalised_angle > 90.0 && normalised_angle <= 270.0 {
-1.0
} else {
1.0
};
Point2d { x, y }.scale(sign)
}
pub fn get_x_component(angle: Angle, y: f64) -> Point2d {
let normalised_angle = ((angle.to_degrees() % 360.0) + 360.0) % 360.0; // between 0 and 360
let x = y / f64::tan(normalised_angle.to_radians());
let sign = if normalised_angle > 180.0 && normalised_angle <= 360.0 {
-1.0
} else {
1.0
};
Point2d { x, y }.scale(sign)
}
pub fn arc_center_and_end(from: Point2d, start_angle: Angle, end_angle: Angle, radius: f64) -> (Point2d, Point2d) {
let start_angle = start_angle.to_radians();
let end_angle = end_angle.to_radians();
let center = Point2d {
x: -1.0 * (radius * start_angle.cos() - from.x),
y: -1.0 * (radius * start_angle.sin() - from.y),
};
let end = Point2d {
x: center.x + radius * end_angle.cos(),
y: center.y + radius * end_angle.sin(),
};
(center, end)
}
pub fn arc_angles(
from: Point2d,
to: Point2d,
center: Point2d,
radius: f64,
source_range: SourceRange,
) -> Result<(Angle, Angle), KclError> {
// First make sure that the points are on the circumference of the circle.
// If not, we'll return an error.
if !is_on_circumference(center, from, radius) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Point {:?} is not on the circumference of the circle with center {:?} and radius {}.",
from, center, radius
),
source_ranges: vec![source_range],
}));
}
if !is_on_circumference(center, to, radius) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Point {:?} is not on the circumference of the circle with center {:?} and radius {}.",
to, center, radius
),
source_ranges: vec![source_range],
}));
}
let start_angle = (from.y - center.y).atan2(from.x - center.x);
let end_angle = (to.y - center.y).atan2(to.x - center.x);
Ok((Angle::from_radians(start_angle), Angle::from_radians(end_angle)))
}
pub fn is_on_circumference(center: Point2d, point: Point2d, radius: f64) -> bool {
let dx = point.x - center.x;
let dy = point.y - center.y;
let distance_squared = dx.powi(2) + dy.powi(2);
// We'll check if the distance squared is approximately equal to radius squared.
// Due to potential floating point inaccuracies, we'll check if the difference
// is very small (e.g., 1e-9) rather than checking for strict equality.
(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]
}
pub struct CircleParams {
pub center: Point2d,
pub radius: f64,
}
pub fn calculate_circle_from_3_points(points: [Point2d; 3]) -> CircleParams {
let center: Point2d = calculate_circle_center(points[0].into(), points[1].into(), points[2].into()).into();
CircleParams {
center,
radius: distance(center, points[1]),
}
}
#[cfg(test)]
mod tests {
// Here you can bring your functions into scope
use pretty_assertions::assert_eq;
use super::{get_x_component, get_y_component, Angle};
use crate::SourceRange;
static EACH_QUAD: [(i32, [i32; 2]); 12] = [
(-315, [1, 1]),
(-225, [-1, 1]),
(-135, [-1, -1]),
(-45, [1, -1]),
(45, [1, 1]),
(135, [-1, 1]),
(225, [-1, -1]),
(315, [1, -1]),
(405, [1, 1]),
(495, [-1, 1]),
(585, [-1, -1]),
(675, [1, -1]),
];
#[test]
fn test_get_y_component() {
let mut expected = Vec::new();
let mut results = Vec::new();
for &(angle, expected_result) in EACH_QUAD.iter() {
let res = get_y_component(Angle::from_degrees(angle as f64), 1.0);
results.push([res.x.round() as i32, res.y.round() as i32]);
expected.push(expected_result);
}
assert_eq!(results, expected);
let result = get_y_component(Angle::zero(), 1.0);
assert_eq!(result.x as i32, 1);
assert_eq!(result.y as i32, 0);
let result = get_y_component(Angle::from_degrees(90.0), 1.0);
assert_eq!(result.x as i32, 1);
assert!(result.y > 100000.0);
let result = get_y_component(Angle::from_degrees(180.0), 1.0);
assert_eq!(result.x as i32, -1);
assert!((result.y - 0.0).abs() < f64::EPSILON);
let result = get_y_component(Angle::from_degrees(270.0), 1.0);
assert_eq!(result.x as i32, -1);
assert!(result.y < -100000.0);
}
#[test]
fn test_get_x_component() {
let mut expected = Vec::new();
let mut results = Vec::new();
for &(angle, expected_result) in EACH_QUAD.iter() {
let res = get_x_component(Angle::from_degrees(angle as f64), 1.0);
results.push([res.x.round() as i32, res.y.round() as i32]);
expected.push(expected_result);
}
assert_eq!(results, expected);
let result = get_x_component(Angle::zero(), 1.0);
assert!(result.x > 100000.0);
assert_eq!(result.y as i32, 1);
let result = get_x_component(Angle::from_degrees(90.0), 1.0);
assert!((result.x - 0.0).abs() < f64::EPSILON);
assert_eq!(result.y as i32, 1);
let result = get_x_component(Angle::from_degrees(180.0), 1.0);
assert!(result.x < -100000.0);
assert_eq!(result.y as i32, 1);
let result = get_x_component(Angle::from_degrees(270.0), 1.0);
assert!((result.x - 0.0).abs() < f64::EPSILON);
assert_eq!(result.y as i32, -1);
}
#[test]
fn test_arc_center_and_end() {
let (center, end) = super::arc_center_and_end(
super::Point2d { x: 0.0, y: 0.0 },
Angle::zero(),
Angle::from_degrees(90.0),
1.0,
);
assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -1.0);
assert_eq!(end.y, 1.0);
let (center, end) = super::arc_center_and_end(
super::Point2d { x: 0.0, y: 0.0 },
Angle::zero(),
Angle::from_degrees(180.0),
1.0,
);
assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -2.0);
assert_eq!(end.y.round(), 0.0);
let (center, end) = super::arc_center_and_end(
super::Point2d { x: 0.0, y: 0.0 },
Angle::zero(),
Angle::from_degrees(180.0),
10.0,
);
assert_eq!(center.x.round(), -10.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -20.0);
assert_eq!(end.y.round(), 0.0);
}
#[test]
fn test_arc_angles() {
let (angle_start, angle_end) = super::arc_angles(
super::Point2d { x: 0.0, y: 0.0 },
super::Point2d { x: -1.0, y: 1.0 },
super::Point2d { x: -1.0, y: 0.0 },
1.0,
SourceRange::default(),
)
.unwrap();
assert_eq!(angle_start.to_degrees().round(), 0.0);
assert_eq!(angle_end.to_degrees().round(), 90.0);
let (angle_start, angle_end) = super::arc_angles(
super::Point2d { x: 0.0, y: 0.0 },
super::Point2d { x: -2.0, y: 0.0 },
super::Point2d { x: -1.0, y: 0.0 },
1.0,
SourceRange::default(),
)
.unwrap();
assert_eq!(angle_start.to_degrees().round(), 0.0);
assert_eq!(angle_end.to_degrees().round(), 180.0);
let (angle_start, angle_end) = super::arc_angles(
super::Point2d { x: 0.0, y: 0.0 },
super::Point2d { x: -20.0, y: 0.0 },
super::Point2d { x: -10.0, y: 0.0 },
10.0,
SourceRange::default(),
)
.unwrap();
assert_eq!(angle_start.to_degrees().round(), 0.0);
assert_eq!(angle_end.to_degrees().round(), 180.0);
let result = super::arc_angles(
super::Point2d { x: 0.0, y: 5.0 },
super::Point2d { x: 5.0, y: 5.0 },
super::Point2d { x: 10.0, y: -10.0 },
10.0,
SourceRange::default(),
);
if let Err(err) = result {
assert!(err.to_string().contains("Point Point2d { x: 0.0, y: 5.0 } is not on the circumference of the circle with center Point2d { x: 10.0, y: -10.0 } and radius 10."));
} else {
panic!("Expected error");
}
assert_eq!(angle_start.to_degrees().round(), 0.0);
assert_eq!(angle_end.to_degrees().round(), 180.0);
}
}
pub type Coords2d = [f64; 2];
pub fn is_points_ccw_wasm(points: &[f64]) -> i32 {
// CCW is positive as that the Math convention
let mut sum = 0.0;
for i in 0..(points.len() / 2) {
let point1 = [points[2 * i], points[2 * i + 1]];
let point2 = [points[(2 * i + 2) % points.len()], points[(2 * i + 3) % points.len()]];
sum += (point2[0] + point1[0]) * (point2[1] - point1[1]);
}
sum.signum() as i32
}
pub fn is_points_ccw(points: &[Coords2d]) -> i32 {
let flattened_points: Vec<f64> = points.iter().flat_map(|&p| vec![p[0], p[1]]).collect();
is_points_ccw_wasm(&flattened_points)
}
fn get_slope(start: Coords2d, end: Coords2d) -> (f64, f64) {
let slope = if start[0] - end[0] == 0.0 {
f64::INFINITY
} else {
(start[1] - end[1]) / (start[0] - end[0])
};
let perp_slope = if slope == f64::INFINITY { 0.0 } else { -1.0 / slope };
(slope, perp_slope)
}
fn get_angle(point1: Coords2d, point2: Coords2d) -> f64 {
let delta_x = point2[0] - point1[0];
let delta_y = point2[1] - point1[1];
let angle = delta_y.atan2(delta_x);
let result = if angle < 0.0 { angle + 2.0 * PI } else { angle };
result * (180.0 / PI)
}
fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
let norm_from_angle = normalize_rad(from_angle);
let norm_to_angle = normalize_rad(to_angle);
let provisional = norm_to_angle - norm_from_angle;
if provisional > -PI && provisional <= PI {
provisional
} else if provisional > PI {
provisional - 2.0 * PI
} else if provisional < -PI {
provisional + 2.0 * PI
} else {
provisional
}
}
fn deg2rad(deg: f64) -> f64 {
deg * (PI / 180.0)
}
fn get_mid_point(
center: Coords2d,
arc_start_point: Coords2d,
arc_end_point: Coords2d,
tan_previous_point: Coords2d,
radius: f64,
obtuse: bool,
) -> Coords2d {
let angle_from_center_to_arc_start = get_angle(center, arc_start_point);
let angle_from_center_to_arc_end = get_angle(center, arc_end_point);
let delta_ang = delta_angle(
deg2rad(angle_from_center_to_arc_start),
deg2rad(angle_from_center_to_arc_end),
);
let delta_ang = delta_ang / 2.0 + deg2rad(angle_from_center_to_arc_start);
let shortest_arc_mid_point: Coords2d = [
delta_ang.cos() * radius + center[0],
delta_ang.sin() * radius + center[1],
];
let opposite_delta = delta_ang + PI;
let longest_arc_mid_point: Coords2d = [
opposite_delta.cos() * radius + center[0],
opposite_delta.sin() * radius + center[1],
];
let rotation_direction_original_points = is_points_ccw(&[tan_previous_point, arc_start_point, arc_end_point]);
let rotation_direction_points_on_arc = is_points_ccw(&[arc_start_point, shortest_arc_mid_point, arc_end_point]);
if rotation_direction_original_points != rotation_direction_points_on_arc && obtuse {
longest_arc_mid_point
} else {
shortest_arc_mid_point
}
}
fn intersect_point_n_slope(point1: Coords2d, slope1: f64, point2: Coords2d, slope2: f64) -> Coords2d {
let x = if slope1.abs() == f64::INFINITY {
point1[0]
} else if slope2.abs() == f64::INFINITY {
point2[0]
} else {
(point2[1] - slope2 * point2[0] - point1[1] + slope1 * point1[0]) / (slope1 - slope2)
};
let y = if slope1.abs() != f64::INFINITY {
slope1 * x - slope1 * point1[0] + point1[1]
} else {
slope2 * x - slope2 * point2[0] + point2[1]
};
[x, y]
}
/// Structure to hold input data for calculating tangential arc information.
pub struct TangentialArcInfoInput {
/// The starting point of the arc.
pub arc_start_point: Coords2d,
/// The ending point of the arc.
pub arc_end_point: Coords2d,
/// The point from which the tangent is drawn.
pub tan_previous_point: Coords2d,
/// Flag to determine if the arc is obtuse. Obtuse means it flows smoothly from the previous segment.
pub obtuse: bool,
}
/// Structure to hold the output data from calculating tangential arc information.
#[allow(dead_code)]
pub struct TangentialArcInfoOutput {
/// The center point of the arc.
pub center: Coords2d,
/// The midpoint on the arc.
pub arc_mid_point: Coords2d,
/// The radius of the arc.
pub radius: f64,
/// Start angle of the arc in radians.
pub start_angle: f64,
/// End angle of the arc in radians.
pub end_angle: f64,
/// If the arc is counter-clockwise.
pub ccw: i32,
/// The length of the arc.
pub arc_length: f64,
}
// tanPreviousPoint and arcStartPoint make up a straight segment leading into the arc (of which the arc should be tangential). The arc should start at arcStartPoint and end at, arcEndPoint
// With this information we should everything we need to calculate the arc's center and radius. However there is two tangential arcs possible, that just varies on their direction
// One is obtuse where the arc smoothly flows from the straight segment, and the other would be acute that immediately cuts back in the other direction. The obtuse boolean is there to control for this.
pub fn get_tangential_arc_to_info(input: TangentialArcInfoInput) -> TangentialArcInfoOutput {
let (_, perp_slope) = get_slope(input.tan_previous_point, input.arc_start_point);
let tangential_line_perp_slope = perp_slope;
// Calculate the midpoint of the line segment between arcStartPoint and arcEndPoint
let mid_point: Coords2d = [
(input.arc_start_point[0] + input.arc_end_point[0]) / 2.0,
(input.arc_start_point[1] + input.arc_end_point[1]) / 2.0,
];
let slope_mid_point_line = get_slope(input.arc_start_point, mid_point);
let center: Coords2d;
let radius: f64;
if tangential_line_perp_slope == slope_mid_point_line.0 {
// can't find the intersection of the two lines if they have the same gradient
// but in this case the center is the midpoint anyway
center = mid_point;
radius =
((input.arc_start_point[0] - center[0]).powi(2) + (input.arc_start_point[1] - center[1]).powi(2)).sqrt();
} else {
center = intersect_point_n_slope(
mid_point,
slope_mid_point_line.1,
input.arc_start_point,
tangential_line_perp_slope,
);
radius =
((input.arc_start_point[0] - center[0]).powi(2) + (input.arc_start_point[1] - center[1]).powi(2)).sqrt();
}
let arc_mid_point = get_mid_point(
center,
input.arc_start_point,
input.arc_end_point,
input.tan_previous_point,
radius,
input.obtuse,
);
let start_angle = (input.arc_start_point[1] - center[1]).atan2(input.arc_start_point[0] - center[0]);
let end_angle = (input.arc_end_point[1] - center[1]).atan2(input.arc_end_point[0] - center[0]);
let ccw = is_points_ccw(&[input.arc_start_point, arc_mid_point, input.arc_end_point]);
let arc_mid_angle = (arc_mid_point[1] - center[1]).atan2(arc_mid_point[0] - center[0]);
let start_to_mid_arc_length = radius
* delta(Angle::from_radians(start_angle), Angle::from_radians(arc_mid_angle))
.to_radians()
.abs();
let mid_to_end_arc_length = radius
* delta(Angle::from_radians(arc_mid_angle), Angle::from_radians(end_angle))
.to_radians()
.abs();
let arc_length = start_to_mid_arc_length + mid_to_end_arc_length;
TangentialArcInfoOutput {
center,
radius,
arc_mid_point,
start_angle,
end_angle,
ccw,
arc_length,
}
}
#[cfg(test)]
mod get_tangential_arc_to_info_tests {
use approx::assert_relative_eq;
use super::*;
fn round_to_three_decimals(num: f64) -> f64 {
(num * 1000.0).round() / 1000.0
}
#[test]
fn test_unique_count() {
assert_eq!(unique_count(vec![1, 2, 2, 3, 2]), 3);
}
#[test]
fn test_basic_case() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [0.0, -5.0],
arc_start_point: [0.0, 0.0],
arc_end_point: [4.0, 0.0],
obtuse: true,
});
assert_relative_eq!(result.center[0], 2.0);
assert_relative_eq!(result.center[1], 0.0);
assert_relative_eq!(result.arc_mid_point[0], 2.0);
assert_relative_eq!(result.arc_mid_point[1], 2.0);
assert_relative_eq!(result.radius, 2.0);
assert_relative_eq!(result.start_angle, PI);
assert_relative_eq!(result.end_angle, 0.0);
assert_eq!(result.ccw, -1);
}
#[test]
fn basic_case_with_arc_centered_at_0_0_and_the_tangential_line_being_45_degrees() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [0.0, -4.0],
arc_start_point: [2.0, -2.0],
arc_end_point: [-2.0, 2.0],
obtuse: true,
});
assert_relative_eq!(result.center[0], 0.0);
assert_relative_eq!(result.center[1], 0.0);
assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[0]), 2.0);
assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[1]), 2.0);
assert_relative_eq!(result.radius, (2.0f64 * 2.0 + 2.0 * 2.0).sqrt());
assert_relative_eq!(result.start_angle, -PI / 4.0);
assert_relative_eq!(result.end_angle, 3.0 * PI / 4.0);
assert_eq!(result.ccw, 1);
}
#[test]
fn test_get_tangential_arc_to_info_moving_arc_end_point() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [0.0, -4.0],
arc_start_point: [2.0, -2.0],
arc_end_point: [2.0, 2.0],
obtuse: true,
});
let expected_radius = (2.0f64 * 2.0 + 2.0 * 2.0).sqrt();
assert_relative_eq!(round_to_three_decimals(result.center[0]), 0.0);
assert_relative_eq!(result.center[1], 0.0);
assert_relative_eq!(result.arc_mid_point[0], expected_radius);
assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[1]), -0.0);
assert_relative_eq!(result.radius, expected_radius);
assert_relative_eq!(result.start_angle, -PI / 4.0);
assert_relative_eq!(result.end_angle, PI / 4.0);
assert_eq!(result.ccw, 1);
}
#[test]
fn test_get_tangential_arc_to_info_moving_arc_end_point_again() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [0.0, -4.0],
arc_start_point: [2.0, -2.0],
arc_end_point: [-2.0, -2.0],
obtuse: true,
});
let expected_radius = (2.0f64 * 2.0 + 2.0 * 2.0).sqrt();
assert_relative_eq!(result.center[0], 0.0);
assert_relative_eq!(result.center[1], 0.0);
assert_relative_eq!(result.radius, expected_radius);
assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[0]), 0.0);
assert_relative_eq!(result.arc_mid_point[1], expected_radius);
assert_relative_eq!(result.start_angle, -PI / 4.0);
assert_relative_eq!(result.end_angle, -3.0 * PI / 4.0);
assert_eq!(result.ccw, 1);
}
#[test]
fn test_get_tangential_arc_to_info_acute_moving_arc_end_point() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [0.0, -4.0],
arc_start_point: [2.0, -2.0],
arc_end_point: [-2.0, -2.0],
obtuse: false,
});
let expected_radius = (2.0f64 * 2.0 + 2.0 * 2.0).sqrt();
assert_relative_eq!(result.center[0], 0.0);
assert_relative_eq!(result.center[1], 0.0);
assert_relative_eq!(result.radius, expected_radius);
assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[0]), -0.0);
assert_relative_eq!(result.arc_mid_point[1], -expected_radius);
assert_relative_eq!(result.start_angle, -PI / 4.0);
assert_relative_eq!(result.end_angle, -3.0 * PI / 4.0);
// would be cw if it was obtuse
assert_eq!(result.ccw, -1);
}
#[test]
fn test_get_tangential_arc_to_info_obtuse_with_wrap_around() {
let arc_end = (std::f64::consts::PI / 4.0).cos() * 2.0;
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [2.0, -4.0],
arc_start_point: [2.0, 0.0],
arc_end_point: [0.0, -2.0],
obtuse: true,
});
assert_relative_eq!(result.center[0], -0.0);
assert_relative_eq!(result.center[1], 0.0);
assert_relative_eq!(result.radius, 2.0);
assert_relative_eq!(result.arc_mid_point[0], -arc_end);
assert_relative_eq!(result.arc_mid_point[1], arc_end);
assert_relative_eq!(result.start_angle, 0.0);
assert_relative_eq!(result.end_angle, -PI / 2.0);
assert_eq!(result.ccw, 1);
}
#[test]
fn test_arc_length_obtuse_cw() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [-1.0, -1.0],
arc_start_point: [-1.0, 0.0],
arc_end_point: [0.0, -1.0],
obtuse: true,
});
let circumference = 2.0 * PI * result.radius;
let expected_length = circumference * 3.0 / 4.0; // 3 quarters of a circle circle
assert_relative_eq!(result.arc_length, expected_length);
}
#[test]
fn test_arc_length_acute_cw() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [-1.0, -1.0],
arc_start_point: [-1.0, 0.0],
arc_end_point: [0.0, 1.0],
obtuse: true,
});
let circumference = 2.0 * PI * result.radius;
let expected_length = circumference / 4.0; // 1 quarters of a circle circle
assert_relative_eq!(result.arc_length, expected_length);
}
#[test]
fn test_arc_length_obtuse_ccw() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [1.0, -1.0],
arc_start_point: [1.0, 0.0],
arc_end_point: [0.0, -1.0],
obtuse: true,
});
let circumference = 2.0 * PI * result.radius;
let expected_length = circumference * 3.0 / 4.0; // 1 quarters of a circle circle
assert_relative_eq!(result.arc_length, expected_length);
}
#[test]
fn test_arc_length_acute_ccw() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
tan_previous_point: [1.0, -1.0],
arc_start_point: [1.0, 0.0],
arc_end_point: [0.0, 1.0],
obtuse: true,
});
let circumference = 2.0 * PI * result.radius;
let expected_length = circumference / 4.0; // 1 quarters of a circle circle
assert_relative_eq!(result.arc_length, expected_length);
}
}
pub fn get_tangent_point_from_previous_arc(
last_arc_center: Coords2d,
last_arc_ccw: bool,
last_arc_end: Coords2d,
) -> Coords2d {
let angle_from_old_center_to_arc_start = get_angle(last_arc_center, last_arc_end);
let tangential_angle = angle_from_old_center_to_arc_start + if last_arc_ccw { -90.0 } else { 90.0 };
// What is the 10.0 constant doing???
[
tangential_angle.to_radians().cos() * 10.0 + last_arc_end[0],
tangential_angle.to_radians().sin() * 10.0 + last_arc_end[1],
]
}