use anyhow::Result;
use kcmc::ModelingCmd;
use kcmc::each_cmd as mcmd;
use kcmc::length_unit::LengthUnit;
use kcmc::shared::Angle;
use kcmc::shared::Point2d as KPoint2d;
use kittycad_modeling_cmds::shared::PathSegment;
use kittycad_modeling_cmds::units::UnitLength;
use kittycad_modeling_cmds::{self as kcmc};
use serde::Serialize;
use super::args::TyF64;
use super::utils::point_to_len_unit;
use super::utils::point_to_mm;
use super::utils::point_to_typed;
use super::utils::untype_point;
use super::utils::untyped_point_to_mm;
use crate::SourceRange;
use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::execution::BasePath;
use crate::execution::ExecState;
use crate::execution::GeoMeta;
use crate::execution::KclValue;
use crate::execution::ModelingCmdMeta;
use crate::execution::Path;
use crate::execution::ProfileClosed;
use crate::execution::Sketch;
use crate::execution::SketchSurface;
use crate::execution::types::RuntimeType;
use crate::execution::types::adjust_length;
use crate::parsing::ast::types::TagNode;
use crate::std::Args;
use crate::std::utils::calculate_circle_center;
use crate::std::utils::distance;
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(untagged)]
pub enum SketchOrSurface {
SketchSurface(SketchSurface),
Sketch(Box<Sketch>),
}
impl SketchOrSurface {
pub fn into_sketch_surface(self) -> SketchSurface {
match self {
SketchOrSurface::SketchSurface(surface) => surface,
SketchOrSurface::Sketch(sketch) => sketch.on,
}
}
}
pub async fn rectangle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_or_surface =
args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
let corner = args.get_kw_arg_opt("corner", &RuntimeType::point2d(), exec_state)?;
let width: TyF64 = args.get_kw_arg("width", &RuntimeType::length(), exec_state)?;
let height: TyF64 = args.get_kw_arg("height", &RuntimeType::length(), exec_state)?;
inner_rectangle(sketch_or_surface, center, corner, width, height, exec_state, args)
.await
.map(Box::new)
.map(|value| KclValue::Sketch { value })
}
async fn inner_rectangle(
sketch_or_surface: SketchOrSurface,
center: Option<[TyF64; 2]>,
corner: Option<[TyF64; 2]>,
width: TyF64,
height: TyF64,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
let sketch_surface = sketch_or_surface.into_sketch_surface();
let (ty, corner) = match (center, corner) {
(Some(center), None) => (
center[0].ty,
[center[0].n - width.n / 2.0, center[1].n - height.n / 2.0],
),
(None, Some(corner)) => (corner[0].ty, [corner[0].n, corner[1].n]),
(None, None) => {
return Err(KclError::new_semantic(KclErrorDetails::new(
"You must supply either `corner` or `center` arguments, but not both".to_string(),
vec![args.source_range],
)));
}
(Some(_), Some(_)) => {
return Err(KclError::new_semantic(KclErrorDetails::new(
"You must supply either `corner` or `center` arguments, but not both".to_string(),
vec![args.source_range],
)));
}
};
let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
let corner_t = [TyF64::new(corner[0], ty), TyF64::new(corner[1], ty)];
let sketch = crate::std::sketch::inner_start_profile(
sketch_surface,
corner_t,
None,
exec_state,
&args.ctx,
args.source_range,
)
.await?;
let sketch_id = sketch.id;
let deltas = [[width.n, 0.0], [0.0, height.n], [-width.n, 0.0], [0.0, -height.n]];
let ids = [
exec_state.next_uuid(),
exec_state.next_uuid(),
exec_state.next_uuid(),
exec_state.next_uuid(),
];
for (id, delta) in ids.iter().copied().zip(deltas) {
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, id),
ModelingCmd::from(
mcmd::ExtendPath::builder()
.path(sketch.id.into())
.segment(PathSegment::Line {
end: KPoint2d::from(untyped_point_to_mm(delta, units))
.with_z(0.0)
.map(LengthUnit),
relative: true,
})
.build(),
),
)
.await?;
}
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, sketch_id),
ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
)
.await?;
let mut new_sketch = sketch;
new_sketch.is_closed = ProfileClosed::Explicitly;
fn add(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
[a[0] + b[0], a[1] + b[1]]
}
let a = (corner, add(corner, deltas[0]));
let b = (a.1, add(a.1, deltas[1]));
let c = (b.1, add(b.1, deltas[2]));
let d = (c.1, add(c.1, deltas[3]));
for (id, (from, to)) in ids.into_iter().zip([a, b, c, d]) {
let current_path = Path::ToPoint {
base: BasePath {
from,
to,
tag: None,
units,
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
new_sketch.paths.push(current_path);
}
Ok(new_sketch)
}
pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_or_surface =
args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
value: Box::new(sketch),
})
}
pub const POINT_ZERO_ZERO: [TyF64; 2] = [
TyF64::new(0.0, crate::exec::NumericType::mm()),
TyF64::new(0.0, crate::exec::NumericType::mm()),
];
pub(super) async fn inner_circle(
sketch_or_surface: SketchOrSurface,
center: Option<[TyF64; 2]>,
radius: Option<TyF64>,
diameter: Option<TyF64>,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
let sketch_surface = sketch_or_surface.into_sketch_surface();
let center = center.unwrap_or(POINT_ZERO_ZERO);
let (center_u, ty) = untype_point(center.clone());
let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
let radius = get_radius(radius, diameter, args.source_range)?;
let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
let sketch =
crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
.await?;
let angle_start = Angle::zero();
let angle_end = Angle::turn();
let id = exec_state.next_uuid();
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, id),
ModelingCmd::from(
mcmd::ExtendPath::builder()
.path(sketch.id.into())
.segment(PathSegment::Arc {
start: angle_start,
end: angle_end,
center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
radius: LengthUnit(radius.to_mm()),
relative: false,
})
.build(),
),
)
.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: radius.to_length_units(units),
center: center_u,
ccw: angle_start < angle_end,
};
let mut new_sketch = sketch;
new_sketch.is_closed = ProfileClosed::Explicitly;
if let Some(tag) = &tag {
new_sketch.add_tag(tag, ¤t_path, exec_state, None);
}
new_sketch.paths.push(current_path);
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, id),
ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
)
.await?;
Ok(new_sketch)
}
pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_or_surface =
args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
let p1 = args.get_kw_arg("p1", &RuntimeType::point2d(), exec_state)?;
let p2 = args.get_kw_arg("p2", &RuntimeType::point2d(), exec_state)?;
let p3 = args.get_kw_arg("p3", &RuntimeType::point2d(), exec_state)?;
let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
let sketch = inner_circle_three_point(sketch_or_surface, p1, p2, p3, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
value: Box::new(sketch),
})
}
async fn inner_circle_three_point(
sketch_surface_or_group: SketchOrSurface,
p1: [TyF64; 2],
p2: [TyF64; 2],
p3: [TyF64; 2],
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
let ty = p1[0].ty;
let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
let p1 = point_to_len_unit(p1, units);
let p2 = point_to_len_unit(p2, units);
let p3 = point_to_len_unit(p3, units);
let center = calculate_circle_center(p1, p2, p3);
let radius = distance(center, p2);
let sketch_surface = sketch_surface_or_group.into_sketch_surface();
let from = [TyF64::new(center[0] + radius, ty), TyF64::new(center[1], ty)];
let sketch = crate::std::sketch::inner_start_profile(
sketch_surface,
from.clone(),
None,
exec_state,
&args.ctx,
args.source_range,
)
.await?;
let angle_start = Angle::zero();
let angle_end = Angle::turn();
let id = exec_state.next_uuid();
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, id),
ModelingCmd::from(
mcmd::ExtendPath::builder()
.path(sketch.id.into())
.segment(PathSegment::Arc {
start: angle_start,
end: angle_end,
center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
radius: adjust_length(units, radius, UnitLength::Millimeters).0.into(),
relative: false,
})
.build(),
),
)
.await?;
let current_path = Path::CircleThreePoint {
base: BasePath {
from: untype_point(from.clone()).0,
to: untype_point(from).0,
tag: tag.clone(),
units,
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
p1,
p2,
p3,
};
let mut new_sketch = sketch;
new_sketch.is_closed = ProfileClosed::Explicitly;
if let Some(tag) = &tag {
new_sketch.add_tag(tag, ¤t_path, exec_state, None);
}
new_sketch.paths.push(current_path);
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, id),
ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
)
.await?;
Ok(new_sketch)
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, Default)]
#[ts(export)]
#[serde(rename_all = "lowercase")]
pub enum PolygonType {
#[default]
Inscribed,
Circumscribed,
}
pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_or_surface =
args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
let radius: TyF64 = args.get_kw_arg("radius", &RuntimeType::length(), exec_state)?;
let num_sides: TyF64 = args.get_kw_arg("numSides", &RuntimeType::count(), exec_state)?;
let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
let inscribed = args.get_kw_arg_opt("inscribed", &RuntimeType::bool(), exec_state)?;
let sketch = inner_polygon(
sketch_or_surface,
radius,
num_sides.n as u64,
center,
inscribed,
exec_state,
args,
)
.await?;
Ok(KclValue::Sketch {
value: Box::new(sketch),
})
}
#[allow(clippy::too_many_arguments)]
async fn inner_polygon(
sketch_surface_or_group: SketchOrSurface,
radius: TyF64,
num_sides: u64,
center: Option<[TyF64; 2]>,
inscribed: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
let center = center.unwrap_or(POINT_ZERO_ZERO);
if num_sides < 3 {
return Err(KclError::new_type(KclErrorDetails::new(
"Polygon must have at least 3 sides".to_string(),
vec![args.source_range],
)));
}
if radius.n <= 0.0 {
return Err(KclError::new_type(KclErrorDetails::new(
"Radius must be greater than 0".to_string(),
vec![args.source_range],
)));
}
let (sketch_surface, units) = match sketch_surface_or_group {
SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.as_length().unwrap_or(UnitLength::Millimeters)),
SketchOrSurface::Sketch(group) => (group.on, group.units),
};
let half_angle = std::f64::consts::PI / num_sides as f64;
let radius_to_vertices = if inscribed.unwrap_or(true) {
radius.n
} else {
radius.n / libm::cos(half_angle)
};
let angle_step = std::f64::consts::TAU / num_sides as f64;
let center_u = point_to_len_unit(center, units);
let vertices: Vec<[f64; 2]> = (0..num_sides)
.map(|i| {
let angle = angle_step * i as f64;
[
center_u[0] + radius_to_vertices * libm::cos(angle),
center_u[1] + radius_to_vertices * libm::sin(angle),
]
})
.collect();
let mut sketch = crate::std::sketch::inner_start_profile(
sketch_surface,
point_to_typed(vertices[0], units),
None,
exec_state,
&args.ctx,
args.source_range,
)
.await?;
for vertex in vertices.iter().skip(1) {
let from = sketch.current_pen_position()?;
let id = exec_state.next_uuid();
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, id),
ModelingCmd::from(
mcmd::ExtendPath::builder()
.path(sketch.id.into())
.segment(PathSegment::Line {
end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
.with_z(0.0)
.map(LengthUnit),
relative: false,
})
.build(),
),
)
.await?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.ignore_units(),
to: *vertex,
tag: None,
units: sketch.units,
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
sketch.paths.push(current_path);
}
let from = sketch.current_pen_position()?;
let close_id = exec_state.next_uuid();
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, close_id),
ModelingCmd::from(
mcmd::ExtendPath::builder()
.path(sketch.id.into())
.segment(PathSegment::Line {
end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
.with_z(0.0)
.map(LengthUnit),
relative: false,
})
.build(),
),
)
.await?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.ignore_units(),
to: vertices[0],
tag: None,
units: sketch.units,
geo_meta: GeoMeta {
id: close_id,
metadata: args.source_range.into(),
},
},
};
sketch.paths.push(current_path);
sketch.is_closed = ProfileClosed::Explicitly;
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args(exec_state, &args),
ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
)
.await?;
Ok(sketch)
}
pub async fn ellipse(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_or_surface =
args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
let sketch = inner_ellipse(
sketch_or_surface,
center,
major_radius,
major_axis,
minor_radius,
tag,
exec_state,
args,
)
.await?;
Ok(KclValue::Sketch {
value: Box::new(sketch),
})
}
#[allow(clippy::too_many_arguments)]
async fn inner_ellipse(
sketch_surface_or_group: SketchOrSurface,
center: Option<[TyF64; 2]>,
major_radius: Option<TyF64>,
major_axis: Option<[TyF64; 2]>,
minor_radius: TyF64,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
let sketch_surface = sketch_surface_or_group.into_sketch_surface();
let center = center.unwrap_or(POINT_ZERO_ZERO);
let (center_u, ty) = untype_point(center.clone());
let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
let major_axis = match (major_axis, major_radius) {
(Some(_), Some(_)) | (None, None) => {
return Err(KclError::new_type(KclErrorDetails::new(
"Provide either `majorAxis` or `majorRadius`.".to_string(),
vec![args.source_range],
)));
}
(Some(major_axis), None) => major_axis,
(None, Some(major_radius)) => [
major_radius.clone(),
TyF64 {
n: 0.0,
ty: major_radius.ty,
},
],
};
let from = [
center_u[0] + major_axis[0].to_length_units(units),
center_u[1] + major_axis[1].to_length_units(units),
];
let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
let sketch =
crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
.await?;
let angle_start = Angle::zero();
let angle_end = Angle::turn();
let id = exec_state.next_uuid();
let axis = KPoint2d::from(untyped_point_to_mm([major_axis[0].n, major_axis[1].n], units)).map(LengthUnit);
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, id),
ModelingCmd::from(
mcmd::ExtendPath::builder()
.path(sketch.id.into())
.segment(PathSegment::Ellipse {
center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
major_axis: axis,
minor_radius: LengthUnit(minor_radius.to_mm()),
start_angle: Angle::from_degrees(angle_start.to_degrees()),
end_angle: Angle::from_degrees(angle_end.to_degrees()),
})
.build(),
),
)
.await?;
let current_path = Path::Ellipse {
base: BasePath {
from,
to: from,
tag: tag.clone(),
units,
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
major_axis: major_axis.map(|x| x.to_length_units(units)),
minor_radius: minor_radius.to_length_units(units),
center: center_u,
ccw: angle_start < angle_end,
};
let mut new_sketch = sketch;
new_sketch.is_closed = ProfileClosed::Explicitly;
if let Some(tag) = &tag {
new_sketch.add_tag(tag, ¤t_path, exec_state, None);
}
new_sketch.paths.push(current_path);
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, id),
ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
)
.await?;
Ok(new_sketch)
}
pub(crate) fn get_radius(
radius: Option<TyF64>,
diameter: Option<TyF64>,
source_range: SourceRange,
) -> Result<TyF64, KclError> {
get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
}
pub(crate) fn get_radius_labelled(
radius: Option<TyF64>,
diameter: Option<TyF64>,
source_range: SourceRange,
label_radius: &'static str,
label_diameter: &'static str,
) -> Result<TyF64, KclError> {
match (radius, diameter) {
(Some(radius), None) => Ok(radius),
(None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
(None, None) => Err(KclError::new_type(KclErrorDetails::new(
format!("This function needs either `{label_diameter}` or `{label_radius}`"),
vec![source_range],
))),
(Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
vec![source_range],
))),
}
}