kcl_lib/execution/
geometry.rs

1use std::ops::{Add, AddAssign, Mul, Sub, SubAssign};
2
3use anyhow::Result;
4use indexmap::IndexMap;
5use kcl_error::SourceRange;
6use kittycad_modeling_cmds as kcmc;
7use kittycad_modeling_cmds::{
8    ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, units::UnitLength, websocket::ModelingCmdReq,
9};
10use parse_display::{Display, FromStr};
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    engine::{DEFAULT_PLANE_INFO, PlaneName},
15    errors::{KclError, KclErrorDetails},
16    execution::{
17        ArtifactId, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier,
18        types::{NumericType, adjust_length},
19    },
20    parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
21    std::{args::TyF64, sketch::PlaneData},
22};
23
24type Point3D = kcmc::shared::Point3d<f64>;
25
26/// A GD&T annotation.
27#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
28#[ts(export)]
29#[serde(tag = "type", rename_all = "camelCase")]
30pub struct GdtAnnotation {
31    /// The engine ID.
32    pub id: uuid::Uuid,
33    #[serde(skip)]
34    pub meta: Vec<Metadata>,
35}
36
37/// A geometry.
38#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
39#[ts(export)]
40#[serde(tag = "type")]
41#[allow(clippy::large_enum_variant)]
42pub enum Geometry {
43    Sketch(Sketch),
44    Solid(Solid),
45}
46
47impl Geometry {
48    pub fn id(&self) -> uuid::Uuid {
49        match self {
50            Geometry::Sketch(s) => s.id,
51            Geometry::Solid(e) => e.id,
52        }
53    }
54
55    /// If this geometry is the result of a pattern, then return the ID of
56    /// the original sketch which was patterned.
57    /// Equivalent to the `id()` method if this isn't a pattern.
58    pub fn original_id(&self) -> uuid::Uuid {
59        match self {
60            Geometry::Sketch(s) => s.original_id,
61            Geometry::Solid(e) => e.sketch.original_id,
62        }
63    }
64}
65
66/// A geometry including an imported geometry.
67#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
68#[ts(export)]
69#[serde(tag = "type")]
70#[allow(clippy::large_enum_variant)]
71pub enum GeometryWithImportedGeometry {
72    Sketch(Sketch),
73    Solid(Solid),
74    ImportedGeometry(Box<ImportedGeometry>),
75}
76
77impl GeometryWithImportedGeometry {
78    pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
79        match self {
80            GeometryWithImportedGeometry::Sketch(s) => Ok(s.id),
81            GeometryWithImportedGeometry::Solid(e) => Ok(e.id),
82            GeometryWithImportedGeometry::ImportedGeometry(i) => {
83                let id = i.id(ctx).await?;
84                Ok(id)
85            }
86        }
87    }
88}
89
90/// A set of geometry.
91#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
92#[ts(export)]
93#[serde(tag = "type")]
94#[allow(clippy::vec_box)]
95pub enum Geometries {
96    Sketches(Vec<Sketch>),
97    Solids(Vec<Solid>),
98}
99
100impl From<Geometry> for Geometries {
101    fn from(value: Geometry) -> Self {
102        match value {
103            Geometry::Sketch(x) => Self::Sketches(vec![x]),
104            Geometry::Solid(x) => Self::Solids(vec![x]),
105        }
106    }
107}
108
109/// Data for an imported geometry.
110#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
111#[ts(export)]
112#[serde(rename_all = "camelCase")]
113pub struct ImportedGeometry {
114    /// The ID of the imported geometry.
115    pub id: uuid::Uuid,
116    /// The original file paths.
117    pub value: Vec<String>,
118    #[serde(skip)]
119    pub meta: Vec<Metadata>,
120    /// If the imported geometry has completed.
121    #[serde(skip)]
122    completed: bool,
123}
124
125impl ImportedGeometry {
126    pub fn new(id: uuid::Uuid, value: Vec<String>, meta: Vec<Metadata>) -> Self {
127        Self {
128            id,
129            value,
130            meta,
131            completed: false,
132        }
133    }
134
135    async fn wait_for_finish(&mut self, ctx: &ExecutorContext) -> Result<(), KclError> {
136        if self.completed {
137            return Ok(());
138        }
139
140        ctx.engine
141            .ensure_async_command_completed(self.id, self.meta.first().map(|m| m.source_range))
142            .await?;
143
144        self.completed = true;
145
146        Ok(())
147    }
148
149    pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
150        if !self.completed {
151            self.wait_for_finish(ctx).await?;
152        }
153
154        Ok(self.id)
155    }
156}
157
158/// Data for a solid, sketch, or an imported geometry.
159#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
160#[ts(export)]
161#[serde(tag = "type", rename_all = "camelCase")]
162#[allow(clippy::vec_box)]
163pub enum SolidOrSketchOrImportedGeometry {
164    ImportedGeometry(Box<ImportedGeometry>),
165    SolidSet(Vec<Solid>),
166    SketchSet(Vec<Sketch>),
167}
168
169impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
170    fn from(value: SolidOrSketchOrImportedGeometry) -> Self {
171        match value {
172            SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
173            SolidOrSketchOrImportedGeometry::SolidSet(mut s) => {
174                if s.len() == 1 {
175                    crate::execution::KclValue::Solid {
176                        value: Box::new(s.pop().unwrap()),
177                    }
178                } else {
179                    crate::execution::KclValue::HomArray {
180                        value: s
181                            .into_iter()
182                            .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
183                            .collect(),
184                        ty: crate::execution::types::RuntimeType::solid(),
185                    }
186                }
187            }
188            SolidOrSketchOrImportedGeometry::SketchSet(mut s) => {
189                if s.len() == 1 {
190                    crate::execution::KclValue::Sketch {
191                        value: Box::new(s.pop().unwrap()),
192                    }
193                } else {
194                    crate::execution::KclValue::HomArray {
195                        value: s
196                            .into_iter()
197                            .map(|s| crate::execution::KclValue::Sketch { value: Box::new(s) })
198                            .collect(),
199                        ty: crate::execution::types::RuntimeType::sketch(),
200                    }
201                }
202            }
203        }
204    }
205}
206
207impl SolidOrSketchOrImportedGeometry {
208    pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
209        match self {
210            SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => {
211                let id = s.id(ctx).await?;
212
213                Ok(vec![id])
214            }
215            SolidOrSketchOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
216            SolidOrSketchOrImportedGeometry::SketchSet(s) => Ok(s.iter().map(|s| s.id).collect()),
217        }
218    }
219}
220
221/// Data for a solid or an imported geometry.
222#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
223#[ts(export)]
224#[serde(tag = "type", rename_all = "camelCase")]
225#[allow(clippy::vec_box)]
226pub enum SolidOrImportedGeometry {
227    ImportedGeometry(Box<ImportedGeometry>),
228    SolidSet(Vec<Solid>),
229}
230
231impl From<SolidOrImportedGeometry> for crate::execution::KclValue {
232    fn from(value: SolidOrImportedGeometry) -> Self {
233        match value {
234            SolidOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
235            SolidOrImportedGeometry::SolidSet(mut s) => {
236                if s.len() == 1 {
237                    crate::execution::KclValue::Solid {
238                        value: Box::new(s.pop().unwrap()),
239                    }
240                } else {
241                    crate::execution::KclValue::HomArray {
242                        value: s
243                            .into_iter()
244                            .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
245                            .collect(),
246                        ty: crate::execution::types::RuntimeType::solid(),
247                    }
248                }
249            }
250        }
251    }
252}
253
254impl SolidOrImportedGeometry {
255    pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
256        match self {
257            SolidOrImportedGeometry::ImportedGeometry(s) => {
258                let id = s.id(ctx).await?;
259
260                Ok(vec![id])
261            }
262            SolidOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
263        }
264    }
265}
266
267/// A helix.
268#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
269#[ts(export)]
270#[serde(rename_all = "camelCase")]
271pub struct Helix {
272    /// The id of the helix.
273    pub value: uuid::Uuid,
274    /// The artifact ID.
275    pub artifact_id: ArtifactId,
276    /// Number of revolutions.
277    pub revolutions: f64,
278    /// Start angle (in degrees).
279    pub angle_start: f64,
280    /// Is the helix rotation counter clockwise?
281    pub ccw: bool,
282    /// The cylinder the helix was created on.
283    pub cylinder_id: Option<uuid::Uuid>,
284    pub units: UnitLength,
285    #[serde(skip)]
286    pub meta: Vec<Metadata>,
287}
288
289#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
290#[ts(export)]
291#[serde(rename_all = "camelCase")]
292pub struct Plane {
293    /// The id of the plane.
294    pub id: uuid::Uuid,
295    /// The artifact ID.
296    pub artifact_id: ArtifactId,
297    // The code for the plane either a string or custom.
298    pub value: PlaneType,
299    /// The information for the plane.
300    #[serde(flatten)]
301    pub info: PlaneInfo,
302    #[serde(skip)]
303    pub meta: Vec<Metadata>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
307#[ts(export)]
308#[serde(rename_all = "camelCase")]
309pub struct PlaneInfo {
310    /// Origin of the plane.
311    pub origin: Point3d,
312    /// What should the plane's X axis be?
313    pub x_axis: Point3d,
314    /// What should the plane's Y axis be?
315    pub y_axis: Point3d,
316    /// What should the plane's Z axis be?
317    pub z_axis: Point3d,
318}
319
320impl PlaneInfo {
321    pub(crate) fn into_plane_data(self) -> PlaneData {
322        if self.origin.is_zero() {
323            match self {
324                Self {
325                    origin:
326                        Point3d {
327                            x: 0.0,
328                            y: 0.0,
329                            z: 0.0,
330                            units: Some(UnitLength::Millimeters),
331                        },
332                    x_axis:
333                        Point3d {
334                            x: 1.0,
335                            y: 0.0,
336                            z: 0.0,
337                            units: _,
338                        },
339                    y_axis:
340                        Point3d {
341                            x: 0.0,
342                            y: 1.0,
343                            z: 0.0,
344                            units: _,
345                        },
346                    z_axis: _,
347                } => return PlaneData::XY,
348                Self {
349                    origin:
350                        Point3d {
351                            x: 0.0,
352                            y: 0.0,
353                            z: 0.0,
354                            units: Some(UnitLength::Millimeters),
355                        },
356                    x_axis:
357                        Point3d {
358                            x: -1.0,
359                            y: 0.0,
360                            z: 0.0,
361                            units: _,
362                        },
363                    y_axis:
364                        Point3d {
365                            x: 0.0,
366                            y: 1.0,
367                            z: 0.0,
368                            units: _,
369                        },
370                    z_axis: _,
371                } => return PlaneData::NegXY,
372                Self {
373                    origin:
374                        Point3d {
375                            x: 0.0,
376                            y: 0.0,
377                            z: 0.0,
378                            units: Some(UnitLength::Millimeters),
379                        },
380                    x_axis:
381                        Point3d {
382                            x: 1.0,
383                            y: 0.0,
384                            z: 0.0,
385                            units: _,
386                        },
387                    y_axis:
388                        Point3d {
389                            x: 0.0,
390                            y: 0.0,
391                            z: 1.0,
392                            units: _,
393                        },
394                    z_axis: _,
395                } => return PlaneData::XZ,
396                Self {
397                    origin:
398                        Point3d {
399                            x: 0.0,
400                            y: 0.0,
401                            z: 0.0,
402                            units: Some(UnitLength::Millimeters),
403                        },
404                    x_axis:
405                        Point3d {
406                            x: -1.0,
407                            y: 0.0,
408                            z: 0.0,
409                            units: _,
410                        },
411                    y_axis:
412                        Point3d {
413                            x: 0.0,
414                            y: 0.0,
415                            z: 1.0,
416                            units: _,
417                        },
418                    z_axis: _,
419                } => return PlaneData::NegXZ,
420                Self {
421                    origin:
422                        Point3d {
423                            x: 0.0,
424                            y: 0.0,
425                            z: 0.0,
426                            units: Some(UnitLength::Millimeters),
427                        },
428                    x_axis:
429                        Point3d {
430                            x: 0.0,
431                            y: 1.0,
432                            z: 0.0,
433                            units: _,
434                        },
435                    y_axis:
436                        Point3d {
437                            x: 0.0,
438                            y: 0.0,
439                            z: 1.0,
440                            units: _,
441                        },
442                    z_axis: _,
443                } => return PlaneData::YZ,
444                Self {
445                    origin:
446                        Point3d {
447                            x: 0.0,
448                            y: 0.0,
449                            z: 0.0,
450                            units: Some(UnitLength::Millimeters),
451                        },
452                    x_axis:
453                        Point3d {
454                            x: 0.0,
455                            y: -1.0,
456                            z: 0.0,
457                            units: _,
458                        },
459                    y_axis:
460                        Point3d {
461                            x: 0.0,
462                            y: 0.0,
463                            z: 1.0,
464                            units: _,
465                        },
466                    z_axis: _,
467                } => return PlaneData::NegYZ,
468                _ => {}
469            }
470        }
471
472        PlaneData::Plane(Self {
473            origin: self.origin,
474            x_axis: self.x_axis,
475            y_axis: self.y_axis,
476            z_axis: self.z_axis,
477        })
478    }
479
480    pub(crate) fn is_right_handed(&self) -> bool {
481        // Katie's formula:
482        // dot(cross(x, y), z) ~= sqrt(dot(x, x) * dot(y, y) * dot(z, z))
483        let lhs = self
484            .x_axis
485            .axes_cross_product(&self.y_axis)
486            .axes_dot_product(&self.z_axis);
487        let rhs_x = self.x_axis.axes_dot_product(&self.x_axis);
488        let rhs_y = self.y_axis.axes_dot_product(&self.y_axis);
489        let rhs_z = self.z_axis.axes_dot_product(&self.z_axis);
490        let rhs = (rhs_x * rhs_y * rhs_z).sqrt();
491        // Check LHS ~= RHS
492        (lhs - rhs).abs() <= 0.0001
493    }
494
495    #[cfg(test)]
496    pub(crate) fn is_left_handed(&self) -> bool {
497        !self.is_right_handed()
498    }
499
500    pub(crate) fn make_right_handed(self) -> Self {
501        if self.is_right_handed() {
502            return self;
503        }
504        // To make it right-handed, negate X, i.e. rotate the plane 180 degrees.
505        Self {
506            origin: self.origin,
507            x_axis: self.x_axis.negated(),
508            y_axis: self.y_axis,
509            z_axis: self.z_axis,
510        }
511    }
512}
513
514impl TryFrom<PlaneData> for PlaneInfo {
515    type Error = KclError;
516
517    fn try_from(value: PlaneData) -> Result<Self, Self::Error> {
518        if let PlaneData::Plane(info) = value {
519            return Ok(info);
520        }
521        let name = match value {
522            PlaneData::XY => PlaneName::Xy,
523            PlaneData::NegXY => PlaneName::NegXy,
524            PlaneData::XZ => PlaneName::Xz,
525            PlaneData::NegXZ => PlaneName::NegXz,
526            PlaneData::YZ => PlaneName::Yz,
527            PlaneData::NegYZ => PlaneName::NegYz,
528            PlaneData::Plane(_) => {
529                // We will never get here since we already checked for PlaneData::Plane.
530                return Err(KclError::new_internal(KclErrorDetails::new(
531                    format!("PlaneData {value:?} not found"),
532                    Default::default(),
533                )));
534            }
535        };
536
537        let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
538            KclError::new_internal(KclErrorDetails::new(
539                format!("Plane {name} not found"),
540                Default::default(),
541            ))
542        })?;
543
544        Ok(info.clone())
545    }
546}
547
548impl From<PlaneData> for PlaneType {
549    fn from(value: PlaneData) -> Self {
550        match value {
551            PlaneData::XY => PlaneType::XY,
552            PlaneData::NegXY => PlaneType::XY,
553            PlaneData::XZ => PlaneType::XZ,
554            PlaneData::NegXZ => PlaneType::XZ,
555            PlaneData::YZ => PlaneType::YZ,
556            PlaneData::NegYZ => PlaneType::YZ,
557            PlaneData::Plane(_) => PlaneType::Custom,
558        }
559    }
560}
561
562impl Plane {
563    pub(crate) fn from_plane_data(value: PlaneData, exec_state: &mut ExecState) -> Result<Self, KclError> {
564        let id = exec_state.next_uuid();
565        Ok(Plane {
566            id,
567            artifact_id: id.into(),
568            info: PlaneInfo::try_from(value.clone())?,
569            value: value.into(),
570            meta: vec![],
571        })
572    }
573
574    /// The standard planes are XY, YZ and XZ (in both positive and negative)
575    pub fn is_standard(&self) -> bool {
576        !matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
577    }
578
579    /// Project a point onto a plane by calculating how far away it is and moving it along the
580    /// normal of the plane so that it now lies on the plane.
581    pub fn project(&self, point: Point3d) -> Point3d {
582        let v = point - self.info.origin;
583        let dot = v.axes_dot_product(&self.info.z_axis);
584
585        point - self.info.z_axis * dot
586    }
587}
588
589/// A face.
590#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
591#[ts(export)]
592#[serde(rename_all = "camelCase")]
593pub struct Face {
594    /// The id of the face.
595    pub id: uuid::Uuid,
596    /// The artifact ID.
597    pub artifact_id: ArtifactId,
598    /// The tag of the face.
599    pub value: String,
600    /// What should the face's X axis be?
601    pub x_axis: Point3d,
602    /// What should the face's Y axis be?
603    pub y_axis: Point3d,
604    /// The solid the face is on.
605    pub solid: Box<Solid>,
606    pub units: UnitLength,
607    #[serde(skip)]
608    pub meta: Vec<Metadata>,
609}
610
611/// Type for a plane.
612#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, FromStr, Display)]
613#[ts(export)]
614#[display(style = "camelCase")]
615pub enum PlaneType {
616    #[serde(rename = "XY", alias = "xy")]
617    #[display("XY")]
618    XY,
619    #[serde(rename = "XZ", alias = "xz")]
620    #[display("XZ")]
621    XZ,
622    #[serde(rename = "YZ", alias = "yz")]
623    #[display("YZ")]
624    YZ,
625    /// A custom plane.
626    #[display("Custom")]
627    Custom,
628    /// A custom plane which has not been sent to the engine. It must be sent before it is used.
629    #[display("Uninit")]
630    Uninit,
631}
632
633#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
634#[ts(export)]
635#[serde(tag = "type", rename_all = "camelCase")]
636pub struct Sketch {
637    /// The id of the sketch (this will change when the engine's reference to it changes).
638    pub id: uuid::Uuid,
639    /// The paths in the sketch.
640    /// Only paths on the "outside" i.e. the perimeter.
641    /// Does not include paths "inside" the profile (for example, edges made by subtracting a profile)
642    pub paths: Vec<Path>,
643    /// Inner paths, resulting from subtract2d to carve profiles out of the sketch.
644    #[serde(default, skip_serializing_if = "Vec::is_empty")]
645    pub inner_paths: Vec<Path>,
646    /// What the sketch is on (can be a plane or a face).
647    pub on: SketchSurface,
648    /// The starting path.
649    pub start: BasePath,
650    /// Tag identifiers that have been declared in this sketch.
651    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
652    pub tags: IndexMap<String, TagIdentifier>,
653    /// The original id of the sketch. This stays the same even if the sketch is
654    /// is sketched on face etc.
655    pub artifact_id: ArtifactId,
656    #[ts(skip)]
657    pub original_id: uuid::Uuid,
658    /// If the sketch includes a mirror.
659    #[serde(skip)]
660    pub mirror: Option<uuid::Uuid>,
661    /// If the sketch is a clone of another sketch.
662    #[serde(skip)]
663    pub clone: Option<uuid::Uuid>,
664    pub units: UnitLength,
665    /// Metadata.
666    #[serde(skip)]
667    pub meta: Vec<Metadata>,
668    /// If not given, defaults to true.
669    #[serde(default = "very_true", skip_serializing_if = "is_true")]
670    pub is_closed: bool,
671}
672
673fn is_true(b: &bool) -> bool {
674    *b
675}
676
677impl Sketch {
678    // Tell the engine to enter sketch mode on the sketch.
679    // Run a specific command, then exit sketch mode.
680    pub(crate) fn build_sketch_mode_cmds(
681        &self,
682        exec_state: &mut ExecState,
683        inner_cmd: ModelingCmdReq,
684    ) -> Vec<ModelingCmdReq> {
685        vec![
686            // Before we extrude, we need to enable the sketch mode.
687            // We do this here in case extrude is called out of order.
688            ModelingCmdReq {
689                cmd: ModelingCmd::from(mcmd::EnableSketchMode {
690                    animated: false,
691                    ortho: false,
692                    entity_id: self.on.id(),
693                    adjust_camera: false,
694                    planar_normal: if let SketchSurface::Plane(plane) = &self.on {
695                        // We pass in the normal for the plane here.
696                        let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
697                        Some(normal.into())
698                    } else {
699                        None
700                    },
701                }),
702                cmd_id: exec_state.next_uuid().into(),
703            },
704            inner_cmd,
705            ModelingCmdReq {
706                cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
707                cmd_id: exec_state.next_uuid().into(),
708            },
709        ]
710    }
711}
712
713/// A sketch type.
714#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
715#[ts(export)]
716#[serde(tag = "type", rename_all = "camelCase")]
717pub enum SketchSurface {
718    Plane(Box<Plane>),
719    Face(Box<Face>),
720}
721
722impl SketchSurface {
723    pub(crate) fn id(&self) -> uuid::Uuid {
724        match self {
725            SketchSurface::Plane(plane) => plane.id,
726            SketchSurface::Face(face) => face.id,
727        }
728    }
729    pub(crate) fn x_axis(&self) -> Point3d {
730        match self {
731            SketchSurface::Plane(plane) => plane.info.x_axis,
732            SketchSurface::Face(face) => face.x_axis,
733        }
734    }
735    pub(crate) fn y_axis(&self) -> Point3d {
736        match self {
737            SketchSurface::Plane(plane) => plane.info.y_axis,
738            SketchSurface::Face(face) => face.y_axis,
739        }
740    }
741}
742
743#[derive(Debug, Clone)]
744pub(crate) enum GetTangentialInfoFromPathsResult {
745    PreviousPoint([f64; 2]),
746    Arc {
747        center: [f64; 2],
748        ccw: bool,
749    },
750    Circle {
751        center: [f64; 2],
752        ccw: bool,
753        radius: f64,
754    },
755    Ellipse {
756        center: [f64; 2],
757        ccw: bool,
758        major_axis: [f64; 2],
759        _minor_radius: f64,
760    },
761}
762
763impl GetTangentialInfoFromPathsResult {
764    pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
765        match self {
766            GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
767            GetTangentialInfoFromPathsResult::Arc { center, ccw } => {
768                crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
769            }
770            // The circle always starts at 0 degrees, so a suitable tangent
771            // point is either directly above or below.
772            GetTangentialInfoFromPathsResult::Circle {
773                center, radius, ccw, ..
774            } => [center[0] + radius, center[1] + if *ccw { -1.0 } else { 1.0 }],
775            GetTangentialInfoFromPathsResult::Ellipse {
776                center,
777                major_axis,
778                ccw,
779                ..
780            } => [center[0] + major_axis[0], center[1] + if *ccw { -1.0 } else { 1.0 }],
781        }
782    }
783}
784
785impl Sketch {
786    pub(crate) fn add_tag(
787        &mut self,
788        tag: NodeRef<'_, TagDeclarator>,
789        current_path: &Path,
790        exec_state: &ExecState,
791        surface: Option<&ExtrudeSurface>,
792    ) {
793        let mut tag_identifier: TagIdentifier = tag.into();
794        let base = current_path.get_base();
795        tag_identifier.info.push((
796            exec_state.stack().current_epoch(),
797            TagEngineInfo {
798                id: base.geo_meta.id,
799                sketch: self.id,
800                path: Some(current_path.clone()),
801                surface: surface.cloned(),
802            },
803        ));
804
805        self.tags.insert(tag.name.to_string(), tag_identifier);
806    }
807
808    pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
809        for t in tags {
810            match self.tags.get_mut(&t.value) {
811                Some(id) => {
812                    id.merge_info(t);
813                }
814                None => {
815                    self.tags.insert(t.value.clone(), t.clone());
816                }
817            }
818        }
819    }
820
821    /// Get the path most recently sketched.
822    pub(crate) fn latest_path(&self) -> Option<&Path> {
823        self.paths.last()
824    }
825
826    /// The "pen" is an imaginary pen drawing the path.
827    /// This gets the current point the pen is hovering over, i.e. the point
828    /// where the last path segment ends, and the next path segment will begin.
829    pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
830        let Some(path) = self.latest_path() else {
831            return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units));
832        };
833
834        let to = path.get_base().to;
835        Ok(Point2d::new(to[0], to[1], path.get_base().units))
836    }
837
838    pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
839        let Some(path) = self.latest_path() else {
840            return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
841        };
842        path.get_tangential_info()
843    }
844}
845
846#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
847#[ts(export)]
848#[serde(tag = "type", rename_all = "camelCase")]
849pub struct Solid {
850    /// The id of the solid.
851    pub id: uuid::Uuid,
852    /// The artifact ID of the solid.  Unlike `id`, this doesn't change.
853    pub artifact_id: ArtifactId,
854    /// The extrude surfaces.
855    pub value: Vec<ExtrudeSurface>,
856    /// The sketch.
857    pub sketch: Sketch,
858    /// The id of the extrusion start cap
859    pub start_cap_id: Option<uuid::Uuid>,
860    /// The id of the extrusion end cap
861    pub end_cap_id: Option<uuid::Uuid>,
862    /// Chamfers or fillets on this solid.
863    #[serde(default, skip_serializing_if = "Vec::is_empty")]
864    pub edge_cuts: Vec<EdgeCut>,
865    /// The units of the solid.
866    pub units: UnitLength,
867    /// Is this a sectional solid?
868    pub sectional: bool,
869    /// Metadata.
870    #[serde(skip)]
871    pub meta: Vec<Metadata>,
872}
873
874impl Solid {
875    pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
876        self.edge_cuts.iter().map(|foc| foc.id())
877    }
878}
879
880/// A fillet or a chamfer.
881#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
882#[ts(export)]
883#[serde(tag = "type", rename_all = "camelCase")]
884pub enum EdgeCut {
885    /// A fillet.
886    Fillet {
887        /// The id of the engine command that called this fillet.
888        id: uuid::Uuid,
889        radius: TyF64,
890        /// The engine id of the edge to fillet.
891        #[serde(rename = "edgeId")]
892        edge_id: uuid::Uuid,
893        tag: Box<Option<TagNode>>,
894    },
895    /// A chamfer.
896    Chamfer {
897        /// The id of the engine command that called this chamfer.
898        id: uuid::Uuid,
899        length: TyF64,
900        /// The engine id of the edge to chamfer.
901        #[serde(rename = "edgeId")]
902        edge_id: uuid::Uuid,
903        tag: Box<Option<TagNode>>,
904    },
905}
906
907impl EdgeCut {
908    pub fn id(&self) -> uuid::Uuid {
909        match self {
910            EdgeCut::Fillet { id, .. } => *id,
911            EdgeCut::Chamfer { id, .. } => *id,
912        }
913    }
914
915    pub fn set_id(&mut self, id: uuid::Uuid) {
916        match self {
917            EdgeCut::Fillet { id: i, .. } => *i = id,
918            EdgeCut::Chamfer { id: i, .. } => *i = id,
919        }
920    }
921
922    pub fn edge_id(&self) -> uuid::Uuid {
923        match self {
924            EdgeCut::Fillet { edge_id, .. } => *edge_id,
925            EdgeCut::Chamfer { edge_id, .. } => *edge_id,
926        }
927    }
928
929    pub fn set_edge_id(&mut self, id: uuid::Uuid) {
930        match self {
931            EdgeCut::Fillet { edge_id: i, .. } => *i = id,
932            EdgeCut::Chamfer { edge_id: i, .. } => *i = id,
933        }
934    }
935
936    pub fn tag(&self) -> Option<TagNode> {
937        match self {
938            EdgeCut::Fillet { tag, .. } => *tag.clone(),
939            EdgeCut::Chamfer { tag, .. } => *tag.clone(),
940        }
941    }
942}
943
944#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS)]
945#[ts(export)]
946pub struct Point2d {
947    pub x: f64,
948    pub y: f64,
949    pub units: UnitLength,
950}
951
952impl Point2d {
953    pub const ZERO: Self = Self {
954        x: 0.0,
955        y: 0.0,
956        units: UnitLength::Millimeters,
957    };
958
959    pub fn new(x: f64, y: f64, units: UnitLength) -> Self {
960        Self { x, y, units }
961    }
962
963    pub fn into_x(self) -> TyF64 {
964        TyF64::new(self.x, self.units.into())
965    }
966
967    pub fn into_y(self) -> TyF64 {
968        TyF64::new(self.y, self.units.into())
969    }
970
971    pub fn ignore_units(self) -> [f64; 2] {
972        [self.x, self.y]
973    }
974}
975
976#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, Default)]
977#[ts(export)]
978pub struct Point3d {
979    pub x: f64,
980    pub y: f64,
981    pub z: f64,
982    pub units: Option<UnitLength>,
983}
984
985impl Point3d {
986    pub const ZERO: Self = Self {
987        x: 0.0,
988        y: 0.0,
989        z: 0.0,
990        units: Some(UnitLength::Millimeters),
991    };
992
993    pub fn new(x: f64, y: f64, z: f64, units: Option<UnitLength>) -> Self {
994        Self { x, y, z, units }
995    }
996
997    pub const fn is_zero(&self) -> bool {
998        self.x == 0.0 && self.y == 0.0 && self.z == 0.0
999    }
1000
1001    /// Calculate the cross product of this vector with another.
1002    ///
1003    /// This should only be applied to axes or other vectors which represent only a direction (and
1004    /// no magnitude) since units are ignored.
1005    pub fn axes_cross_product(&self, other: &Self) -> Self {
1006        Self {
1007            x: self.y * other.z - self.z * other.y,
1008            y: self.z * other.x - self.x * other.z,
1009            z: self.x * other.y - self.y * other.x,
1010            units: None,
1011        }
1012    }
1013
1014    /// Calculate the dot product of this vector with another.
1015    ///
1016    /// This should only be applied to axes or other vectors which represent only a direction (and
1017    /// no magnitude) since units are ignored.
1018    pub fn axes_dot_product(&self, other: &Self) -> f64 {
1019        let x = self.x * other.x;
1020        let y = self.y * other.y;
1021        let z = self.z * other.z;
1022        x + y + z
1023    }
1024
1025    pub fn normalize(&self) -> Self {
1026        let len = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
1027        Point3d {
1028            x: self.x / len,
1029            y: self.y / len,
1030            z: self.z / len,
1031            units: None,
1032        }
1033    }
1034
1035    pub fn as_3_dims(&self) -> ([f64; 3], Option<UnitLength>) {
1036        let p = [self.x, self.y, self.z];
1037        let u = self.units;
1038        (p, u)
1039    }
1040
1041    pub(crate) fn negated(self) -> Self {
1042        Self {
1043            x: -self.x,
1044            y: -self.y,
1045            z: -self.z,
1046            units: self.units,
1047        }
1048    }
1049}
1050
1051impl From<[TyF64; 3]> for Point3d {
1052    fn from(p: [TyF64; 3]) -> Self {
1053        Self {
1054            x: p[0].n,
1055            y: p[1].n,
1056            z: p[2].n,
1057            units: p[0].ty.as_length(),
1058        }
1059    }
1060}
1061
1062impl From<Point3d> for Point3D {
1063    fn from(p: Point3d) -> Self {
1064        Self { x: p.x, y: p.y, z: p.z }
1065    }
1066}
1067
1068impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
1069    fn from(p: Point3d) -> Self {
1070        if let Some(units) = p.units {
1071            Self {
1072                x: LengthUnit(adjust_length(units, p.x, UnitLength::Millimeters).0),
1073                y: LengthUnit(adjust_length(units, p.y, UnitLength::Millimeters).0),
1074                z: LengthUnit(adjust_length(units, p.z, UnitLength::Millimeters).0),
1075            }
1076        } else {
1077            Self {
1078                x: LengthUnit(p.x),
1079                y: LengthUnit(p.y),
1080                z: LengthUnit(p.z),
1081            }
1082        }
1083    }
1084}
1085
1086impl Add for Point3d {
1087    type Output = Point3d;
1088
1089    fn add(self, rhs: Self) -> Self::Output {
1090        // TODO should assert that self and rhs the same units or coerce them
1091        Point3d {
1092            x: self.x + rhs.x,
1093            y: self.y + rhs.y,
1094            z: self.z + rhs.z,
1095            units: self.units,
1096        }
1097    }
1098}
1099
1100impl AddAssign for Point3d {
1101    fn add_assign(&mut self, rhs: Self) {
1102        *self = *self + rhs
1103    }
1104}
1105
1106impl Sub for Point3d {
1107    type Output = Point3d;
1108
1109    fn sub(self, rhs: Self) -> Self::Output {
1110        let (x, y, z) = if rhs.units != self.units
1111            && let Some(sunits) = self.units
1112            && let Some(runits) = rhs.units
1113        {
1114            (
1115                adjust_length(runits, rhs.x, sunits).0,
1116                adjust_length(runits, rhs.y, sunits).0,
1117                adjust_length(runits, rhs.z, sunits).0,
1118            )
1119        } else {
1120            (rhs.x, rhs.y, rhs.z)
1121        };
1122        Point3d {
1123            x: self.x - x,
1124            y: self.y - y,
1125            z: self.z - z,
1126            units: self.units,
1127        }
1128    }
1129}
1130
1131impl SubAssign for Point3d {
1132    fn sub_assign(&mut self, rhs: Self) {
1133        *self = *self - rhs
1134    }
1135}
1136
1137impl Mul<f64> for Point3d {
1138    type Output = Point3d;
1139
1140    fn mul(self, rhs: f64) -> Self::Output {
1141        Point3d {
1142            x: self.x * rhs,
1143            y: self.y * rhs,
1144            z: self.z * rhs,
1145            units: self.units,
1146        }
1147    }
1148}
1149
1150/// A base path.
1151#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1152#[ts(export)]
1153#[serde(rename_all = "camelCase")]
1154pub struct BasePath {
1155    /// The from point.
1156    #[ts(type = "[number, number]")]
1157    pub from: [f64; 2],
1158    /// The to point.
1159    #[ts(type = "[number, number]")]
1160    pub to: [f64; 2],
1161    pub units: UnitLength,
1162    /// The tag of the path.
1163    pub tag: Option<TagNode>,
1164    /// Metadata.
1165    #[serde(rename = "__geoMeta")]
1166    pub geo_meta: GeoMeta,
1167}
1168
1169impl BasePath {
1170    pub fn get_to(&self) -> [TyF64; 2] {
1171        let ty: NumericType = self.units.into();
1172        [TyF64::new(self.to[0], ty), TyF64::new(self.to[1], ty)]
1173    }
1174
1175    pub fn get_from(&self) -> [TyF64; 2] {
1176        let ty: NumericType = self.units.into();
1177        [TyF64::new(self.from[0], ty), TyF64::new(self.from[1], ty)]
1178    }
1179}
1180
1181/// Geometry metadata.
1182#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1183#[ts(export)]
1184#[serde(rename_all = "camelCase")]
1185pub struct GeoMeta {
1186    /// The id of the geometry.
1187    pub id: uuid::Uuid,
1188    /// Metadata.
1189    #[serde(flatten)]
1190    pub metadata: Metadata,
1191}
1192
1193/// A path.
1194#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1195#[ts(export)]
1196#[serde(tag = "type")]
1197pub enum Path {
1198    /// A straight line which ends at the given point.
1199    ToPoint {
1200        #[serde(flatten)]
1201        base: BasePath,
1202    },
1203    /// A arc that is tangential to the last path segment that goes to a point
1204    TangentialArcTo {
1205        #[serde(flatten)]
1206        base: BasePath,
1207        /// the arc's center
1208        #[ts(type = "[number, number]")]
1209        center: [f64; 2],
1210        /// arc's direction
1211        ccw: bool,
1212    },
1213    /// A arc that is tangential to the last path segment
1214    TangentialArc {
1215        #[serde(flatten)]
1216        base: BasePath,
1217        /// the arc's center
1218        #[ts(type = "[number, number]")]
1219        center: [f64; 2],
1220        /// arc's direction
1221        ccw: bool,
1222    },
1223    // TODO: consolidate segment enums, remove Circle. https://github.com/KittyCAD/modeling-app/issues/3940
1224    /// a complete arc
1225    Circle {
1226        #[serde(flatten)]
1227        base: BasePath,
1228        /// the arc's center
1229        #[ts(type = "[number, number]")]
1230        center: [f64; 2],
1231        /// the arc's radius
1232        radius: f64,
1233        /// arc's direction
1234        /// This is used to compute the tangential angle.
1235        ccw: bool,
1236    },
1237    CircleThreePoint {
1238        #[serde(flatten)]
1239        base: BasePath,
1240        /// Point 1 of the circle
1241        #[ts(type = "[number, number]")]
1242        p1: [f64; 2],
1243        /// Point 2 of the circle
1244        #[ts(type = "[number, number]")]
1245        p2: [f64; 2],
1246        /// Point 3 of the circle
1247        #[ts(type = "[number, number]")]
1248        p3: [f64; 2],
1249    },
1250    ArcThreePoint {
1251        #[serde(flatten)]
1252        base: BasePath,
1253        /// Point 1 of the arc (base on the end of previous segment)
1254        #[ts(type = "[number, number]")]
1255        p1: [f64; 2],
1256        /// Point 2 of the arc (interiorAbsolute kwarg)
1257        #[ts(type = "[number, number]")]
1258        p2: [f64; 2],
1259        /// Point 3 of the arc (endAbsolute kwarg)
1260        #[ts(type = "[number, number]")]
1261        p3: [f64; 2],
1262    },
1263    /// A path that is horizontal.
1264    Horizontal {
1265        #[serde(flatten)]
1266        base: BasePath,
1267        /// The x coordinate.
1268        x: f64,
1269    },
1270    /// An angled line to.
1271    AngledLineTo {
1272        #[serde(flatten)]
1273        base: BasePath,
1274        /// The x coordinate.
1275        x: Option<f64>,
1276        /// The y coordinate.
1277        y: Option<f64>,
1278    },
1279    /// A base path.
1280    Base {
1281        #[serde(flatten)]
1282        base: BasePath,
1283    },
1284    /// A circular arc, not necessarily tangential to the current point.
1285    Arc {
1286        #[serde(flatten)]
1287        base: BasePath,
1288        /// Center of the circle that this arc is drawn on.
1289        center: [f64; 2],
1290        /// Radius of the circle that this arc is drawn on.
1291        radius: f64,
1292        /// True if the arc is counterclockwise.
1293        ccw: bool,
1294    },
1295    Ellipse {
1296        #[serde(flatten)]
1297        base: BasePath,
1298        center: [f64; 2],
1299        major_axis: [f64; 2],
1300        minor_radius: f64,
1301        ccw: bool,
1302    },
1303    //TODO: (bc) figure this out
1304    Conic {
1305        #[serde(flatten)]
1306        base: BasePath,
1307    },
1308}
1309
1310impl Path {
1311    pub fn get_id(&self) -> uuid::Uuid {
1312        match self {
1313            Path::ToPoint { base } => base.geo_meta.id,
1314            Path::Horizontal { base, .. } => base.geo_meta.id,
1315            Path::AngledLineTo { base, .. } => base.geo_meta.id,
1316            Path::Base { base } => base.geo_meta.id,
1317            Path::TangentialArcTo { base, .. } => base.geo_meta.id,
1318            Path::TangentialArc { base, .. } => base.geo_meta.id,
1319            Path::Circle { base, .. } => base.geo_meta.id,
1320            Path::CircleThreePoint { base, .. } => base.geo_meta.id,
1321            Path::Arc { base, .. } => base.geo_meta.id,
1322            Path::ArcThreePoint { base, .. } => base.geo_meta.id,
1323            Path::Ellipse { base, .. } => base.geo_meta.id,
1324            Path::Conic { base, .. } => base.geo_meta.id,
1325        }
1326    }
1327
1328    pub fn set_id(&mut self, id: uuid::Uuid) {
1329        match self {
1330            Path::ToPoint { base } => base.geo_meta.id = id,
1331            Path::Horizontal { base, .. } => base.geo_meta.id = id,
1332            Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
1333            Path::Base { base } => base.geo_meta.id = id,
1334            Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
1335            Path::TangentialArc { base, .. } => base.geo_meta.id = id,
1336            Path::Circle { base, .. } => base.geo_meta.id = id,
1337            Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
1338            Path::Arc { base, .. } => base.geo_meta.id = id,
1339            Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
1340            Path::Ellipse { base, .. } => base.geo_meta.id = id,
1341            Path::Conic { base, .. } => base.geo_meta.id = id,
1342        }
1343    }
1344
1345    pub fn get_tag(&self) -> Option<TagNode> {
1346        match self {
1347            Path::ToPoint { base } => base.tag.clone(),
1348            Path::Horizontal { base, .. } => base.tag.clone(),
1349            Path::AngledLineTo { base, .. } => base.tag.clone(),
1350            Path::Base { base } => base.tag.clone(),
1351            Path::TangentialArcTo { base, .. } => base.tag.clone(),
1352            Path::TangentialArc { base, .. } => base.tag.clone(),
1353            Path::Circle { base, .. } => base.tag.clone(),
1354            Path::CircleThreePoint { base, .. } => base.tag.clone(),
1355            Path::Arc { base, .. } => base.tag.clone(),
1356            Path::ArcThreePoint { base, .. } => base.tag.clone(),
1357            Path::Ellipse { base, .. } => base.tag.clone(),
1358            Path::Conic { base, .. } => base.tag.clone(),
1359        }
1360    }
1361
1362    pub fn get_base(&self) -> &BasePath {
1363        match self {
1364            Path::ToPoint { base } => base,
1365            Path::Horizontal { base, .. } => base,
1366            Path::AngledLineTo { base, .. } => base,
1367            Path::Base { base } => base,
1368            Path::TangentialArcTo { base, .. } => base,
1369            Path::TangentialArc { base, .. } => base,
1370            Path::Circle { base, .. } => base,
1371            Path::CircleThreePoint { base, .. } => base,
1372            Path::Arc { base, .. } => base,
1373            Path::ArcThreePoint { base, .. } => base,
1374            Path::Ellipse { base, .. } => base,
1375            Path::Conic { base, .. } => base,
1376        }
1377    }
1378
1379    /// Where does this path segment start?
1380    pub fn get_from(&self) -> [TyF64; 2] {
1381        let p = &self.get_base().from;
1382        let ty: NumericType = self.get_base().units.into();
1383        [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1384    }
1385
1386    /// Where does this path segment end?
1387    pub fn get_to(&self) -> [TyF64; 2] {
1388        let p = &self.get_base().to;
1389        let ty: NumericType = self.get_base().units.into();
1390        [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1391    }
1392
1393    /// The path segment start point and its type.
1394    pub fn start_point_components(&self) -> ([f64; 2], NumericType) {
1395        let p = &self.get_base().from;
1396        let ty: NumericType = self.get_base().units.into();
1397        (*p, ty)
1398    }
1399
1400    /// The path segment end point and its type.
1401    pub fn end_point_components(&self) -> ([f64; 2], NumericType) {
1402        let p = &self.get_base().to;
1403        let ty: NumericType = self.get_base().units.into();
1404        (*p, ty)
1405    }
1406
1407    /// Length of this path segment, in cartesian plane. Not all segment types
1408    /// are supported.
1409    pub fn length(&self) -> Option<TyF64> {
1410        let n = match self {
1411            Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
1412                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1413            }
1414            Self::TangentialArc {
1415                base: _,
1416                center,
1417                ccw: _,
1418            }
1419            | Self::TangentialArcTo {
1420                base: _,
1421                center,
1422                ccw: _,
1423            } => {
1424                // The radius can be calculated as the linear distance between `to` and `center`,
1425                // or between `from` and `center`. They should be the same.
1426                let radius = linear_distance(&self.get_base().from, center);
1427                debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
1428                // TODO: Call engine utils to figure this out.
1429                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1430            }
1431            Self::Circle { radius, .. } => Some(2.0 * std::f64::consts::PI * radius),
1432            Self::CircleThreePoint { .. } => {
1433                let circle_center = crate::std::utils::calculate_circle_from_3_points([
1434                    self.get_base().from,
1435                    self.get_base().to,
1436                    self.get_base().to,
1437                ]);
1438                let radius = linear_distance(
1439                    &[circle_center.center[0], circle_center.center[1]],
1440                    &self.get_base().from,
1441                );
1442                Some(2.0 * std::f64::consts::PI * radius)
1443            }
1444            Self::Arc { .. } => {
1445                // TODO: Call engine utils to figure this out.
1446                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1447            }
1448            Self::ArcThreePoint { .. } => {
1449                // TODO: Call engine utils to figure this out.
1450                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1451            }
1452            Self::Ellipse { .. } => {
1453                // Not supported.
1454                None
1455            }
1456            Self::Conic { .. } => {
1457                // Not supported.
1458                None
1459            }
1460        };
1461        n.map(|n| TyF64::new(n, self.get_base().units.into()))
1462    }
1463
1464    pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1465        match self {
1466            Path::ToPoint { base } => Some(base),
1467            Path::Horizontal { base, .. } => Some(base),
1468            Path::AngledLineTo { base, .. } => Some(base),
1469            Path::Base { base } => Some(base),
1470            Path::TangentialArcTo { base, .. } => Some(base),
1471            Path::TangentialArc { base, .. } => Some(base),
1472            Path::Circle { base, .. } => Some(base),
1473            Path::CircleThreePoint { base, .. } => Some(base),
1474            Path::Arc { base, .. } => Some(base),
1475            Path::ArcThreePoint { base, .. } => Some(base),
1476            Path::Ellipse { base, .. } => Some(base),
1477            Path::Conic { base, .. } => Some(base),
1478        }
1479    }
1480
1481    pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1482        match self {
1483            Path::TangentialArc { center, ccw, .. }
1484            | Path::TangentialArcTo { center, ccw, .. }
1485            | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1486                center: *center,
1487                ccw: *ccw,
1488            },
1489            Path::ArcThreePoint { p1, p2, p3, .. } => {
1490                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1491                GetTangentialInfoFromPathsResult::Arc {
1492                    center: circle.center,
1493                    ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
1494                }
1495            }
1496            Path::Circle {
1497                center, ccw, radius, ..
1498            } => GetTangentialInfoFromPathsResult::Circle {
1499                center: *center,
1500                ccw: *ccw,
1501                radius: *radius,
1502            },
1503            Path::CircleThreePoint { p1, p2, p3, .. } => {
1504                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1505                let center_point = [circle.center[0], circle.center[1]];
1506                GetTangentialInfoFromPathsResult::Circle {
1507                    center: center_point,
1508                    // Note: a circle is always ccw regardless of the order of points
1509                    ccw: true,
1510                    radius: circle.radius,
1511                }
1512            }
1513            // TODO: (bc) fix me
1514            Path::Ellipse {
1515                center,
1516                major_axis,
1517                minor_radius,
1518                ccw,
1519                ..
1520            } => GetTangentialInfoFromPathsResult::Ellipse {
1521                center: *center,
1522                major_axis: *major_axis,
1523                _minor_radius: *minor_radius,
1524                ccw: *ccw,
1525            },
1526            Path::Conic { .. }
1527            | Path::ToPoint { .. }
1528            | Path::Horizontal { .. }
1529            | Path::AngledLineTo { .. }
1530            | Path::Base { .. } => {
1531                let base = self.get_base();
1532                GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1533            }
1534        }
1535    }
1536
1537    /// i.e. not a curve
1538    pub(crate) fn is_straight_line(&self) -> bool {
1539        matches!(self, Path::AngledLineTo { .. } | Path::ToPoint { .. })
1540    }
1541}
1542
1543/// Compute the straight-line distance between a pair of (2D) points.
1544#[rustfmt::skip]
1545fn linear_distance(
1546    [x0, y0]: &[f64; 2],
1547    [x1, y1]: &[f64; 2]
1548) -> f64 {
1549    let y_sq = (y1 - y0).powi(2);
1550    let x_sq = (x1 - x0).powi(2);
1551    (y_sq + x_sq).sqrt()
1552}
1553
1554/// An extrude surface.
1555#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1556#[ts(export)]
1557#[serde(tag = "type", rename_all = "camelCase")]
1558pub enum ExtrudeSurface {
1559    /// An extrude plane.
1560    ExtrudePlane(ExtrudePlane),
1561    ExtrudeArc(ExtrudeArc),
1562    Chamfer(ChamferSurface),
1563    Fillet(FilletSurface),
1564}
1565
1566// Chamfer surface.
1567#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1568#[ts(export)]
1569#[serde(rename_all = "camelCase")]
1570pub struct ChamferSurface {
1571    /// The id for the chamfer surface.
1572    pub face_id: uuid::Uuid,
1573    /// The tag.
1574    pub tag: Option<Node<TagDeclarator>>,
1575    /// Metadata.
1576    #[serde(flatten)]
1577    pub geo_meta: GeoMeta,
1578}
1579
1580// Fillet surface.
1581#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1582#[ts(export)]
1583#[serde(rename_all = "camelCase")]
1584pub struct FilletSurface {
1585    /// The id for the fillet surface.
1586    pub face_id: uuid::Uuid,
1587    /// The tag.
1588    pub tag: Option<Node<TagDeclarator>>,
1589    /// Metadata.
1590    #[serde(flatten)]
1591    pub geo_meta: GeoMeta,
1592}
1593
1594/// An extruded plane.
1595#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1596#[ts(export)]
1597#[serde(rename_all = "camelCase")]
1598pub struct ExtrudePlane {
1599    /// The face id for the extrude plane.
1600    pub face_id: uuid::Uuid,
1601    /// The tag.
1602    pub tag: Option<Node<TagDeclarator>>,
1603    /// Metadata.
1604    #[serde(flatten)]
1605    pub geo_meta: GeoMeta,
1606}
1607
1608/// An extruded arc.
1609#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1610#[ts(export)]
1611#[serde(rename_all = "camelCase")]
1612pub struct ExtrudeArc {
1613    /// The face id for the extrude plane.
1614    pub face_id: uuid::Uuid,
1615    /// The tag.
1616    pub tag: Option<Node<TagDeclarator>>,
1617    /// Metadata.
1618    #[serde(flatten)]
1619    pub geo_meta: GeoMeta,
1620}
1621
1622impl ExtrudeSurface {
1623    pub fn get_id(&self) -> uuid::Uuid {
1624        match self {
1625            ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1626            ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1627            ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1628            ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1629        }
1630    }
1631
1632    pub fn face_id(&self) -> uuid::Uuid {
1633        match self {
1634            ExtrudeSurface::ExtrudePlane(ep) => ep.face_id,
1635            ExtrudeSurface::ExtrudeArc(ea) => ea.face_id,
1636            ExtrudeSurface::Fillet(f) => f.face_id,
1637            ExtrudeSurface::Chamfer(c) => c.face_id,
1638        }
1639    }
1640
1641    pub fn set_face_id(&mut self, face_id: uuid::Uuid) {
1642        match self {
1643            ExtrudeSurface::ExtrudePlane(ep) => ep.face_id = face_id,
1644            ExtrudeSurface::ExtrudeArc(ea) => ea.face_id = face_id,
1645            ExtrudeSurface::Fillet(f) => f.face_id = face_id,
1646            ExtrudeSurface::Chamfer(c) => c.face_id = face_id,
1647        }
1648    }
1649
1650    pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1651        match self {
1652            ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1653            ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1654            ExtrudeSurface::Fillet(f) => f.tag.clone(),
1655            ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1656        }
1657    }
1658}
1659
1660#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, ts_rs::TS)]
1661pub struct SketchVarId(pub usize);
1662
1663impl SketchVarId {
1664    pub fn to_constraint_id(self, range: SourceRange) -> Result<kcl_ezpz::Id, KclError> {
1665        self.0.try_into().map_err(|_| {
1666            KclError::new_type(KclErrorDetails::new(
1667                "Cannot convert to constraint ID since the sketch variable ID is too large".to_owned(),
1668                vec![range],
1669            ))
1670        })
1671    }
1672}
1673
1674#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1675#[ts(export_to = "Geometry.ts")]
1676#[serde(rename_all = "camelCase")]
1677pub struct SketchVar {
1678    pub id: SketchVarId,
1679    pub initial_value: f64,
1680    pub ty: NumericType,
1681    #[serde(skip)]
1682    pub meta: Vec<Metadata>,
1683}