Files
modeling-app/rust/kcl-lib/src/execution/geometry.rs
Jess Frazelle bd4bad0020 allow sending async commands to engine (#6342)
* start of async

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

check at end if the async commands completed

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

run at the end of inner_run

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

set import as async

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

updates

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

updates

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

add to the wasm side

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

updates

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

fmt

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

* fire

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

* flake

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

* fixup for awaiting on import

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

* updates

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

* updates

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

* fix mock

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

* fix mock

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

* updates

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

* fixes

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

* add a test where we import then do a bunch of other stuff

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

* updates

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

* fixup to see

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

* fixups

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

* fix tests

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

* updates

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

* cross platform time

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>

* updates

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

* another appearance tests

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

* new docs and tests

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

* updates

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

* dont loop so tight

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-17 17:22:19 -07:00

1451 lines
46 KiB
Rust

use std::ops::{Add, AddAssign, Mul};
use anyhow::Result;
use indexmap::IndexMap;
use kittycad_modeling_cmds as kcmc;
use kittycad_modeling_cmds::{each_cmd as mcmd, length_unit::LengthUnit, websocket::ModelingCmdReq, ModelingCmd};
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
execution::{types::NumericType, ArtifactId, ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
std::{args::TyF64, sketch::PlaneData},
};
use super::ExecutorContext;
type Point2D = kcmc::shared::Point2d<f64>;
type Point3D = kcmc::shared::Point3d<f64>;
/// A geometry.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Geometry {
Sketch(Sketch),
Solid(Solid),
}
impl Geometry {
pub fn id(&self) -> uuid::Uuid {
match self {
Geometry::Sketch(s) => s.id,
Geometry::Solid(e) => e.id,
}
}
/// If this geometry is the result of a pattern, then return the ID of
/// the original sketch which was patterned.
/// Equivalent to the `id()` method if this isn't a pattern.
pub fn original_id(&self) -> uuid::Uuid {
match self {
Geometry::Sketch(s) => s.original_id,
Geometry::Solid(e) => e.sketch.original_id,
}
}
}
/// A set of geometry.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
#[allow(clippy::vec_box)]
pub enum Geometries {
Sketches(Vec<Sketch>),
Solids(Vec<Solid>),
}
impl From<Geometry> for Geometries {
fn from(value: Geometry) -> Self {
match value {
Geometry::Sketch(x) => Self::Sketches(vec![x]),
Geometry::Solid(x) => Self::Solids(vec![x]),
}
}
}
/// Data for an imported geometry.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ImportedGeometry {
/// The ID of the imported geometry.
pub id: uuid::Uuid,
/// The original file paths.
pub value: Vec<String>,
#[serde(skip)]
pub meta: Vec<Metadata>,
/// If the imported geometry has completed.
#[serde(skip)]
completed: bool,
}
impl ImportedGeometry {
pub fn new(id: uuid::Uuid, value: Vec<String>, meta: Vec<Metadata>) -> Self {
Self {
id,
value,
meta,
completed: false,
}
}
async fn wait_for_finish(&mut self, ctx: &ExecutorContext) -> Result<(), KclError> {
if self.completed {
return Ok(());
}
ctx.engine
.ensure_async_command_completed(self.id, self.meta.first().map(|m| m.source_range))
.await?;
self.completed = true;
Ok(())
}
pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
if !self.completed {
self.wait_for_finish(ctx).await?;
}
Ok(self.id)
}
}
/// Data for a solid, sketch, or an imported geometry.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::vec_box)]
pub enum SolidOrSketchOrImportedGeometry {
ImportedGeometry(Box<ImportedGeometry>),
SolidSet(Vec<Solid>),
SketchSet(Vec<Sketch>),
}
impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
fn from(value: SolidOrSketchOrImportedGeometry) -> Self {
match value {
SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
SolidOrSketchOrImportedGeometry::SolidSet(mut s) => {
if s.len() == 1 {
crate::execution::KclValue::Solid {
value: Box::new(s.pop().unwrap()),
}
} else {
crate::execution::KclValue::HomArray {
value: s
.into_iter()
.map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
.collect(),
ty: crate::execution::types::RuntimeType::solid(),
}
}
}
SolidOrSketchOrImportedGeometry::SketchSet(mut s) => {
if s.len() == 1 {
crate::execution::KclValue::Sketch {
value: Box::new(s.pop().unwrap()),
}
} else {
crate::execution::KclValue::HomArray {
value: s
.into_iter()
.map(|s| crate::execution::KclValue::Sketch { value: Box::new(s) })
.collect(),
ty: crate::execution::types::RuntimeType::sketch(),
}
}
}
}
}
}
impl SolidOrSketchOrImportedGeometry {
pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
match self {
SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => {
let id = s.id(ctx).await?;
Ok(vec![id])
}
SolidOrSketchOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
SolidOrSketchOrImportedGeometry::SketchSet(s) => Ok(s.iter().map(|s| s.id).collect()),
}
}
}
/// Data for a solid or an imported geometry.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::vec_box)]
pub enum SolidOrImportedGeometry {
ImportedGeometry(Box<ImportedGeometry>),
SolidSet(Vec<Solid>),
}
impl From<SolidOrImportedGeometry> for crate::execution::KclValue {
fn from(value: SolidOrImportedGeometry) -> Self {
match value {
SolidOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
SolidOrImportedGeometry::SolidSet(mut s) => {
if s.len() == 1 {
crate::execution::KclValue::Solid {
value: Box::new(s.pop().unwrap()),
}
} else {
crate::execution::KclValue::HomArray {
value: s
.into_iter()
.map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
.collect(),
ty: crate::execution::types::RuntimeType::solid(),
}
}
}
}
}
}
impl SolidOrImportedGeometry {
pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
match self {
SolidOrImportedGeometry::ImportedGeometry(s) => {
let id = s.id(ctx).await?;
Ok(vec![id])
}
SolidOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
}
}
}
/// A helix.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Helix {
/// The id of the helix.
pub value: uuid::Uuid,
/// The artifact ID.
pub artifact_id: ArtifactId,
/// Number of revolutions.
pub revolutions: f64,
/// Start angle (in degrees).
pub angle_start: f64,
/// Is the helix rotation counter clockwise?
pub ccw: bool,
/// The cylinder the helix was created on.
pub cylinder_id: Option<uuid::Uuid>,
pub units: UnitLen,
#[serde(skip)]
pub meta: Vec<Metadata>,
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Plane {
/// The id of the plane.
pub id: uuid::Uuid,
/// The artifact ID.
pub artifact_id: ArtifactId,
// The code for the plane either a string or custom.
pub value: PlaneType,
/// Origin of the plane.
pub origin: Point3d,
/// What should the plane's X axis be?
pub x_axis: Point3d,
/// What should the plane's Y axis be?
pub y_axis: Point3d,
/// The z-axis (normal).
pub z_axis: Point3d,
pub units: UnitLen,
#[serde(skip)]
pub meta: Vec<Metadata>,
}
impl Plane {
pub(crate) fn into_plane_data(self) -> PlaneData {
if self.origin.is_zero() {
match self {
Self {
origin:
Point3d {
x: 0.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
x_axis:
Point3d {
x: 1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
y_axis:
Point3d {
x: 0.0,
y: 1.0,
z: 0.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 0.0,
y: 0.0,
z: 1.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::XY,
Self {
origin:
Point3d {
x: 0.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
x_axis:
Point3d {
x: 1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
y_axis:
Point3d {
x: 0.0,
y: 1.0,
z: 0.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 0.0,
y: 0.0,
z: -1.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::NegXY,
Self {
origin:
Point3d {
x: 0.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
x_axis:
Point3d {
x: 1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
y_axis:
Point3d {
x: 0.0,
y: 0.0,
z: 1.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 0.0,
y: -1.0,
z: 0.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::XZ,
Self {
origin:
Point3d {
x: 0.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
x_axis:
Point3d {
x: 1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
y_axis:
Point3d {
x: 0.0,
y: 0.0,
z: 1.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 0.0,
y: 1.0,
z: 0.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::NegXZ,
Self {
origin:
Point3d {
x: 0.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
x_axis:
Point3d {
x: 0.0,
y: 1.0,
z: 0.0,
units: UnitLen::Mm,
},
y_axis:
Point3d {
x: 0.0,
y: 0.0,
z: 1.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::YZ,
Self {
origin:
Point3d {
x: 0.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
x_axis:
Point3d {
x: 0.0,
y: 1.0,
z: 0.0,
units: UnitLen::Mm,
},
y_axis:
Point3d {
x: 0.0,
y: 0.0,
z: 1.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: -1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::NegYZ,
_ => {}
}
}
PlaneData::Plane {
origin: self.origin,
x_axis: self.x_axis,
y_axis: self.y_axis,
z_axis: self.z_axis,
}
}
pub(crate) fn from_plane_data(value: PlaneData, exec_state: &mut ExecState) -> Self {
let id = exec_state.next_uuid();
match value {
PlaneData::XY => Plane {
id,
artifact_id: id.into(),
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
z_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
value: PlaneType::XY,
units: exec_state.length_unit(),
meta: vec![],
},
PlaneData::NegXY => Plane {
id,
artifact_id: id.into(),
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
z_axis: Point3d::new(0.0, 0.0, -1.0, UnitLen::Mm),
value: PlaneType::XY,
units: exec_state.length_unit(),
meta: vec![],
},
PlaneData::XZ => Plane {
id,
artifact_id: id.into(),
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
z_axis: Point3d::new(0.0, -1.0, 0.0, UnitLen::Mm),
value: PlaneType::XZ,
units: exec_state.length_unit(),
meta: vec![],
},
PlaneData::NegXZ => Plane {
id,
artifact_id: id.into(),
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
z_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
value: PlaneType::XZ,
units: exec_state.length_unit(),
meta: vec![],
},
PlaneData::YZ => Plane {
id,
artifact_id: id.into(),
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
z_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Mm),
value: PlaneType::YZ,
units: exec_state.length_unit(),
meta: vec![],
},
PlaneData::NegYZ => Plane {
id,
artifact_id: id.into(),
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
z_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Mm),
value: PlaneType::YZ,
units: exec_state.length_unit(),
meta: vec![],
},
PlaneData::Plane {
origin,
x_axis,
y_axis,
z_axis,
} => {
let id = exec_state.next_uuid();
Plane {
id,
artifact_id: id.into(),
origin,
x_axis,
y_axis,
z_axis,
value: PlaneType::Custom,
units: exec_state.length_unit(),
meta: vec![],
}
}
}
}
/// The standard planes are XY, YZ and XZ (in both positive and negative)
pub fn is_standard(&self) -> bool {
!matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
}
}
/// A face.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Face {
/// The id of the face.
pub id: uuid::Uuid,
/// The artifact ID.
pub artifact_id: ArtifactId,
/// The tag of the face.
pub value: String,
/// What should the face's X axis be?
pub x_axis: Point3d,
/// What should the face's Y axis be?
pub y_axis: Point3d,
/// The z-axis (normal).
pub z_axis: Point3d,
/// The solid the face is on.
pub solid: Box<Solid>,
pub units: UnitLen,
#[serde(skip)]
pub meta: Vec<Metadata>,
}
/// Type for a plane.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
#[ts(export)]
#[display(style = "camelCase")]
pub enum PlaneType {
#[serde(rename = "XY", alias = "xy")]
#[display("XY")]
XY,
#[serde(rename = "XZ", alias = "xz")]
#[display("XZ")]
XZ,
#[serde(rename = "YZ", alias = "yz")]
#[display("YZ")]
YZ,
/// A custom plane.
#[display("Custom")]
Custom,
/// A custom plane which has not been sent to the engine. It must be sent before it is used.
#[display("Uninit")]
Uninit,
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct Sketch {
/// The id of the sketch (this will change when the engine's reference to it changes).
pub id: uuid::Uuid,
/// The paths in the sketch.
pub paths: Vec<Path>,
/// What the sketch is on (can be a plane or a face).
pub on: SketchSurface,
/// The starting path.
pub start: BasePath,
/// Tag identifiers that have been declared in this sketch.
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub tags: IndexMap<String, TagIdentifier>,
/// The original id of the sketch. This stays the same even if the sketch is
/// is sketched on face etc.
pub artifact_id: ArtifactId,
#[ts(skip)]
pub original_id: uuid::Uuid,
/// If the sketch includes a mirror.
#[serde(skip)]
pub mirror: Option<uuid::Uuid>,
pub units: UnitLen,
/// Metadata.
#[serde(skip)]
pub meta: Vec<Metadata>,
}
impl Sketch {
// Tell the engine to enter sketch mode on the sketch.
// Run a specific command, then exit sketch mode.
pub(crate) fn build_sketch_mode_cmds(
&self,
exec_state: &mut ExecState,
inner_cmd: ModelingCmdReq,
) -> Vec<ModelingCmdReq> {
vec![
// Before we extrude, we need to enable the sketch mode.
// We do this here in case extrude is called out of order.
ModelingCmdReq {
cmd: ModelingCmd::from(mcmd::EnableSketchMode {
animated: false,
ortho: false,
entity_id: self.on.id(),
adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &self.on {
// We pass in the normal for the plane here.
Some(plane.z_axis.into())
} else {
None
},
}),
cmd_id: exec_state.next_uuid().into(),
},
inner_cmd,
ModelingCmdReq {
cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
cmd_id: exec_state.next_uuid().into(),
},
]
}
}
/// A sketch type.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SketchSurface {
Plane(Box<Plane>),
Face(Box<Face>),
}
impl SketchSurface {
pub(crate) fn id(&self) -> uuid::Uuid {
match self {
SketchSurface::Plane(plane) => plane.id,
SketchSurface::Face(face) => face.id,
}
}
pub(crate) fn x_axis(&self) -> Point3d {
match self {
SketchSurface::Plane(plane) => plane.x_axis,
SketchSurface::Face(face) => face.x_axis,
}
}
pub(crate) fn y_axis(&self) -> Point3d {
match self {
SketchSurface::Plane(plane) => plane.y_axis,
SketchSurface::Face(face) => face.y_axis,
}
}
pub(crate) fn z_axis(&self) -> Point3d {
match self {
SketchSurface::Plane(plane) => plane.z_axis,
SketchSurface::Face(face) => face.z_axis,
}
}
pub(crate) fn units(&self) -> UnitLen {
match self {
SketchSurface::Plane(plane) => plane.units,
SketchSurface::Face(face) => face.units,
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum GetTangentialInfoFromPathsResult {
PreviousPoint([f64; 2]),
Arc { center: [f64; 2], ccw: bool },
Circle { center: [f64; 2], ccw: bool, radius: f64 },
}
impl GetTangentialInfoFromPathsResult {
pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
match self {
GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
GetTangentialInfoFromPathsResult::Arc { center, ccw } => {
crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
}
// The circle always starts at 0 degrees, so a suitable tangent
// point is either directly above or below.
GetTangentialInfoFromPathsResult::Circle {
center, radius, ccw, ..
} => [center[0] + radius, center[1] + if *ccw { -1.0 } else { 1.0 }],
}
}
}
impl Sketch {
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path, exec_state: &ExecState) {
let mut tag_identifier: TagIdentifier = tag.into();
let base = current_path.get_base();
tag_identifier.info.push((
exec_state.stack().current_epoch(),
TagEngineInfo {
id: base.geo_meta.id,
sketch: self.id,
path: Some(current_path.clone()),
surface: None,
},
));
self.tags.insert(tag.name.to_string(), tag_identifier);
}
pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
for t in tags {
match self.tags.get_mut(&t.value) {
Some(id) => {
id.merge_info(t);
}
None => {
self.tags.insert(t.value.clone(), t.clone());
}
}
}
}
/// Get the path most recently sketched.
pub(crate) fn latest_path(&self) -> Option<&Path> {
self.paths.last()
}
/// The "pen" is an imaginary pen drawing the path.
/// This gets the current point the pen is hovering over, i.e. the point
/// where the last path segment ends, and the next path segment will begin.
pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
let Some(path) = self.latest_path() else {
return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units));
};
Ok(path.get_to().into())
}
pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
let Some(path) = self.latest_path() else {
return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
};
path.get_tangential_info()
}
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct Solid {
/// The id of the solid.
pub id: uuid::Uuid,
/// The artifact ID of the solid. Unlike `id`, this doesn't change.
pub artifact_id: ArtifactId,
/// The extrude surfaces.
pub value: Vec<ExtrudeSurface>,
/// The sketch.
pub sketch: Sketch,
/// The height of the solid.
pub height: f64,
/// The id of the extrusion start cap
pub start_cap_id: Option<uuid::Uuid>,
/// The id of the extrusion end cap
pub end_cap_id: Option<uuid::Uuid>,
/// Chamfers or fillets on this solid.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_cuts: Vec<EdgeCut>,
pub units: UnitLen,
/// Metadata.
#[serde(skip)]
pub meta: Vec<Metadata>,
}
impl Solid {
pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
self.edge_cuts.iter().map(|foc| foc.id())
}
}
/// A fillet or a chamfer.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum EdgeCut {
/// A fillet.
Fillet {
/// The id of the engine command that called this fillet.
id: uuid::Uuid,
radius: TyF64,
/// The engine id of the edge to fillet.
#[serde(rename = "edgeId")]
edge_id: uuid::Uuid,
tag: Box<Option<TagNode>>,
},
/// A chamfer.
Chamfer {
/// The id of the engine command that called this chamfer.
id: uuid::Uuid,
length: TyF64,
/// The engine id of the edge to chamfer.
#[serde(rename = "edgeId")]
edge_id: uuid::Uuid,
tag: Box<Option<TagNode>>,
},
}
impl EdgeCut {
pub fn id(&self) -> uuid::Uuid {
match self {
EdgeCut::Fillet { id, .. } => *id,
EdgeCut::Chamfer { id, .. } => *id,
}
}
pub fn edge_id(&self) -> uuid::Uuid {
match self {
EdgeCut::Fillet { edge_id, .. } => *edge_id,
EdgeCut::Chamfer { edge_id, .. } => *edge_id,
}
}
pub fn tag(&self) -> Option<TagNode> {
match self {
EdgeCut::Fillet { tag, .. } => *tag.clone(),
EdgeCut::Chamfer { tag, .. } => *tag.clone(),
}
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Point2d {
pub x: f64,
pub y: f64,
pub units: UnitLen,
}
impl From<[TyF64; 2]> for Point2d {
fn from(p: [TyF64; 2]) -> Self {
Self {
x: p[0].n,
y: p[1].n,
units: p[0].ty.expect_length(),
}
}
}
impl From<Point2d> for [f64; 2] {
fn from(p: Point2d) -> Self {
[p.x, p.y]
}
}
impl From<Point2d> for Point2D {
fn from(p: Point2d) -> Self {
Self { x: p.x, y: p.y }
}
}
impl Point2d {
pub const ZERO: Self = Self {
x: 0.0,
y: 0.0,
units: UnitLen::Mm,
};
pub fn new(x: f64, y: f64, units: UnitLen) -> Self {
Self { x, y, units }
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)]
#[ts(export)]
pub struct Point3d {
pub x: f64,
pub y: f64,
pub z: f64,
pub units: UnitLen,
}
impl Point3d {
pub const ZERO: Self = Self {
x: 0.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
};
pub fn new(x: f64, y: f64, z: f64, units: UnitLen) -> Self {
Self { x, y, z, units }
}
pub const fn is_zero(&self) -> bool {
self.x == 0.0 && self.y == 0.0 && self.z == 0.0
}
}
impl From<[TyF64; 3]> for Point3d {
fn from(p: [TyF64; 3]) -> Self {
Self {
x: p[0].n,
y: p[1].n,
z: p[2].n,
units: p[0].ty.expect_length(),
}
}
}
impl From<Point3d> for Point3D {
fn from(p: Point3d) -> Self {
Self { x: p.x, y: p.y, z: p.z }
}
}
impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
fn from(p: Point3d) -> Self {
Self {
x: LengthUnit(p.x),
y: LengthUnit(p.y),
z: LengthUnit(p.z),
}
}
}
impl Add for Point3d {
type Output = Point3d;
fn add(self, rhs: Self) -> Self::Output {
// TODO should assert that self and rhs the same units or coerce them
Point3d {
x: self.x + rhs.x,
y: self.y + rhs.y,
z: self.z + rhs.z,
units: self.units,
}
}
}
impl AddAssign for Point3d {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs
}
}
impl Mul<f64> for Point3d {
type Output = Point3d;
fn mul(self, rhs: f64) -> Self::Output {
Point3d {
x: self.x * rhs,
y: self.y * rhs,
z: self.z * rhs,
units: self.units,
}
}
}
/// A base path.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct BasePath {
/// The from point.
#[ts(type = "[number, number]")]
pub from: [f64; 2],
/// The to point.
#[ts(type = "[number, number]")]
pub to: [f64; 2],
pub units: UnitLen,
/// The tag of the path.
pub tag: Option<TagNode>,
/// Metadata.
#[serde(rename = "__geoMeta")]
pub geo_meta: GeoMeta,
}
impl BasePath {
pub fn get_to(&self) -> [TyF64; 2] {
let ty: NumericType = self.units.into();
[TyF64::new(self.to[0], ty.clone()), TyF64::new(self.to[1], ty)]
}
pub fn get_from(&self) -> [TyF64; 2] {
let ty: NumericType = self.units.into();
[TyF64::new(self.from[0], ty.clone()), TyF64::new(self.from[1], ty)]
}
}
/// Geometry metadata.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct GeoMeta {
/// The id of the geometry.
pub id: uuid::Uuid,
/// Metadata.
#[serde(flatten)]
pub metadata: Metadata,
}
/// A path.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Path {
/// A path that goes to a point.
ToPoint {
#[serde(flatten)]
base: BasePath,
},
/// A arc that is tangential to the last path segment that goes to a point
TangentialArcTo {
#[serde(flatten)]
base: BasePath,
/// the arc's center
#[ts(type = "[number, number]")]
center: [f64; 2],
/// arc's direction
ccw: bool,
},
/// A arc that is tangential to the last path segment
TangentialArc {
#[serde(flatten)]
base: BasePath,
/// the arc's center
#[ts(type = "[number, number]")]
center: [f64; 2],
/// arc's direction
ccw: bool,
},
// TODO: consolidate segment enums, remove Circle. https://github.com/KittyCAD/modeling-app/issues/3940
/// a complete arc
Circle {
#[serde(flatten)]
base: BasePath,
/// the arc's center
#[ts(type = "[number, number]")]
center: [f64; 2],
/// the arc's radius
radius: f64,
/// arc's direction
/// This is used to compute the tangential angle.
ccw: bool,
},
CircleThreePoint {
#[serde(flatten)]
base: BasePath,
/// Point 1 of the circle
#[ts(type = "[number, number]")]
p1: [f64; 2],
/// Point 2 of the circle
#[ts(type = "[number, number]")]
p2: [f64; 2],
/// Point 3 of the circle
#[ts(type = "[number, number]")]
p3: [f64; 2],
},
ArcThreePoint {
#[serde(flatten)]
base: BasePath,
/// Point 1 of the arc (base on the end of previous segment)
#[ts(type = "[number, number]")]
p1: [f64; 2],
/// Point 2 of the arc (interior kwarg)
#[ts(type = "[number, number]")]
p2: [f64; 2],
/// Point 3 of the arc (end kwarg)
#[ts(type = "[number, number]")]
p3: [f64; 2],
},
/// A path that is horizontal.
Horizontal {
#[serde(flatten)]
base: BasePath,
/// The x coordinate.
x: f64,
},
/// An angled line to.
AngledLineTo {
#[serde(flatten)]
base: BasePath,
/// The x coordinate.
x: Option<f64>,
/// The y coordinate.
y: Option<f64>,
},
/// A base path.
Base {
#[serde(flatten)]
base: BasePath,
},
/// A circular arc, not necessarily tangential to the current point.
Arc {
#[serde(flatten)]
base: BasePath,
/// Center of the circle that this arc is drawn on.
center: [f64; 2],
/// Radius of the circle that this arc is drawn on.
radius: f64,
/// True if the arc is counterclockwise.
ccw: bool,
},
}
/// What kind of path is this?
#[derive(Display)]
enum PathType {
ToPoint,
Base,
TangentialArc,
TangentialArcTo,
Circle,
CircleThreePoint,
Horizontal,
AngledLineTo,
Arc,
}
impl From<&Path> for PathType {
fn from(value: &Path) -> Self {
match value {
Path::ToPoint { .. } => Self::ToPoint,
Path::TangentialArcTo { .. } => Self::TangentialArcTo,
Path::TangentialArc { .. } => Self::TangentialArc,
Path::Circle { .. } => Self::Circle,
Path::CircleThreePoint { .. } => Self::CircleThreePoint,
Path::Horizontal { .. } => Self::Horizontal,
Path::AngledLineTo { .. } => Self::AngledLineTo,
Path::Base { .. } => Self::Base,
Path::Arc { .. } => Self::Arc,
Path::ArcThreePoint { .. } => Self::Arc,
}
}
}
impl Path {
pub fn get_id(&self) -> uuid::Uuid {
match self {
Path::ToPoint { base } => base.geo_meta.id,
Path::Horizontal { base, .. } => base.geo_meta.id,
Path::AngledLineTo { base, .. } => base.geo_meta.id,
Path::Base { base } => base.geo_meta.id,
Path::TangentialArcTo { base, .. } => base.geo_meta.id,
Path::TangentialArc { base, .. } => base.geo_meta.id,
Path::Circle { base, .. } => base.geo_meta.id,
Path::CircleThreePoint { base, .. } => base.geo_meta.id,
Path::Arc { base, .. } => base.geo_meta.id,
Path::ArcThreePoint { base, .. } => base.geo_meta.id,
}
}
pub fn get_tag(&self) -> Option<TagNode> {
match self {
Path::ToPoint { base } => base.tag.clone(),
Path::Horizontal { base, .. } => base.tag.clone(),
Path::AngledLineTo { base, .. } => base.tag.clone(),
Path::Base { base } => base.tag.clone(),
Path::TangentialArcTo { base, .. } => base.tag.clone(),
Path::TangentialArc { base, .. } => base.tag.clone(),
Path::Circle { base, .. } => base.tag.clone(),
Path::CircleThreePoint { base, .. } => base.tag.clone(),
Path::Arc { base, .. } => base.tag.clone(),
Path::ArcThreePoint { base, .. } => base.tag.clone(),
}
}
pub fn get_base(&self) -> &BasePath {
match self {
Path::ToPoint { base } => base,
Path::Horizontal { base, .. } => base,
Path::AngledLineTo { base, .. } => base,
Path::Base { base } => base,
Path::TangentialArcTo { base, .. } => base,
Path::TangentialArc { base, .. } => base,
Path::Circle { base, .. } => base,
Path::CircleThreePoint { base, .. } => base,
Path::Arc { base, .. } => base,
Path::ArcThreePoint { base, .. } => base,
}
}
/// Where does this path segment start?
pub fn get_from(&self) -> [TyF64; 2] {
let p = &self.get_base().from;
let ty: NumericType = self.get_base().units.into();
[TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
}
/// Where does this path segment end?
pub fn get_to(&self) -> [TyF64; 2] {
let p = &self.get_base().to;
let ty: NumericType = self.get_base().units.into();
[TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
}
/// Length of this path segment, in cartesian plane.
pub fn length(&self) -> TyF64 {
let n = match self {
Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
linear_distance(&self.get_base().from, &self.get_base().to)
}
Self::TangentialArc {
base: _,
center,
ccw: _,
}
| Self::TangentialArcTo {
base: _,
center,
ccw: _,
} => {
// The radius can be calculated as the linear distance between `to` and `center`,
// or between `from` and `center`. They should be the same.
let radius = linear_distance(&self.get_base().from, center);
debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
// TODO: Call engine utils to figure this out.
linear_distance(&self.get_base().from, &self.get_base().to)
}
Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
Self::CircleThreePoint { .. } => {
let circle_center = crate::std::utils::calculate_circle_from_3_points([
self.get_base().from,
self.get_base().to,
self.get_base().to,
]);
let radius = linear_distance(
&[circle_center.center[0], circle_center.center[1]],
&self.get_base().from,
);
2.0 * std::f64::consts::PI * radius
}
Self::Arc { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(&self.get_base().from, &self.get_base().to)
}
Self::ArcThreePoint { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(&self.get_base().from, &self.get_base().to)
}
};
TyF64::new(n, self.get_base().units.into())
}
pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
match self {
Path::ToPoint { base } => Some(base),
Path::Horizontal { base, .. } => Some(base),
Path::AngledLineTo { base, .. } => Some(base),
Path::Base { base } => Some(base),
Path::TangentialArcTo { base, .. } => Some(base),
Path::TangentialArc { base, .. } => Some(base),
Path::Circle { base, .. } => Some(base),
Path::CircleThreePoint { base, .. } => Some(base),
Path::Arc { base, .. } => Some(base),
Path::ArcThreePoint { base, .. } => Some(base),
}
}
pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
match self {
Path::TangentialArc { center, ccw, .. }
| Path::TangentialArcTo { center, ccw, .. }
| Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
center: *center,
ccw: *ccw,
},
Path::ArcThreePoint { p1, p2, p3, .. } => {
let circle_center = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
GetTangentialInfoFromPathsResult::Arc {
center: circle_center.center,
ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
}
}
Path::Circle {
center, ccw, radius, ..
} => GetTangentialInfoFromPathsResult::Circle {
center: *center,
ccw: *ccw,
radius: *radius,
},
Path::CircleThreePoint { p1, p2, p3, .. } => {
let circle_center = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
let radius = linear_distance(&[circle_center.center[0], circle_center.center[1]], p1);
let center_point = [circle_center.center[0], circle_center.center[1]];
GetTangentialInfoFromPathsResult::Circle {
center: center_point,
// Note: a circle is always ccw regardless of the order of points
ccw: true,
radius,
}
}
Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
let base = self.get_base();
GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
}
}
}
}
/// Compute the straight-line distance between a pair of (2D) points.
#[rustfmt::skip]
fn linear_distance(
[x0, y0]: &[f64; 2],
[x1, y1]: &[f64; 2]
) -> f64 {
let y_sq = (y1 - y0).powi(2);
let x_sq = (x1 - x0).powi(2);
(y_sq + x_sq).sqrt()
}
/// An extrude surface.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ExtrudeSurface {
/// An extrude plane.
ExtrudePlane(ExtrudePlane),
ExtrudeArc(ExtrudeArc),
Chamfer(ChamferSurface),
Fillet(FilletSurface),
}
// Chamfer surface.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ChamferSurface {
/// The id for the chamfer surface.
pub face_id: uuid::Uuid,
/// The tag.
pub tag: Option<Node<TagDeclarator>>,
/// Metadata.
#[serde(flatten)]
pub geo_meta: GeoMeta,
}
// Fillet surface.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct FilletSurface {
/// The id for the fillet surface.
pub face_id: uuid::Uuid,
/// The tag.
pub tag: Option<Node<TagDeclarator>>,
/// Metadata.
#[serde(flatten)]
pub geo_meta: GeoMeta,
}
/// An extruded plane.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExtrudePlane {
/// The face id for the extrude plane.
pub face_id: uuid::Uuid,
/// The tag.
pub tag: Option<Node<TagDeclarator>>,
/// Metadata.
#[serde(flatten)]
pub geo_meta: GeoMeta,
}
/// An extruded arc.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExtrudeArc {
/// The face id for the extrude plane.
pub face_id: uuid::Uuid,
/// The tag.
pub tag: Option<Node<TagDeclarator>>,
/// Metadata.
#[serde(flatten)]
pub geo_meta: GeoMeta,
}
impl ExtrudeSurface {
pub fn get_id(&self) -> uuid::Uuid {
match self {
ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
ExtrudeSurface::Fillet(f) => f.geo_meta.id,
ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
}
}
pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
match self {
ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
ExtrudeSurface::Fillet(f) => f.tag.clone(),
ExtrudeSurface::Chamfer(c) => c.tag.clone(),
}
}
}