Skip to main content

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