Skip to main content

kcl_lib/execution/
geometry.rs

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