Skip to main content

kcl_lib/execution/
geometry.rs

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