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